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,979 @@
1
+ """SpreadsheetBench rollout — codegen & ReAct batch execution.
2
+
3
+ Provides:
4
+ - process_one_codegen(): single/multi-round code generation (no tool-call)
5
+ - run_spreadsheet_batch_codegen(): batch wrapper for codegen
6
+ - process_one(): ReAct agent with tool-call (legacy)
7
+ - run_spreadsheet_batch(): batch wrapper for ReAct (legacy)
8
+ - load_items(): load benchmark .json/.jsonl files
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import glob as _glob
13
+ import json
14
+ import os
15
+ import shutil
16
+ import tempfile
17
+ import time
18
+ import traceback
19
+ from concurrent.futures import (
20
+ FIRST_COMPLETED,
21
+ ThreadPoolExecutor,
22
+ wait,
23
+ TimeoutError as FuturesTimeoutError,
24
+ )
25
+
26
+ import openpyxl
27
+
28
+ from skillopt.envs.spreadsheetbench.react_agent import run_react
29
+ from skillopt.envs.spreadsheetbench.evaluator import (
30
+ evaluate, _generate_cell_names, _compare_cell_value,
31
+ )
32
+ from skillopt.envs.spreadsheetbench.executor import run_generated_code
33
+
34
+
35
+ # ── Data loading ─────────────────────────────────────────────────────────────
36
+
37
+
38
+ def load_items(path: str) -> list[dict]:
39
+ """Load a benchmark file. Supports both .jsonl and .json (list of dicts)."""
40
+ if path.endswith(".json"):
41
+ with open(path) as f:
42
+ data = json.load(f)
43
+ if isinstance(data, dict):
44
+ data = data.get("data") or list(data.values())
45
+ return list(data)
46
+ items = []
47
+ with open(path) as f:
48
+ for line in f:
49
+ line = line.strip()
50
+ if line:
51
+ items.append(json.loads(line))
52
+ return items
53
+
54
+
55
+ # ── Test case discovery ──────────────────────────────────────────────────────
56
+
57
+
58
+ def _find_test_cases(task_dir: str) -> list[tuple[str, str, str]]:
59
+ """Return [(case_no, input_path, answer_path), ...] sorted by case_no.
60
+
61
+ Supports naming conventions used by SpreadsheetBench releases:
62
+ * ``{no}_{id}_input.xlsx`` + ``{no}_{id}_answer.xlsx`` (original)
63
+ * ``{no}_{id}_init.xlsx`` + ``{no}_{id}_golden.xlsx`` (verified_400)
64
+ * ``initial.xlsx`` + ``golden.xlsx`` (verified_400, no prefix)
65
+ """
66
+ cases: list[tuple[str, str, str]] = []
67
+ inputs = sorted(_glob.glob(os.path.join(task_dir, "*_input.xlsx")))
68
+ for ip in inputs:
69
+ no = os.path.basename(ip).split("_", 1)[0]
70
+ ap = ip.replace("_input.xlsx", "_answer.xlsx")
71
+ if os.path.exists(ap):
72
+ cases.append((no, ip, ap))
73
+ inits = sorted(_glob.glob(os.path.join(task_dir, "*_init.xlsx")))
74
+ for ip in inits:
75
+ no = os.path.basename(ip).split("_", 1)[0]
76
+ ap = ip.replace("_init.xlsx", "_golden.xlsx")
77
+ if os.path.exists(ap):
78
+ cases.append((no, ip, ap))
79
+
80
+ # Fallback: bare initial.xlsx + golden.xlsx (no numbered prefix)
81
+ if not cases:
82
+ bare_init = os.path.join(task_dir, "initial.xlsx")
83
+ bare_gold = os.path.join(task_dir, "golden.xlsx")
84
+ if os.path.exists(bare_init) and os.path.exists(bare_gold):
85
+ cases.append(("1", bare_init, bare_gold))
86
+
87
+ return cases
88
+
89
+
90
+ # ── Auto-verify helper ──────────────────────────────────────────────────────
91
+
92
+ # The official SpreadsheetBench evaluator never serialises cells to text — it
93
+ # compares in memory and returns only a pass/fail bool. The per-cell report
94
+ # below is a repo-local training aid (fed back to the model on retry and saved
95
+ # into the trajectory for reflection). On most tasks the answer range is a
96
+ # handful of cells, so the full report is tiny. But a few tasks have answer
97
+ # ranges spanning tens of thousands of cells (e.g. 80-42 =
98
+ # 'Consolidate_ALL'!A2:L8000 ≈ 96k cells); dumping every cell explodes the
99
+ # report to several MB, floods the model's context and bloats conversation
100
+ # files. We therefore apply the same head+tail character truncation the rest of
101
+ # the codebase uses for oversized trajectory text (cf. reflect.py / slow_update.py
102
+ # `text[:half] + "...[truncated]...\n" + text[-half:]`): keep the first and last
103
+ # `_MAX_REPORT_CHARS // 2` chars so both the leading and trailing wrong cells
104
+ # stay visible. Small reports are unchanged.
105
+ _MAX_REPORT_CHARS = 12000 # head+tail char budget (~6000 head + 6000 tail)
106
+
107
+
108
+ def _auto_verify_output(
109
+ pred_path: str,
110
+ gold_path: str,
111
+ answer_position: str,
112
+ ) -> str:
113
+ """Reopen the predicted xlsx and compare cells at answer_position with gold.
114
+
115
+ Returns a human-readable verification report that can be appended to the
116
+ trajectory so the error analyst can see exactly what went wrong (e.g.
117
+ ``cell A1: got=None, expected=420``). Oversized reports are head+tail
118
+ truncated to `_MAX_REPORT_CHARS` chars, matching the rest of the codebase.
119
+ """
120
+ if not os.path.exists(pred_path):
121
+ return "Verification: output file does not exist."
122
+ try:
123
+ wb_pred = openpyxl.load_workbook(pred_path, data_only=True)
124
+ wb_gold = openpyxl.load_workbook(gold_path, data_only=True)
125
+ except Exception as e:
126
+ return f"Verification: could not open workbooks: {e}"
127
+
128
+ lines = ["## Output Verification"]
129
+ try:
130
+ for scr in (answer_position or "").split(","):
131
+ scr = scr.strip()
132
+ if not scr:
133
+ continue
134
+ if "!" in scr:
135
+ sheet_name, cell_range = scr.split("!", 1)
136
+ sheet_name = sheet_name.strip().strip("'\"")
137
+ else:
138
+ sheet_name = wb_gold.sheetnames[0]
139
+ cell_range = scr
140
+ cell_range = cell_range.strip().strip("'\"")
141
+
142
+ cell_names = _generate_cell_names(cell_range)
143
+ ws_pred = wb_pred[sheet_name] if sheet_name in wb_pred.sheetnames else None
144
+ ws_gold = wb_gold[sheet_name] if sheet_name in wb_gold.sheetnames else None
145
+
146
+ if ws_pred is None:
147
+ lines.append(f" Sheet '{sheet_name}' NOT FOUND in output.")
148
+ continue
149
+
150
+ n_empty_correct = 0 # empty-on-both correct cells collapsed to a count
151
+ for cn in cell_names:
152
+ gv = ws_gold[cn].value if ws_gold else "N/A"
153
+ pv = ws_pred[cn].value
154
+ # Use the official cell comparator so this report's ✓/✗ agrees
155
+ # with the real scorer (evaluate). repr() equality would wrongly
156
+ # flag e.g. 5 vs 5.0 or None vs "" as mismatches and mislead the
157
+ # model into "fixing" cells that already pass scoring.
158
+ ok_cell = ws_gold is not None and _compare_cell_value(gv, pv)
159
+ # Collapse only cells that are correct AND empty on both sides
160
+ # (got=None, expected=None ✓): pure noise. Every other cell —
161
+ # including non-empty correct cells — is listed in full; the
162
+ # final head+tail char cap keeps the report bounded.
163
+ if ok_cell and gv in (None, "") and pv in (None, ""):
164
+ n_empty_correct += 1
165
+ continue
166
+ match = "✓" if ok_cell else "✗"
167
+ lines.append(f" {sheet_name}!{cn}: got={pv!r}, expected={gv!r} {match}")
168
+ if n_empty_correct:
169
+ lines.append(
170
+ f" (+{n_empty_correct} empty cells correct, omitted)"
171
+ )
172
+
173
+ # Also check if any cells in the output contain formula strings
174
+ formula_cells = []
175
+ for sn in wb_pred.sheetnames:
176
+ ws = wb_pred[sn]
177
+ for row in ws.iter_rows(max_row=min(ws.max_row, 200), values_only=False):
178
+ for cell in row:
179
+ if isinstance(cell.value, str) and cell.value.startswith("="):
180
+ formula_cells.append(f"{sn}!{cell.coordinate}={cell.value}")
181
+ if len(formula_cells) >= 10:
182
+ break
183
+ if len(formula_cells) >= 10:
184
+ break
185
+ if len(formula_cells) >= 10:
186
+ break
187
+ if formula_cells:
188
+ lines.append(f"\n WARNING: {len(formula_cells)} cells contain Excel formulas (openpyxl cannot evaluate them):")
189
+ for fc in formula_cells[:5]:
190
+ lines.append(f" {fc}")
191
+ if len(formula_cells) > 5:
192
+ lines.append(f" ... and {len(formula_cells) - 5} more")
193
+ finally:
194
+ wb_pred.close()
195
+ wb_gold.close()
196
+
197
+ report = "\n".join(lines)
198
+ # Head+tail truncation, matching reflect.py / slow_update.py: keep the first
199
+ # and last half so both leading and trailing wrong cells remain visible.
200
+ if len(report) > _MAX_REPORT_CHARS:
201
+ half = _MAX_REPORT_CHARS // 2
202
+ report = (
203
+ report[:half]
204
+ + f"\n ...[verification report truncated, {len(report)} chars total]...\n"
205
+ + report[-half:]
206
+ )
207
+ return report
208
+
209
+
210
+ # ── Per-task worker ──────────────────────────────────────────────────────────
211
+
212
+
213
+ def process_one(
214
+ item: dict,
215
+ data_root: str,
216
+ out_root: str,
217
+ skill_content: str,
218
+ max_turns: int,
219
+ diagnostic_mode: bool = False,
220
+ diagnostic_instruction: str = "",
221
+ diagnostic_trace_context: str = "",
222
+ max_completion_tokens: int = 16384,
223
+ ) -> dict:
224
+ """Run the ReAct agent on a single SpreadsheetBench task.
225
+
226
+ Returns a result dict compatible with ``compute_score()``.
227
+ """
228
+ task_id = str(item["id"])
229
+ instruction = item["instruction"]
230
+ instruction_type = item.get("instruction_type", "")
231
+ answer_position = item.get("answer_position", "")
232
+ answer_sheet = item.get("answer_sheet", "")
233
+ if answer_position and answer_sheet and "!" not in answer_position:
234
+ answer_position_eval = f"{answer_sheet}!{answer_position}"
235
+ else:
236
+ answer_position_eval = answer_position
237
+
238
+ # Determine task_type from instruction_type
239
+ itype_lower = (instruction_type or "").lower()
240
+ if "cell" in itype_lower:
241
+ task_type = "cell_level"
242
+ elif "sheet" in itype_lower:
243
+ task_type = "sheet_level"
244
+ else:
245
+ task_type = "other"
246
+
247
+ sp = item.get("spreadsheet_path", f"spreadsheet/{task_id}")
248
+ task_dir = sp if os.path.isabs(sp) else os.path.join(data_root, sp)
249
+
250
+ result = {
251
+ "id": task_id,
252
+ "ok": False,
253
+ "instruction_type": instruction_type,
254
+ "task_type": task_type,
255
+ "task_description": instruction,
256
+ "phase": "setup",
257
+ "fail_reason": "",
258
+ "agent_ok": False,
259
+ "exec_ok": False,
260
+ "n_cases": 0,
261
+ "n_exec_pass": 0,
262
+ "n_pass": 0,
263
+ "soft": 0.0,
264
+ "hard": 0,
265
+ "n_turns": 0,
266
+ "cases": [],
267
+ "error": "",
268
+ }
269
+
270
+ try:
271
+ cases = _find_test_cases(task_dir)
272
+ result["n_cases"] = len(cases)
273
+ if not cases:
274
+ result["fail_reason"] = "no-test-cases"
275
+ return result
276
+
277
+ task_out_dir = os.path.join(out_root, "predictions", task_id)
278
+ os.makedirs(task_out_dir, exist_ok=True)
279
+
280
+ no1, ip1, _ = cases[0]
281
+ pred_path_1 = os.path.join(task_out_dir, f"{no1}_pred.xlsx")
282
+ target_prompt_parts = [
283
+ f"# Instruction\n{instruction}",
284
+ f"# Input file\n{ip1}",
285
+ f"# Output file\n{pred_path_1}",
286
+ ]
287
+ if instruction_type:
288
+ target_prompt_parts.append(f"# Instruction type\n{instruction_type}")
289
+ if answer_position_eval:
290
+ target_prompt_parts.append(f"# Answer position\n{answer_position_eval}")
291
+ if diagnostic_trace_context.strip():
292
+ target_prompt_parts.insert(
293
+ 0,
294
+ "# Previous Codex Trace Snapshot\n"
295
+ "This is a partial transcript from an earlier attempt. Use it as your current reasoning context.\n\n"
296
+ f"{diagnostic_trace_context.strip()}",
297
+ )
298
+ if diagnostic_mode and diagnostic_instruction.strip():
299
+ target_prompt_parts.append(f"# Training readout\n{diagnostic_instruction.strip()}")
300
+ target_user_prompt = "\n\n".join(target_prompt_parts)
301
+ try:
302
+ from skillopt.envs.spreadsheetbench.react_agent import _build_system
303
+ target_system_prompt = _build_system(skill_content)
304
+ except Exception:
305
+ target_system_prompt = ""
306
+ if target_system_prompt:
307
+ with open(os.path.join(task_out_dir, "target_system_prompt.txt"), "w") as f:
308
+ f.write(target_system_prompt)
309
+ result["target_system_prompt"] = target_system_prompt
310
+ with open(os.path.join(task_out_dir, "target_user_prompt.txt"), "w") as f:
311
+ f.write(target_user_prompt)
312
+ result["target_user_prompt"] = target_user_prompt
313
+
314
+ # ── Stage 1: run ReAct agent on test case 1 ─────────────────────
315
+ result["phase"] = "agent"
316
+
317
+ work_dir = tempfile.mkdtemp(prefix=f"react_{task_id}_")
318
+ try:
319
+ # Copy input so agent works in an isolated directory
320
+ work_input = os.path.join(work_dir, os.path.basename(ip1))
321
+ shutil.copy2(ip1, work_input)
322
+
323
+ agent_result = run_react(
324
+ instruction=instruction,
325
+ input_path=work_input,
326
+ output_path=pred_path_1,
327
+ work_dir=work_dir,
328
+ instruction_type=instruction_type,
329
+ answer_position=answer_position_eval,
330
+ skill_content=skill_content,
331
+ max_turns=max_turns,
332
+ max_output_tokens=max_completion_tokens,
333
+ diagnostic_mode=diagnostic_mode,
334
+ diagnostic_instruction=diagnostic_instruction,
335
+ diagnostic_trace_context=diagnostic_trace_context,
336
+ )
337
+ result["n_turns"] = agent_result.get("n_turns", 0)
338
+ if agent_result.get("target_system_prompt"):
339
+ with open(os.path.join(task_out_dir, "target_system_prompt.txt"), "w") as f:
340
+ f.write(agent_result["target_system_prompt"])
341
+ result["target_system_prompt"] = agent_result["target_system_prompt"]
342
+ if agent_result.get("target_user_prompt"):
343
+ with open(os.path.join(task_out_dir, "target_user_prompt.txt"), "w") as f:
344
+ f.write(agent_result["target_user_prompt"])
345
+ result["target_user_prompt"] = agent_result["target_user_prompt"]
346
+
347
+ # Save conversation log
348
+ with open(os.path.join(task_out_dir, "conversation.json"), "w") as f:
349
+ json.dump(
350
+ agent_result.get("conversation", []),
351
+ f, ensure_ascii=False, indent=2,
352
+ )
353
+
354
+ # Copy solution.py if the agent wrote one
355
+ solution_src = os.path.join(work_dir, "solution.py")
356
+ solution_dst = os.path.join(task_out_dir, "solution.py")
357
+ if os.path.exists(solution_src):
358
+ shutil.copy2(solution_src, solution_dst)
359
+
360
+ except Exception as e:
361
+ result["fail_reason"] = f"agent-error: {type(e).__name__}: {e}"
362
+ result["error"] = traceback.format_exc()
363
+ return result
364
+ finally:
365
+ shutil.rmtree(work_dir, ignore_errors=True)
366
+
367
+ result["agent_ok"] = True
368
+
369
+ # ── Stage 2: evaluate all test cases ─────────────────────────────
370
+ result["phase"] = "eval"
371
+ solution_path = os.path.join(task_out_dir, "solution.py")
372
+ all_exec = True
373
+
374
+ for i, (no, ip, ap) in enumerate(cases):
375
+ pred_path = os.path.join(task_out_dir, f"{no}_pred.xlsx")
376
+
377
+ if i > 0:
378
+ # Re-apply solution.py to subsequent test cases
379
+ if not os.path.exists(solution_path):
380
+ all_exec = False
381
+ result["cases"].append(
382
+ {"no": no, "stage": "exec", "ok": False, "error": "no-solution-py"}
383
+ )
384
+ if not result["fail_reason"]:
385
+ result["fail_reason"] = "no-solution-py-for-other-cases"
386
+ continue
387
+
388
+ with open(solution_path) as f:
389
+ code = f.read()
390
+
391
+ # Prepend new INPUT_PATH / OUTPUT_PATH
392
+ preamble = (
393
+ f"INPUT_PATH = {ip!r}\n"
394
+ f"OUTPUT_PATH = {pred_path!r}\n"
395
+ )
396
+ full_code = preamble + code
397
+
398
+ ok_exec, err = run_generated_code(full_code, ip, pred_path)
399
+ if not ok_exec:
400
+ all_exec = False
401
+ result["cases"].append(
402
+ {"no": no, "stage": "exec", "ok": False, "error": err[:500]}
403
+ )
404
+ if not result["fail_reason"]:
405
+ tail = err.strip().splitlines()[-1][:200] if err.strip() else "unknown"
406
+ result["fail_reason"] = f"exec-error: {tail}"
407
+ continue
408
+
409
+ # ── Evaluate ─────────────────────────────────────────────────
410
+ if not os.path.exists(pred_path):
411
+ all_exec = False
412
+ result["cases"].append(
413
+ {"no": no, "stage": "exec", "ok": False, "error": "output-not-found"}
414
+ )
415
+ if not result["fail_reason"]:
416
+ result["fail_reason"] = "output-not-found"
417
+ continue
418
+
419
+ result["n_exec_pass"] += 1
420
+ try:
421
+ ev = evaluate(pred_path, ap, instruction_type, answer_position_eval)
422
+ except Exception as e: # noqa: BLE001
423
+ ev = {"ok": False, "reason": f"eval-exception: {type(e).__name__}: {e}"}
424
+
425
+ if ev["ok"]:
426
+ result["n_pass"] += 1
427
+ else:
428
+ if not result["fail_reason"]:
429
+ result["fail_reason"] = f"eval-mismatch: {ev['reason'][:200]}"
430
+ result["cases"].append(
431
+ {"no": no, "stage": "eval", "ok": ev["ok"], "reason": ev.get("reason", "")}
432
+ )
433
+
434
+ result["exec_ok"] = all_exec
435
+ n_cases = result["n_cases"]
436
+ n_pass = result["n_pass"]
437
+ result["soft"] = (n_pass / n_cases) if n_cases else 0.0
438
+ result["hard"] = 1 if (n_cases > 0 and n_pass == n_cases) else 0
439
+ result["ok"] = bool(result["hard"])
440
+ if result["ok"]:
441
+ result["fail_reason"] = ""
442
+ return result
443
+
444
+ except Exception as e: # noqa: BLE001
445
+ result["fail_reason"] = f"unexpected: {type(e).__name__}: {e}"
446
+ result["error"] = traceback.format_exc()
447
+ return result
448
+
449
+
450
+ # ── Batch runner ─────────────────────────────────────────────────────────────
451
+
452
+
453
+ def run_spreadsheet_batch(
454
+ items: list[dict],
455
+ data_root: str,
456
+ out_root: str,
457
+ skill_content: str,
458
+ max_turns: int = 30,
459
+ max_completion_tokens: int = 16384,
460
+ max_api_workers: int = 64,
461
+ task_timeout: int = 600,
462
+ diagnostic_mode: bool = False,
463
+ diagnostic_instruction: str = "",
464
+ diagnostic_trace_context_by_id: dict[str, str] | None = None,
465
+ ) -> list[dict]:
466
+ """Run the ReAct agent on all items with ThreadPoolExecutor.
467
+
468
+ Returns list of result dicts compatible with ``compute_score()``.
469
+ """
470
+ os.makedirs(out_root, exist_ok=True)
471
+
472
+ # Check for already-done items (resume support)
473
+ results_path = os.path.join(out_root, "results.jsonl")
474
+ done_ids: set[str] = set()
475
+ existing: list[dict] = []
476
+ if os.path.exists(results_path):
477
+ with open(results_path) as f:
478
+ for line in f:
479
+ try:
480
+ r = json.loads(line)
481
+ done_ids.add(str(r["id"]))
482
+ existing.append(r)
483
+ except Exception:
484
+ pass
485
+
486
+ pending = [it for it in items if str(it["id"]) not in done_ids]
487
+ print(
488
+ f" [spreadsheet rollout] total={len(items)} done={len(done_ids)} "
489
+ f"pending={len(pending)} workers={max_api_workers} task_timeout={task_timeout}s"
490
+ )
491
+
492
+ if not pending:
493
+ return existing
494
+
495
+ t0 = time.time()
496
+ results = list(existing)
497
+ started_at: dict[str, float] = {}
498
+
499
+ def _timeout_result(item: dict) -> dict:
500
+ return {
501
+ "id": str(item["id"]),
502
+ "ok": False,
503
+ "phase": "timeout",
504
+ "fail_reason": f"task-timeout-{task_timeout}s",
505
+ "n_cases": 0, "n_pass": 0, "soft": 0.0, "hard": 0,
506
+ "n_turns": 0, "cases": [], "error": "timeout",
507
+ }
508
+
509
+ def _error_result(item: dict, exc: Exception) -> dict:
510
+ return {
511
+ "id": str(item["id"]),
512
+ "ok": False,
513
+ "phase": "error",
514
+ "fail_reason": f"unexpected: {type(exc).__name__}: {exc}",
515
+ "n_cases": 0, "n_pass": 0, "soft": 0.0, "hard": 0,
516
+ "n_turns": 0, "cases": [], "error": str(exc),
517
+ }
518
+
519
+ def _run_one(it: dict) -> dict:
520
+ started_at[str(it["id"])] = time.time()
521
+ return process_one(
522
+ it,
523
+ data_root,
524
+ out_root,
525
+ skill_content,
526
+ max_turns,
527
+ diagnostic_mode,
528
+ diagnostic_instruction,
529
+ (diagnostic_trace_context_by_id or {}).get(str(it["id"]), ""),
530
+ max_completion_tokens,
531
+ )
532
+
533
+ ex = ThreadPoolExecutor(max_workers=max_api_workers)
534
+ try:
535
+ futs = {ex.submit(_run_one, it): it for it in pending}
536
+ pending_futs = set(futs)
537
+ finished = 0
538
+ while pending_futs:
539
+ done, _ = wait(pending_futs, timeout=5, return_when=FIRST_COMPLETED)
540
+ now = time.time()
541
+ timed_out = [
542
+ fut for fut in pending_futs - done
543
+ if str(futs[fut]["id"]) in started_at
544
+ and now - started_at[str(futs[fut]["id"])] >= task_timeout
545
+ ]
546
+ for fut in done:
547
+ pending_futs.remove(fut)
548
+ item = futs[fut]
549
+ try:
550
+ res = fut.result()
551
+ except FuturesTimeoutError:
552
+ res = _timeout_result(item)
553
+ except Exception as e: # noqa: BLE001
554
+ res = _error_result(item, e)
555
+ results.append(res)
556
+ finished += 1
557
+ status = "PASS" if res.get("hard") else ("TIMEOUT" if res.get("phase") == "timeout" else "FAIL")
558
+ dt = time.time() - t0
559
+ print(
560
+ f" {finished}/{len(pending)} id={res['id']:<10} {status} "
561
+ f"turns={res.get('n_turns', 0):<3} "
562
+ f"cases={res.get('n_pass', 0)}/{res.get('n_cases', 0)} "
563
+ f"dt={dt:.0f}s"
564
+ )
565
+ for fut in timed_out:
566
+ pending_futs.remove(fut)
567
+ res = _timeout_result(futs[fut])
568
+ results.append(res)
569
+ finished += 1
570
+ status = "TIMEOUT"
571
+ dt = time.time() - t0
572
+ print(
573
+ f" {finished}/{len(pending)} id={res['id']:<10} {status} "
574
+ f"turns={res.get('n_turns', 0):<3} "
575
+ f"cases={res.get('n_pass', 0)}/{res.get('n_cases', 0)} "
576
+ f"dt={dt:.0f}s"
577
+ )
578
+ finally:
579
+ ex.shutdown(wait=False, cancel_futures=True)
580
+
581
+ return results
582
+
583
+
584
+ # ── Codegen per-task worker (no tool-call) ──────────────────────────────────
585
+
586
+
587
+ def process_one_codegen(
588
+ item: dict,
589
+ data_root: str,
590
+ out_root: str,
591
+ skill_content: str,
592
+ mode: str = "single",
593
+ max_turns: int = 5,
594
+ max_completion_tokens: int = 16384,
595
+ task_timeout: int = 600,
596
+ use_eval_feedback: bool = False,
597
+ diagnostic_mode: bool = False,
598
+ diagnostic_instruction: str = "",
599
+ diagnostic_trace_context: str = "",
600
+ ) -> dict:
601
+ """Run codegen agent (single or multi-round) on one SpreadsheetBench task.
602
+
603
+ This matches the official evaluation setting: LLM generates a Python code
604
+ block, no function-calling / tool-use.
605
+ """
606
+ from skillopt.envs.spreadsheetbench.codegen_agent import run_single, run_multi
607
+
608
+ task_id = str(item["id"])
609
+ instruction = item["instruction"]
610
+ instruction_type = item.get("instruction_type", "")
611
+ answer_position = item.get("answer_position", "")
612
+ answer_sheet = item.get("answer_sheet", "")
613
+ if answer_position and answer_sheet and "!" not in answer_position:
614
+ answer_position_eval = f"{answer_sheet}!{answer_position}"
615
+ else:
616
+ answer_position_eval = answer_position
617
+
618
+ itype_lower = (instruction_type or "").lower()
619
+ if "cell" in itype_lower:
620
+ task_type = "cell_level"
621
+ elif "sheet" in itype_lower:
622
+ task_type = "sheet_level"
623
+ else:
624
+ task_type = "other"
625
+
626
+ sp = item.get("spreadsheet_path", f"spreadsheet/{task_id}")
627
+ task_dir = sp if os.path.isabs(sp) else os.path.join(data_root, sp)
628
+
629
+ result = {
630
+ "id": task_id,
631
+ "ok": False,
632
+ "instruction_type": instruction_type,
633
+ "task_type": task_type,
634
+ "task_description": instruction,
635
+ "phase": "setup",
636
+ "fail_reason": "",
637
+ "llm_ok": False,
638
+ "code_ok": False,
639
+ "exec_ok": False,
640
+ "n_cases": 0,
641
+ "n_exec_pass": 0,
642
+ "n_pass": 0,
643
+ "soft": 0.0,
644
+ "hard": 0,
645
+ "n_turns": 0,
646
+ "cases": [],
647
+ "error": "",
648
+ }
649
+
650
+ try:
651
+ cases = _find_test_cases(task_dir)
652
+ result["n_cases"] = len(cases)
653
+ if not cases:
654
+ result["fail_reason"] = "no-test-cases"
655
+ return result
656
+
657
+ task_out_dir = os.path.join(out_root, "predictions", task_id)
658
+ os.makedirs(task_out_dir, exist_ok=True)
659
+
660
+ # ── Save context for Optimizer (Reflect stage) ──────────────────
661
+ from skillopt.envs.spreadsheetbench.codegen_agent import (
662
+ _preview_workbook, _build_system, _build_user,
663
+ )
664
+ first_input_for_preview = cases[0][1]
665
+ try:
666
+ preview_text = _preview_workbook(first_input_for_preview)
667
+ except Exception:
668
+ preview_text = "(preview failed)"
669
+ target_system = _build_system(skill_content)
670
+ target_user = _build_user(
671
+ instruction,
672
+ first_input_for_preview,
673
+ instruction_type,
674
+ answer_position_eval,
675
+ diagnostic_mode=diagnostic_mode,
676
+ diagnostic_instruction=diagnostic_instruction,
677
+ diagnostic_trace_context=diagnostic_trace_context,
678
+ )
679
+
680
+ with open(os.path.join(task_out_dir, "spreadsheet_preview.txt"), "w") as f:
681
+ f.write(preview_text)
682
+ with open(os.path.join(task_out_dir, "target_system_prompt.txt"), "w") as f:
683
+ f.write(target_system)
684
+ with open(os.path.join(task_out_dir, "target_user_prompt.txt"), "w") as f:
685
+ f.write(target_user)
686
+
687
+ result["spreadsheet_preview"] = preview_text
688
+ result["target_system_prompt"] = target_system
689
+ result["target_user_prompt"] = target_user
690
+
691
+ # ── LLM phase ──────────────────────────────────────────────────
692
+ result["phase"] = "llm"
693
+ first_input = cases[0][1]
694
+ first_gold = cases[0][2]
695
+ first_pred = os.path.join(task_out_dir, f"{cases[0][0]}_pred.xlsx")
696
+
697
+ try:
698
+ if mode == "multi":
699
+ agent_result = run_multi(
700
+ instruction=instruction,
701
+ input_xlsx=first_input,
702
+ output_path=first_pred,
703
+ instruction_type=instruction_type,
704
+ answer_position=answer_position_eval,
705
+ skill_content=skill_content,
706
+ max_turns=max_turns,
707
+ max_output_tokens=max_completion_tokens,
708
+ task_timeout=task_timeout,
709
+ gold_path=first_gold if use_eval_feedback else "",
710
+ diagnostic_mode=diagnostic_mode,
711
+ diagnostic_instruction=diagnostic_instruction,
712
+ diagnostic_trace_context=diagnostic_trace_context,
713
+ )
714
+ else:
715
+ agent_result = run_single(
716
+ instruction=instruction,
717
+ input_xlsx=first_input,
718
+ output_path=first_pred,
719
+ instruction_type=instruction_type,
720
+ answer_position=answer_position_eval,
721
+ skill_content=skill_content,
722
+ max_output_tokens=max_completion_tokens,
723
+ task_timeout=task_timeout,
724
+ diagnostic_mode=diagnostic_mode,
725
+ diagnostic_instruction=diagnostic_instruction,
726
+ diagnostic_trace_context=diagnostic_trace_context,
727
+ )
728
+ except Exception as e: # noqa: BLE001
729
+ result["fail_reason"] = f"llm-call-failed: {type(e).__name__}: {e}"
730
+ result["error"] = traceback.format_exc()
731
+ return result
732
+
733
+ result["llm_ok"] = True
734
+ result["n_turns"] = agent_result.get("n_turns", 1)
735
+ code = agent_result.get("code", "")
736
+ raw = agent_result.get("raw", "")
737
+
738
+ # Save artifacts
739
+ with open(os.path.join(task_out_dir, "code.py"), "w") as f:
740
+ f.write(code)
741
+ with open(os.path.join(task_out_dir, "raw.txt"), "w") as f:
742
+ f.write(raw)
743
+ if agent_result.get("conversation"):
744
+ with open(os.path.join(task_out_dir, "conversation.json"), "w") as f:
745
+ json.dump(agent_result["conversation"], f, ensure_ascii=False, indent=2)
746
+
747
+ if not code.strip():
748
+ result["phase"] = "extract"
749
+ result["fail_reason"] = "empty-code-block"
750
+ return result
751
+ result["code_ok"] = True
752
+
753
+ # ── Exec + eval per test case ──────────────────────────────────
754
+ result["phase"] = "exec"
755
+ all_exec = True
756
+ # Collect enrichment info for the conversation/trajectory
757
+ enrichment_parts: list[str] = []
758
+
759
+ for no, ip, ap in cases:
760
+ pred_path = os.path.join(task_out_dir, f"{no}_pred.xlsx")
761
+
762
+ # For multi mode, the first case may already be produced
763
+ if not os.path.exists(pred_path):
764
+ ok_exec, err = run_generated_code(code, ip, pred_path)
765
+ if not ok_exec:
766
+ all_exec = False
767
+ result["cases"].append(
768
+ {"no": no, "stage": "exec", "ok": False, "error": err[:500]}
769
+ )
770
+ if not result["fail_reason"]:
771
+ tail = err.strip().splitlines()[-1][:200] if err.strip() else "unknown"
772
+ result["fail_reason"] = f"exec-error: {tail}"
773
+ enrichment_parts.append(
774
+ f"## Execution (case {no})\nERROR: {err[:500]}"
775
+ )
776
+ continue
777
+
778
+ if not os.path.exists(pred_path):
779
+ all_exec = False
780
+ result["cases"].append(
781
+ {"no": no, "stage": "exec", "ok": False, "error": "output-not-found"}
782
+ )
783
+ if not result["fail_reason"]:
784
+ result["fail_reason"] = "output-not-found"
785
+ continue
786
+
787
+ result["n_exec_pass"] += 1
788
+ try:
789
+ ev = evaluate(pred_path, ap, instruction_type, answer_position_eval)
790
+ except Exception as e: # noqa: BLE001
791
+ ev = {"ok": False, "reason": f"eval-exception: {type(e).__name__}: {e}"}
792
+
793
+ if ev["ok"]:
794
+ result["n_pass"] += 1
795
+ else:
796
+ if not result["fail_reason"]:
797
+ result["fail_reason"] = f"eval-mismatch: {ev['reason'][:200]}"
798
+ result["cases"].append(
799
+ {"no": no, "stage": "eval", "ok": ev["ok"], "reason": ev.get("reason", "")}
800
+ )
801
+
802
+ # Auto-verify: reopen output and compare cells at answer_position
803
+ if answer_position_eval:
804
+ verify_report = _auto_verify_output(pred_path, ap, answer_position_eval)
805
+ enrichment_parts.append(
806
+ f"## Eval Result (case {no}): {'PASS' if ev['ok'] else 'FAIL'}\n"
807
+ f"{ev.get('reason', '')}\n\n{verify_report}"
808
+ )
809
+
810
+ result["exec_ok"] = all_exec
811
+
812
+ # ── Enrich conversation with eval details ──────────────────────
813
+ if enrichment_parts:
814
+ enrichment_msg = "\n\n---\n\n".join(enrichment_parts)
815
+ conversation = agent_result.get("conversation", [])
816
+ conversation.append({
817
+ "role": "system",
818
+ "content": f"[POST-EXECUTION VERIFICATION]\n\n{enrichment_msg}",
819
+ })
820
+ # Re-save the enriched conversation
821
+ with open(os.path.join(task_out_dir, "conversation.json"), "w") as f:
822
+ json.dump(conversation, f, ensure_ascii=False, indent=2)
823
+ n_cases = result["n_cases"]
824
+ n_pass = result["n_pass"]
825
+ result["soft"] = (n_pass / n_cases) if n_cases else 0.0
826
+ result["hard"] = 1 if (n_cases > 0 and n_pass == n_cases) else 0
827
+ result["ok"] = bool(result["hard"])
828
+ if result["ok"]:
829
+ result["fail_reason"] = ""
830
+ return result
831
+
832
+ except Exception as e: # noqa: BLE001
833
+ result["fail_reason"] = f"unexpected: {type(e).__name__}: {e}"
834
+ result["error"] = traceback.format_exc()
835
+ return result
836
+
837
+
838
+ # ── Codegen batch runner ────────────────────────────────────────────────────
839
+
840
+
841
+ def run_spreadsheet_batch_codegen(
842
+ items: list[dict],
843
+ data_root: str,
844
+ out_root: str,
845
+ skill_content: str,
846
+ mode: str = "single",
847
+ max_turns: int = 5,
848
+ max_completion_tokens: int = 16384,
849
+ max_api_workers: int = 32,
850
+ task_timeout: int = 0,
851
+ use_eval_feedback: bool = False,
852
+ diagnostic_mode: bool = False,
853
+ diagnostic_instruction: str = "",
854
+ diagnostic_trace_context_by_id: dict[str, str] | None = None,
855
+ ) -> list[dict]:
856
+ """Run codegen agent on all items (no tool-call).
857
+
858
+ Args:
859
+ mode: "single" or "multi".
860
+ task_timeout: Hard per-task timeout in seconds at the future level.
861
+ 0 or negative disables the per-task timeout.
862
+ """
863
+ no_task_timeout = task_timeout <= 0
864
+ task_timeout_label = "none" if no_task_timeout else f"{task_timeout}s"
865
+
866
+ os.makedirs(out_root, exist_ok=True)
867
+
868
+ results_path = os.path.join(out_root, "results.jsonl")
869
+ done_ids: set[str] = set()
870
+ existing: list[dict] = []
871
+ if os.path.exists(results_path):
872
+ with open(results_path) as f:
873
+ for line in f:
874
+ try:
875
+ r = json.loads(line)
876
+ done_ids.add(str(r["id"]))
877
+ existing.append(r)
878
+ except Exception:
879
+ pass
880
+
881
+ pending = [it for it in items if str(it["id"]) not in done_ids]
882
+ print(
883
+ f" [spreadsheet codegen-{mode}] total={len(items)} done={len(done_ids)} "
884
+ f"pending={len(pending)} workers={max_api_workers} task_timeout={task_timeout_label}"
885
+ )
886
+
887
+ if not pending:
888
+ return existing
889
+
890
+ t0 = time.time()
891
+ results = list(existing)
892
+
893
+ started_at: dict[str, float] = {}
894
+
895
+ def _run_one(it: dict) -> dict:
896
+ started_at[str(it["id"])] = time.time()
897
+ return process_one_codegen(
898
+ it,
899
+ data_root,
900
+ out_root,
901
+ skill_content,
902
+ mode,
903
+ max_turns,
904
+ max_completion_tokens,
905
+ task_timeout,
906
+ use_eval_feedback,
907
+ diagnostic_mode,
908
+ diagnostic_instruction,
909
+ (diagnostic_trace_context_by_id or {}).get(str(it["id"]), ""),
910
+ )
911
+
912
+ def _timeout_result(item: dict) -> dict:
913
+ return {
914
+ "id": str(item["id"]),
915
+ "ok": False,
916
+ "instruction_type": item.get("instruction_type", ""),
917
+ "task_type": "other",
918
+ "phase": "timeout",
919
+ "fail_reason": f"task-timeout-{task_timeout}s",
920
+ "n_cases": 0, "n_pass": 0, "soft": 0.0, "hard": 0,
921
+ "n_turns": 0, "cases": [], "error": "timeout",
922
+ }
923
+
924
+ def _error_result(item: dict, e: Exception) -> dict:
925
+ return {
926
+ "id": str(item["id"]),
927
+ "ok": False,
928
+ "instruction_type": item.get("instruction_type", ""),
929
+ "task_type": "other",
930
+ "phase": "error",
931
+ "fail_reason": f"unexpected: {type(e).__name__}: {e}",
932
+ "n_cases": 0, "n_pass": 0, "soft": 0.0, "hard": 0,
933
+ "n_turns": 0, "cases": [], "error": str(e),
934
+ }
935
+
936
+ def _record(res: dict, i: int) -> None:
937
+ results.append(res)
938
+ status = "PASS" if res.get("hard") else ("TIMEOUT" if res.get("phase") == "timeout" else "FAIL")
939
+ dt = time.time() - t0
940
+ print(
941
+ f" {i}/{len(pending)} id={res['id']:<10} {status} "
942
+ f"turns={res.get('n_turns', 0):<3} "
943
+ f"cases={res.get('n_pass', 0)}/{res.get('n_cases', 0)} "
944
+ f"dt={dt:.0f}s"
945
+ )
946
+
947
+ ex = ThreadPoolExecutor(max_workers=max_api_workers)
948
+ try:
949
+ futs = {ex.submit(_run_one, it): it for it in pending}
950
+ pending_futs = set(futs)
951
+ finished = 0
952
+ while pending_futs:
953
+ done, _ = wait(pending_futs, timeout=5, return_when=FIRST_COMPLETED)
954
+ now = time.time()
955
+ timed_out = [] if no_task_timeout else [
956
+ fut for fut in pending_futs - done
957
+ if str(futs[fut]["id"]) in started_at
958
+ and now - started_at[str(futs[fut]["id"])] >= task_timeout
959
+ ]
960
+ for fut in done:
961
+ pending_futs.remove(fut)
962
+ item = futs[fut]
963
+ try:
964
+ res = fut.result()
965
+ except FuturesTimeoutError:
966
+ res = _timeout_result(item)
967
+ except Exception as e: # noqa: BLE001
968
+ res = _error_result(item, e)
969
+ finished += 1
970
+ _record(res, finished)
971
+ for fut in timed_out:
972
+ pending_futs.remove(fut)
973
+ fut.cancel()
974
+ finished += 1
975
+ _record(_timeout_result(futs[fut]), finished)
976
+ finally:
977
+ ex.shutdown(wait=False, cancel_futures=True)
978
+
979
+ return results