self-evolve-framework 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/package.json +1 -1
  2. package/template/rules/ponytail.mdc +98 -23
  3. package/template/skills/skillopt-sleep/SKILL.md +42 -0
  4. package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
  5. package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
  6. package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
  7. package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
  8. package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
  9. package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
  10. package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
  11. package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
  12. package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
  13. package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
  14. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
  15. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
  16. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
  17. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
  18. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
  19. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
  20. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
  21. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
  22. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
  23. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
  24. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
  25. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
  26. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
  27. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
  28. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
  29. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
  30. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
  31. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
  32. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
  33. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
  34. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
  35. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
  36. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
  37. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
  38. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
  39. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
  40. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
  41. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
  42. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
  43. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
  44. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
  45. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
  46. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
  47. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
  48. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
  49. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
  50. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
  51. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
  52. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
  53. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
  54. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
  55. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
  56. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
  57. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
  58. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
  59. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
  60. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
  61. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
  62. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
  63. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
  64. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
  65. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
  66. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
  67. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
  68. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
  69. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
  70. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
  71. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
  72. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
  73. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
  74. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
  75. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
  76. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
  77. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
  78. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
  79. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
  80. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
  81. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
  82. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
  83. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
  84. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
  85. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
  86. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
  87. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
  88. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
  89. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
  90. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
  91. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
  92. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
  93. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
  94. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
  95. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
  96. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
  97. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
  98. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
  99. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
  100. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
  101. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
  102. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
  103. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
  104. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
  105. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
  106. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
  107. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
  108. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
  109. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
  110. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
  111. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
  112. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
  113. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
  114. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
  115. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
  116. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
  117. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
  118. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
  119. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
  120. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
  121. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
  122. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
  123. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
  124. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
  125. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
  126. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
  127. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
  128. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
  129. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
  130. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
  131. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
  132. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
  133. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
  134. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
  135. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
  136. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
  137. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
  138. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
  139. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
  140. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
  141. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
  142. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
  143. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
  144. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
  145. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
  146. package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
  147. package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
  148. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
  149. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
  150. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
  151. package/template/skills/ponytail/SKILL.md +0 -133
  152. package/template/skills/ponytail/scripts/hooks/claude-codex-hooks.json +0 -44
  153. package/template/skills/ponytail/scripts/hooks/copilot-hooks.json +0 -21
  154. package/template/skills/ponytail/scripts/hooks/ponytail-activate.js +0 -91
  155. package/template/skills/ponytail/scripts/hooks/ponytail-config.js +0 -122
  156. package/template/skills/ponytail/scripts/hooks/ponytail-instructions.js +0 -94
  157. package/template/skills/ponytail/scripts/hooks/ponytail-mode-tracker.js +0 -55
  158. package/template/skills/ponytail/scripts/hooks/ponytail-runtime.js +0 -68
  159. package/template/skills/ponytail/scripts/hooks/ponytail-statusline.ps1 +0 -21
  160. package/template/skills/ponytail/scripts/hooks/ponytail-statusline.sh +0 -12
  161. package/template/skills/ponytail/scripts/hooks/ponytail-subagent.js +0 -22
  162. package/template/skills/ponytail/scripts/mcp/README.md +0 -46
  163. package/template/skills/ponytail/scripts/mcp/index.js +0 -48
  164. package/template/skills/ponytail/scripts/mcp/instructions.js +0 -26
  165. package/template/skills/ponytail/scripts/mcp/package.json +0 -13
  166. package/template/skills/ponytail/scripts/mcp/test/instructions.test.js +0 -22
  167. package/template/skills/ponytail-audit/SKILL.md +0 -41
  168. package/template/skills/ponytail-debt/SKILL.md +0 -44
  169. package/template/skills/ponytail-gain/SKILL.md +0 -50
  170. package/template/skills/ponytail-help/SKILL.md +0 -69
  171. package/template/skills/ponytail-review/SKILL.md +0 -57
@@ -0,0 +1,15 @@
1
+ # OfficeQA Skill
2
+
3
+ ## Retrieval Discipline
4
+ - Start by narrowing to the most likely candidate file before reading long passages.
5
+ - Prefer targeted search terms that name the exact entity, period, measure, or table concept from the question.
6
+ - After a promising match, read only a small surrounding span and verify it matches the requested year, basis, and unit.
7
+
8
+ ## Evidence Discipline
9
+ - Extract the exact value from the retrieved text before doing any arithmetic.
10
+ - Keep track of each operand's period, unit, and semantic role so nearby proxy values are not mixed in.
11
+ - If the question asks for a transformed or derived quantity, compute only after confirming every operand.
12
+
13
+ ## Final Answer Discipline
14
+ - Return the final answer only after one last consistency check against the retrieved evidence.
15
+ - Copy the final answer from a checked value, not from an unverified intermediate guess.
@@ -0,0 +1,552 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ import html
5
+ import json
6
+ import os
7
+ import re
8
+ import socket
9
+ import time
10
+ from functools import lru_cache
11
+ from html.parser import HTMLParser
12
+ from pathlib import Path
13
+ from urllib.error import HTTPError, URLError
14
+ from urllib.parse import parse_qs, urlparse
15
+ from urllib.request import Request, urlopen
16
+
17
+ _MAX_READ_CHARS = 4000
18
+ _MAX_GREP_MATCHES = 20
19
+ _MAX_GLOB_MATCHES = 50
20
+ _MAX_ORACLE_PAGE_CHARS = 24000
21
+ _MAX_ORACLE_CONTEXT_CHARS = 80000
22
+ DEFAULT_USER_AGENT = (
23
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
24
+ "(KHTML, like Gecko) Chrome/135.0 Safari/537.36"
25
+ )
26
+ DEFAULT_CUSTOM_SEARCH_URL = "http://apisix.westus2.cloudapp.azure.com/search_tool/search"
27
+ DEFAULT_CUSTOM_SEARCH_AUTH_ENV = "OFFICEQA_CUSTOM_SEARCH_AUTH"
28
+ DEFAULT_CUSTOM_SEARCH_PROVIDER = "duckduckgo"
29
+ DEFAULT_CUSTOM_SEARCH_MAX_RESULTS = 4
30
+ DEFAULT_CUSTOM_SEARCH_TIMEOUT = 20
31
+ DEFAULT_CUSTOM_SEARCH_MAX_RETRIES = 4
32
+ DEFAULT_CUSTOM_SEARCH_INITIAL_BACKOFF_SECONDS = 1.0
33
+
34
+
35
+ def _normalize_data_dirs(data_dirs: list[str] | tuple[str, ...] | str | None, project_root: Path) -> list[str]:
36
+ if data_dirs is None:
37
+ return []
38
+ if isinstance(data_dirs, str):
39
+ items = [part.strip() for chunk in data_dirs.split(os.pathsep) for part in chunk.split(",")]
40
+ else:
41
+ items = [str(item).strip() for item in data_dirs]
42
+ resolved: list[str] = []
43
+ for item in items:
44
+ if not item:
45
+ continue
46
+ path = Path(item).expanduser()
47
+ if not path.is_absolute():
48
+ path = project_root / path
49
+ resolved.append(str(path))
50
+ return resolved
51
+
52
+
53
+ def resolve_docs_roots(data_dirs: list[str] | tuple[str, ...] | str | None = None) -> list[str]:
54
+ project_root = Path(__file__).resolve().parents[3]
55
+ env_value = os.environ.get("OFFICEQA_DOCS_DIR", "").strip()
56
+ candidates = _normalize_data_dirs(data_dirs, project_root)
57
+ candidates.extend(_normalize_data_dirs(env_value, project_root))
58
+ candidates.extend([
59
+ str(project_root / "data" / "officeqa_docs_official"),
60
+ str(project_root / "data" / "officeqa_smoke_docs"),
61
+ os.path.expanduser("~/officeqa-sparse/treasury_bulletins_parsed"),
62
+ os.path.expanduser("~/officeqa/treasury_bulletins_parsed"),
63
+ ])
64
+ roots: list[str] = []
65
+ seen: set[str] = set()
66
+ for candidate in candidates:
67
+ path = Path(candidate).expanduser()
68
+ if not path.is_dir():
69
+ continue
70
+ transformed = path / "transformed"
71
+ resolved = str((transformed if transformed.is_dir() else path).resolve())
72
+ if resolved in seen:
73
+ continue
74
+ seen.add(resolved)
75
+ roots.append(resolved)
76
+ if not roots:
77
+ raise FileNotFoundError("OfficeQA docs directory not found. Set OFFICEQA_DOCS_DIR or env.data_dirs.")
78
+ return roots
79
+
80
+
81
+ def _is_allowed(path: str, allowed_roots: list[str], allowed_files: list[str]) -> bool:
82
+ try:
83
+ resolved = str(Path(path).resolve())
84
+ except FileNotFoundError:
85
+ return False
86
+ if not any(resolved.startswith(root + os.sep) or resolved == root for root in allowed_roots):
87
+ return False
88
+ if not allowed_files:
89
+ return True
90
+ base = os.path.basename(resolved)
91
+ return base in allowed_files
92
+
93
+
94
+ def resolve_candidate_files(source_files: list[str], allowed_roots: list[str]) -> list[str]:
95
+ resolved: list[str] = []
96
+ seen: set[str] = set()
97
+ for root in allowed_roots:
98
+ for dirpath, _, filenames in os.walk(root):
99
+ for filename in filenames:
100
+ if source_files and filename not in source_files:
101
+ continue
102
+ full = str(Path(dirpath, filename).resolve())
103
+ if full in seen:
104
+ continue
105
+ seen.add(full)
106
+ resolved.append(full)
107
+ return resolved
108
+
109
+
110
+ def _as_list(value: object) -> list[str]:
111
+ if value is None:
112
+ return []
113
+ if isinstance(value, list):
114
+ return [str(item).strip() for item in value if str(item).strip()]
115
+ text = str(value).strip()
116
+ if not text:
117
+ return []
118
+ try:
119
+ loaded = json.loads(text)
120
+ except json.JSONDecodeError:
121
+ loaded = None
122
+ if isinstance(loaded, list):
123
+ return [str(item).strip() for item in loaded if str(item).strip()]
124
+ if "\n" in text:
125
+ return [part.strip() for part in text.splitlines() if part.strip()]
126
+ return [text]
127
+
128
+
129
+ def _extract_page_number(source_doc: str) -> int | None:
130
+ text = str(source_doc or "").strip()
131
+ if not text:
132
+ return None
133
+ parsed = urlparse(text)
134
+ query = parse_qs(parsed.query)
135
+ for key in ("page", "pagenum", "page_id"):
136
+ for raw_value in query.get(key, []):
137
+ try:
138
+ return int(str(raw_value).strip())
139
+ except ValueError:
140
+ continue
141
+ match = re.search(r"(?:[?&]|^)page=(\d+)", text)
142
+ if match:
143
+ return int(match.group(1))
144
+ return None
145
+
146
+
147
+ def _iter_oracle_refs(source_files: object, source_docs: object) -> list[tuple[str, int, str]]:
148
+ files = _as_list(source_files)
149
+ docs = _as_list(source_docs)
150
+ refs: list[tuple[str, int, str]] = []
151
+ seen: set[tuple[str, int, str]] = set()
152
+ if not files or not docs:
153
+ return refs
154
+ for index, source_doc in enumerate(docs):
155
+ page_number = _extract_page_number(source_doc)
156
+ if page_number is None:
157
+ continue
158
+ if index < len(files):
159
+ source_file = files[index]
160
+ elif len(files) == 1:
161
+ source_file = files[0]
162
+ else:
163
+ continue
164
+ key = (source_file, page_number, source_doc)
165
+ if key in seen:
166
+ continue
167
+ seen.add(key)
168
+ refs.append(key)
169
+ return refs
170
+
171
+
172
+ def _parsed_root_candidates(docs_roots: list[str]) -> list[Path]:
173
+ candidates: list[Path] = []
174
+ seen: set[str] = set()
175
+ for root in docs_roots:
176
+ path = Path(root).expanduser()
177
+ for candidate in (
178
+ path,
179
+ path.parent,
180
+ path / "treasury_bulletins_parsed",
181
+ path.parent / "treasury_bulletins_parsed",
182
+ ):
183
+ resolved = str(candidate.resolve()) if candidate.exists() else str(candidate)
184
+ if resolved in seen:
185
+ continue
186
+ seen.add(resolved)
187
+ candidates.append(candidate)
188
+ return candidates
189
+
190
+
191
+ def _locate_parsed_json(source_file: str, docs_roots: list[str]) -> Path | None:
192
+ source_path = Path(str(source_file).strip())
193
+ stem = source_path.stem if source_path.suffix else source_path.name
194
+ if not stem:
195
+ return None
196
+ candidate_names = [stem + ".json"]
197
+ if source_path.suffix == ".json":
198
+ candidate_names.insert(0, source_path.name)
199
+ for root in _parsed_root_candidates(docs_roots):
200
+ for name in candidate_names:
201
+ path = root / "jsons" / name
202
+ if path.is_file():
203
+ return path
204
+ return None
205
+
206
+
207
+ class _TableMarkdownParser(HTMLParser):
208
+ def __init__(self) -> None:
209
+ super().__init__(convert_charrefs=True)
210
+ self.rows: list[list[str]] = []
211
+ self._row: list[str] | None = None
212
+ self._cell: list[str] | None = None
213
+
214
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
215
+ if tag.lower() == "tr":
216
+ self._row = []
217
+ elif tag.lower() in {"td", "th"} and self._row is not None:
218
+ self._cell = []
219
+
220
+ def handle_data(self, data: str) -> None:
221
+ if self._cell is not None:
222
+ self._cell.append(data)
223
+
224
+ def handle_endtag(self, tag: str) -> None:
225
+ normalized_tag = tag.lower()
226
+ if normalized_tag in {"td", "th"} and self._cell is not None and self._row is not None:
227
+ cell = re.sub(r"\s+", " ", "".join(self._cell)).strip()
228
+ self._row.append(cell)
229
+ self._cell = None
230
+ elif normalized_tag == "tr" and self._row is not None:
231
+ if any(cell for cell in self._row):
232
+ self.rows.append(self._row)
233
+ self._row = None
234
+ self._cell = None
235
+
236
+
237
+ def _escape_markdown_cell(value: str) -> str:
238
+ return str(value).replace("\n", " ").replace("|", "\\|").strip()
239
+
240
+
241
+ def _html_table_to_markdown(raw_html: str) -> str:
242
+ parser = _TableMarkdownParser()
243
+ try:
244
+ parser.feed(raw_html)
245
+ except Exception: # noqa: BLE001
246
+ parser.rows = []
247
+ rows = parser.rows
248
+ if not rows:
249
+ text = re.sub(r"(?is)<[^>]+>", " ", raw_html)
250
+ return re.sub(r"\s+", " ", html.unescape(text)).strip()
251
+ width = max(len(row) for row in rows)
252
+ normalized_rows = [row + [""] * (width - len(row)) for row in rows]
253
+ header = normalized_rows[0]
254
+ body = normalized_rows[1:]
255
+ lines = [
256
+ "| " + " | ".join(_escape_markdown_cell(cell) for cell in header) + " |",
257
+ "| " + " | ".join(["---"] * width) + " |",
258
+ ]
259
+ lines.extend("| " + " | ".join(_escape_markdown_cell(cell) for cell in row) + " |" for row in body)
260
+ return "\n".join(lines)
261
+
262
+
263
+ def _render_parsed_content(content: str) -> str:
264
+ text = content.strip()
265
+ if not text:
266
+ return ""
267
+ if "<table" in text.lower():
268
+ return _html_table_to_markdown(text)
269
+ text = html.unescape(text)
270
+ text = re.sub(r"\r\n?", "\n", text)
271
+ return re.sub(r"\n{3,}", "\n\n", text).strip()
272
+
273
+
274
+ def _element_page_ids(element: dict) -> set[int]:
275
+ page_ids: set[int] = set()
276
+ bbox = element.get("bbox")
277
+ if not isinstance(bbox, list):
278
+ return page_ids
279
+ for box in bbox:
280
+ if not isinstance(box, dict):
281
+ continue
282
+ raw_page_id = box.get("page_id")
283
+ try:
284
+ page_ids.add(int(raw_page_id))
285
+ except (TypeError, ValueError):
286
+ continue
287
+ return page_ids
288
+
289
+
290
+ @lru_cache(maxsize=256)
291
+ def _load_parsed_elements(json_path: str) -> tuple[dict, ...]:
292
+ with open(json_path, encoding="utf-8") as f:
293
+ payload = json.load(f)
294
+ document = payload.get("document") if isinstance(payload, dict) else {}
295
+ elements = document.get("elements") if isinstance(document, dict) else []
296
+ if not isinstance(elements, list):
297
+ return ()
298
+ return tuple(element for element in elements if isinstance(element, dict))
299
+
300
+
301
+ @lru_cache(maxsize=2048)
302
+ def _render_parsed_page(json_path: str, page_number: int) -> str:
303
+ rendered: list[str] = []
304
+ for element in _load_parsed_elements(json_path):
305
+ if page_number not in _element_page_ids(element):
306
+ continue
307
+ content = element.get("content")
308
+ if not isinstance(content, str) or not content.strip():
309
+ continue
310
+ section = _render_parsed_content(content)
311
+ if section:
312
+ rendered.append(section)
313
+ return "\n\n".join(rendered).strip()
314
+
315
+
316
+ def build_oracle_parsed_pages_context(
317
+ source_files: object,
318
+ source_docs: object,
319
+ docs_roots: list[str],
320
+ *,
321
+ max_page_chars: int = _MAX_ORACLE_PAGE_CHARS,
322
+ max_total_chars: int = _MAX_ORACLE_CONTEXT_CHARS,
323
+ evidence_note: str = "Treat it as primary document evidence and combine it with custom web search results when useful.",
324
+ ) -> str:
325
+ """Render oracle parsed OfficeQA pages referenced by source_docs/source_files."""
326
+ refs = _iter_oracle_refs(source_files, source_docs)
327
+ if not refs:
328
+ return ""
329
+
330
+ blocks: list[str] = []
331
+ total_chars = 0
332
+ seen_pages: set[tuple[str, int]] = set()
333
+ for source_file, page_number, source_doc in refs:
334
+ json_path = _locate_parsed_json(source_file, docs_roots)
335
+ if json_path is None:
336
+ continue
337
+ page_key = (str(json_path), page_number)
338
+ if page_key in seen_pages:
339
+ continue
340
+ seen_pages.add(page_key)
341
+ page_text = _render_parsed_page(str(json_path), page_number)
342
+ if not page_text:
343
+ continue
344
+ if len(page_text) > max_page_chars:
345
+ omitted = len(page_text) - max_page_chars
346
+ page_text = page_text[:max_page_chars].rstrip() + f"\n\n[... {omitted} characters omitted from this parsed page ...]"
347
+ block = (
348
+ f"### {source_file} page {page_number}\n"
349
+ f"Source URL: {source_doc}\n\n"
350
+ f"{page_text}"
351
+ )
352
+ if total_chars + len(block) > max_total_chars:
353
+ remaining = max_total_chars - total_chars
354
+ if remaining <= 0:
355
+ break
356
+ block = block[:remaining].rstrip() + "\n\n[... oracle parsed page context truncated ...]"
357
+ blocks.append(block)
358
+ break
359
+ blocks.append(block)
360
+ total_chars += len(block)
361
+ if not blocks:
362
+ return ""
363
+ return (
364
+ "The following content is pre-parsed from the oracle OfficeQA source page(s). "
365
+ f"{evidence_note.strip()}\n\n"
366
+ + "\n\n".join(blocks)
367
+ )
368
+
369
+
370
+ def _extract_search_items(payload: object) -> list[dict]:
371
+ if isinstance(payload, list):
372
+ return [item for item in payload if isinstance(item, dict)]
373
+ if not isinstance(payload, dict):
374
+ return []
375
+ candidate_keys = (
376
+ "results",
377
+ "items",
378
+ "data",
379
+ "organic",
380
+ "organic_results",
381
+ "search_results",
382
+ "webPages",
383
+ "value",
384
+ )
385
+ for key in candidate_keys:
386
+ value = payload.get(key)
387
+ if isinstance(value, list):
388
+ return [item for item in value if isinstance(item, dict)]
389
+ if isinstance(value, dict):
390
+ nested = _extract_search_items(value)
391
+ if nested:
392
+ return nested
393
+ return []
394
+
395
+
396
+ def _normalize_search_item(item: dict, index: int) -> str:
397
+ title = str(
398
+ item.get("title")
399
+ or item.get("name")
400
+ or item.get("headline")
401
+ or item.get("source")
402
+ or f"Result {index}"
403
+ ).strip()
404
+ url = str(
405
+ item.get("url")
406
+ or item.get("link")
407
+ or item.get("href")
408
+ or item.get("display_url")
409
+ or ""
410
+ ).strip()
411
+ snippet = str(
412
+ item.get("snippet")
413
+ or item.get("description")
414
+ or item.get("body")
415
+ or item.get("text")
416
+ or item.get("content")
417
+ or ""
418
+ ).strip()
419
+ lines = [f"[{index}] {title}"]
420
+ if url:
421
+ lines.append(f"URL: {url}")
422
+ if snippet:
423
+ lines.append(f"Snippet: {snippet}")
424
+ return "\n".join(lines)
425
+
426
+
427
+ def _format_search_payload(query: str, payload: object) -> str:
428
+ items = _extract_search_items(payload)
429
+ header = f"Query: {query}"
430
+ if not items:
431
+ body = json.dumps(payload, ensure_ascii=False) if payload else "[no results]"
432
+ return f"{header}\n{body}"
433
+ rendered = [_normalize_search_item(item, index) for index, item in enumerate(items, start=1)]
434
+ return f"{header}\n\n" + "\n\n".join(rendered)
435
+
436
+
437
+ def _is_retryable_search_http_error(status_code: int) -> bool:
438
+ return status_code in {408, 429} or status_code >= 500
439
+
440
+
441
+ def custom_search(
442
+ query: str,
443
+ *,
444
+ api_url: str = DEFAULT_CUSTOM_SEARCH_URL,
445
+ auth_token: str | None = None,
446
+ auth_env: str = DEFAULT_CUSTOM_SEARCH_AUTH_ENV,
447
+ provider: str = DEFAULT_CUSTOM_SEARCH_PROVIDER,
448
+ max_num_results: int = DEFAULT_CUSTOM_SEARCH_MAX_RESULTS,
449
+ timeout: int = DEFAULT_CUSTOM_SEARCH_TIMEOUT,
450
+ max_retries: int = DEFAULT_CUSTOM_SEARCH_MAX_RETRIES,
451
+ initial_backoff_seconds: float = DEFAULT_CUSTOM_SEARCH_INITIAL_BACKOFF_SECONDS,
452
+ ) -> str:
453
+ query = str(query or "").strip()
454
+ if not query:
455
+ raise ValueError("custom_search query must be non-empty")
456
+ token = str(auth_token or os.environ.get(auth_env, "")).strip()
457
+ if not token:
458
+ raise ValueError(f"custom_search auth token missing; set {auth_env}")
459
+ payload = json.dumps(
460
+ {
461
+ "query": query,
462
+ "max_num_results": int(max_num_results),
463
+ "provider": provider,
464
+ },
465
+ ensure_ascii=False,
466
+ ).encode("utf-8")
467
+ req = Request(
468
+ api_url,
469
+ data=payload,
470
+ headers={
471
+ "Authorization": token,
472
+ "Content-Type": "application/json",
473
+ "User-Agent": DEFAULT_USER_AGENT,
474
+ },
475
+ method="POST",
476
+ )
477
+ attempts = max(1, int(max_retries) + 1)
478
+ last_error: RuntimeError | None = None
479
+ for attempt in range(1, attempts + 1):
480
+ try:
481
+ with urlopen(req, timeout=timeout) as response:
482
+ raw_body = response.read().decode("utf-8", errors="ignore")
483
+ break
484
+ except HTTPError as exc:
485
+ detail = exc.read().decode("utf-8", errors="ignore")
486
+ last_error = RuntimeError(f"custom_search HTTP {exc.code}: {detail[:1000]}")
487
+ if attempt >= attempts or not _is_retryable_search_http_error(exc.code):
488
+ raise last_error from exc
489
+ except (URLError, TimeoutError, socket.timeout) as exc:
490
+ last_error = RuntimeError(f"custom_search connection error: {exc}")
491
+ if attempt >= attempts:
492
+ raise last_error from exc
493
+ backoff_seconds = max(0.0, float(initial_backoff_seconds)) * (2 ** (attempt - 1))
494
+ if backoff_seconds > 0:
495
+ time.sleep(backoff_seconds)
496
+ else:
497
+ raise last_error or RuntimeError("custom_search failed without a captured error")
498
+ try:
499
+ parsed = json.loads(raw_body)
500
+ except json.JSONDecodeError:
501
+ return f"Query: {query}\n\n{raw_body.strip() or '[empty response]'}"
502
+ return _format_search_payload(query, parsed)
503
+
504
+
505
+ def run_tool(name: str, arguments: dict, *, allowed_roots: list[str], allowed_files: list[str]) -> tuple[str, str]:
506
+ if name == "glob":
507
+ pattern = str(arguments.get("pattern") or "*")
508
+ matches: list[str] = []
509
+ for root in allowed_roots:
510
+ for dirpath, _, filenames in os.walk(root):
511
+ for filename in filenames:
512
+ if allowed_files and filename not in allowed_files:
513
+ continue
514
+ rel = os.path.relpath(os.path.join(dirpath, filename), root)
515
+ if fnmatch.fnmatch(rel, pattern) or fnmatch.fnmatch(filename, pattern):
516
+ matches.append(os.path.join(dirpath, filename))
517
+ if len(matches) >= _MAX_GLOB_MATCHES:
518
+ break
519
+ if len(matches) >= _MAX_GLOB_MATCHES:
520
+ break
521
+ return f"glob(pattern={pattern!r})", "\n".join(matches) if matches else "[no matches]"
522
+
523
+ if name == "read":
524
+ path = str(arguments.get("path") or "")
525
+ if not path:
526
+ return "read(path='')", "[read error: missing path]"
527
+ if not _is_allowed(path, allowed_roots, allowed_files):
528
+ return f"read(path={path!r})", "[read error: path not allowed]"
529
+ start = max(int(arguments.get("start") or 1), 1)
530
+ limit = max(int(arguments.get("limit") or 80), 1)
531
+ with open(path, encoding="utf-8") as f:
532
+ lines = f.readlines()
533
+ excerpt = "".join(lines[start - 1:start - 1 + limit])
534
+ return f"read(path={path!r}, start={start}, limit={limit})", excerpt[:_MAX_READ_CHARS] or "[empty file]"
535
+
536
+ if name == "grep":
537
+ pattern = str(arguments.get("pattern") or "").lower()
538
+ path = str(arguments.get("path") or "")
539
+ if not pattern or not path:
540
+ return f"grep(pattern={pattern!r}, path={path!r})", "[grep error: missing pattern or path]"
541
+ if not _is_allowed(path, allowed_roots, allowed_files):
542
+ return f"grep(pattern={pattern!r}, path={path!r})", "[grep error: path not allowed]"
543
+ matches: list[str] = []
544
+ with open(path, encoding="utf-8") as f:
545
+ for idx, line in enumerate(f, start=1):
546
+ if pattern in line.lower():
547
+ matches.append(f"{idx}: {line.rstrip()}")
548
+ if len(matches) >= _MAX_GREP_MATCHES:
549
+ break
550
+ return f"grep(pattern={pattern!r}, path={path!r})", "\n".join(matches) if matches else "[no matches]"
551
+
552
+ return name, f"[tool error: unknown tool {name}]"
@@ -0,0 +1 @@
1
+ """SearchQA environment package for ReflACT."""
@@ -0,0 +1,96 @@
1
+ """SearchQA environment adapter for ReflACT."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+
6
+ from skillopt.datasets.base import BatchSpec
7
+ from skillopt.envs.base import EnvAdapter
8
+ from skillopt.envs.searchqa.dataloader import SearchQADataLoader
9
+ from skillopt.envs.searchqa.rollout import run_batch
10
+ from skillopt.model import get_target_backend
11
+
12
+
13
+ class SearchQAAdapter(EnvAdapter):
14
+ """SearchQA environment adapter."""
15
+
16
+ def __init__(
17
+ self,
18
+ split_dir: str = "",
19
+ data_path: str = "",
20
+ split_mode: str = "ratio",
21
+ split_ratio: str = "2:1:7",
22
+ split_seed: int = 42,
23
+ split_output_dir: str = "",
24
+ max_turns: int = 1,
25
+ exec_timeout: int = 120,
26
+ workers: int = 64,
27
+ analyst_workers: int = 16,
28
+ failure_only: bool = False,
29
+ minibatch_size: int = 8,
30
+ edit_budget: int = 4,
31
+ seed: int = 42,
32
+ limit: int = 0,
33
+ max_completion_tokens: int = 16384,
34
+ ) -> None:
35
+ self.max_turns = max_turns
36
+ self.exec_timeout = exec_timeout
37
+ self.workers = workers
38
+ self.max_completion_tokens = int(max_completion_tokens)
39
+ self.analyst_workers = analyst_workers
40
+ self.failure_only = failure_only
41
+ self.minibatch_size = minibatch_size
42
+ self.edit_budget = edit_budget
43
+ self.dataloader = SearchQADataLoader(
44
+ split_dir=split_dir,
45
+ data_path=data_path,
46
+ split_mode=split_mode,
47
+ split_ratio=split_ratio,
48
+ split_seed=split_seed,
49
+ split_output_dir=split_output_dir,
50
+ seed=seed,
51
+ limit=limit,
52
+ )
53
+
54
+ def setup(self, cfg: dict) -> None:
55
+ super().setup(cfg)
56
+ self.dataloader.setup(cfg)
57
+
58
+ def get_dataloader(self):
59
+ return self.dataloader
60
+
61
+ def build_env_from_batch(self, batch: BatchSpec, **kwargs):
62
+ return list(batch.payload or [])
63
+
64
+ def build_train_env(self, batch_size: int, seed: int, **kwargs):
65
+ batch = self.dataloader.build_train_batch(batch_size=batch_size, seed=seed, **kwargs)
66
+ return self.build_env_from_batch(batch, **kwargs)
67
+
68
+ def build_eval_env(self, env_num: int, split: str, seed: int, **kwargs):
69
+ batch = self.dataloader.build_eval_batch(env_num=env_num, split=split, seed=seed, **kwargs)
70
+ return self.build_env_from_batch(batch, **kwargs)
71
+
72
+ def rollout(
73
+ self,
74
+ env_manager, # actually list[dict] for SearchQA
75
+ skill_content: str,
76
+ out_dir: str,
77
+ **kwargs,
78
+ ) -> list[dict]:
79
+ """Run QA agent on items. Resume-aware."""
80
+ items: list[dict] = env_manager # type alias for clarity
81
+ return run_batch(
82
+ items=items,
83
+ out_root=out_dir,
84
+ skill_content=skill_content,
85
+ max_turns=self.max_turns,
86
+ exec_timeout=self.exec_timeout,
87
+ workers=self.workers,
88
+ max_completion_tokens=self.max_completion_tokens,
89
+ diagnostic_mode=kwargs.get("diagnostic_mode", False),
90
+ diagnostic_instruction=kwargs.get("diagnostic_instruction", ""),
91
+ diagnostic_trace_context_by_id=kwargs.get("diagnostic_trace_context_by_id"),
92
+ task_timeout=self.exec_timeout,
93
+ )
94
+
95
+ def get_task_types(self) -> list[str]:
96
+ return ["qa"]