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.
- package/package.json +1 -1
- package/template/rules/ponytail.mdc +98 -23
- package/template/skills/skillopt-sleep/SKILL.md +42 -0
- package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
- package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
- package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
- package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
- package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
- package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
- package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
- package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
- package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
- package/template/skills/ponytail/SKILL.md +0 -133
- package/template/skills/ponytail/scripts/hooks/claude-codex-hooks.json +0 -44
- package/template/skills/ponytail/scripts/hooks/copilot-hooks.json +0 -21
- package/template/skills/ponytail/scripts/hooks/ponytail-activate.js +0 -91
- package/template/skills/ponytail/scripts/hooks/ponytail-config.js +0 -122
- package/template/skills/ponytail/scripts/hooks/ponytail-instructions.js +0 -94
- package/template/skills/ponytail/scripts/hooks/ponytail-mode-tracker.js +0 -55
- package/template/skills/ponytail/scripts/hooks/ponytail-runtime.js +0 -68
- package/template/skills/ponytail/scripts/hooks/ponytail-statusline.ps1 +0 -21
- package/template/skills/ponytail/scripts/hooks/ponytail-statusline.sh +0 -12
- package/template/skills/ponytail/scripts/hooks/ponytail-subagent.js +0 -22
- package/template/skills/ponytail/scripts/mcp/README.md +0 -46
- package/template/skills/ponytail/scripts/mcp/index.js +0 -48
- package/template/skills/ponytail/scripts/mcp/instructions.js +0 -26
- package/template/skills/ponytail/scripts/mcp/package.json +0 -13
- package/template/skills/ponytail/scripts/mcp/test/instructions.test.js +0 -22
- package/template/skills/ponytail-audit/SKILL.md +0 -41
- package/template/skills/ponytail-debt/SKILL.md +0 -44
- package/template/skills/ponytail-gain/SKILL.md +0 -50
- package/template/skills/ponytail-help/SKILL.md +0 -69
- package/template/skills/ponytail-review/SKILL.md +0 -57
package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py
ADDED
|
@@ -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
|