synth-ai 0.1.9__py3-none-any.whl → 0.2.1.dev0__py3-none-any.whl

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 (266) hide show
  1. synth_ai/__init__.py +28 -2
  2. synth_ai/core/system.py +4 -0
  3. synth_ai/environments/__init__.py +35 -0
  4. synth_ai/environments/environment/__init__.py +1 -0
  5. synth_ai/environments/environment/artifacts/__init__.py +1 -0
  6. synth_ai/environments/environment/artifacts/base.py +50 -0
  7. synth_ai/environments/environment/core.py +22 -0
  8. synth_ai/environments/environment/db/__init__.py +1 -0
  9. synth_ai/environments/environment/db/sqlite.py +45 -0
  10. synth_ai/environments/environment/registry.py +24 -0
  11. synth_ai/environments/environment/resources/sqlite.py +46 -0
  12. synth_ai/environments/environment/results.py +1 -0
  13. synth_ai/environments/environment/rewards/__init__.py +1 -0
  14. synth_ai/environments/environment/rewards/core.py +28 -0
  15. synth_ai/environments/environment/shared_engine.py +26 -0
  16. synth_ai/environments/environment/tools/__init__.py +34 -0
  17. synth_ai/environments/examples/__init__.py +1 -0
  18. synth_ai/environments/examples/crafter_classic/__init__.py +8 -0
  19. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +58 -0
  20. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +152 -0
  21. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +1194 -0
  22. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +51 -0
  23. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +872 -0
  24. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +1412 -0
  25. synth_ai/environments/examples/crafter_classic/agent_demos/test_crafter_react_agent.py +1110 -0
  26. synth_ai/environments/examples/crafter_classic/config_logging.py +111 -0
  27. synth_ai/environments/examples/crafter_classic/engine.py +502 -0
  28. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +63 -0
  29. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +5 -0
  30. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +74 -0
  31. synth_ai/environments/examples/crafter_classic/environment.py +255 -0
  32. synth_ai/environments/examples/crafter_classic/taskset.py +228 -0
  33. synth_ai/environments/examples/enron/agent_demos/test_synth_react.py +535 -0
  34. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +156 -0
  35. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +280 -0
  36. synth_ai/environments/examples/enron/art_helpers/types_enron.py +24 -0
  37. synth_ai/environments/examples/enron/engine.py +291 -0
  38. synth_ai/environments/examples/enron/environment.py +165 -0
  39. synth_ai/environments/examples/enron/taskset.py +112 -0
  40. synth_ai/environments/examples/enron/units/keyword_stats.py +111 -0
  41. synth_ai/environments/examples/enron/units/test_email_index.py +8 -0
  42. synth_ai/environments/examples/minigrid/__init__.py +48 -0
  43. synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +1188 -0
  44. synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +47 -0
  45. synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +562 -0
  46. synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +220 -0
  47. synth_ai/environments/examples/minigrid/agent_demos/test_minigrid_react_agent.py +393 -0
  48. synth_ai/environments/examples/minigrid/engine.py +589 -0
  49. synth_ai/environments/examples/minigrid/environment.py +274 -0
  50. synth_ai/environments/examples/minigrid/environment_mapping.py +242 -0
  51. synth_ai/environments/examples/minigrid/puzzle_loader.py +416 -0
  52. synth_ai/environments/examples/minigrid/taskset.py +583 -0
  53. synth_ai/environments/examples/minigrid/units/test_action_behavior.py +226 -0
  54. synth_ai/environments/examples/minigrid/units/test_debug_messages.py +83 -0
  55. synth_ai/environments/examples/minigrid/units/test_exploration.py +120 -0
  56. synth_ai/environments/examples/minigrid/units/test_minigrid_engine.py +214 -0
  57. synth_ai/environments/examples/minigrid/units/test_minigrid_environment.py +238 -0
  58. synth_ai/environments/examples/minigrid/units/test_minigrid_environment_mapping.py +301 -0
  59. synth_ai/environments/examples/minigrid/units/test_minigrid_taskset.py +210 -0
  60. synth_ai/environments/examples/nethack/__init__.py +7 -0
  61. synth_ai/environments/examples/nethack/achievements.py +337 -0
  62. synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +981 -0
  63. synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +74 -0
  64. synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +832 -0
  65. synth_ai/environments/examples/nethack/agent_demos/test_nethack_react_agent.py +1112 -0
  66. synth_ai/environments/examples/nethack/engine.py +738 -0
  67. synth_ai/environments/examples/nethack/environment.py +255 -0
  68. synth_ai/environments/examples/nethack/helpers/__init__.py +42 -0
  69. synth_ai/environments/examples/nethack/helpers/action_mapping.py +301 -0
  70. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +401 -0
  71. synth_ai/environments/examples/nethack/helpers/observation_utils.py +433 -0
  72. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +201 -0
  73. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +268 -0
  74. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +308 -0
  75. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +430 -0
  76. synth_ai/environments/examples/nethack/taskset.py +323 -0
  77. synth_ai/environments/examples/nethack/units/test_nethack_engine.py +277 -0
  78. synth_ai/environments/examples/nethack/units/test_nethack_environment.py +281 -0
  79. synth_ai/environments/examples/nethack/units/test_nethack_taskset.py +213 -0
  80. synth_ai/environments/examples/nethack/units/test_recording.py +307 -0
  81. synth_ai/environments/examples/red/__init__.py +7 -0
  82. synth_ai/environments/examples/red/agent_demos/__init__.py +1 -0
  83. synth_ai/environments/examples/red/agent_demos/test_synth_react.py +1471 -0
  84. synth_ai/environments/examples/red/config_logging.py +110 -0
  85. synth_ai/environments/examples/red/engine.py +693 -0
  86. synth_ai/environments/examples/red/engine_helpers/__init__.py +1 -0
  87. synth_ai/environments/examples/red/engine_helpers/memory_map.py +28 -0
  88. synth_ai/environments/examples/red/engine_helpers/reward_components.py +275 -0
  89. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +142 -0
  90. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +56 -0
  91. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +283 -0
  92. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +149 -0
  93. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +137 -0
  94. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +56 -0
  95. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +330 -0
  96. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +120 -0
  97. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +558 -0
  98. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +312 -0
  99. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +147 -0
  100. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +246 -0
  101. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +367 -0
  102. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +139 -0
  103. synth_ai/environments/examples/red/environment.py +235 -0
  104. synth_ai/environments/examples/red/taskset.py +77 -0
  105. synth_ai/environments/examples/red/test_fixes.py +125 -0
  106. synth_ai/environments/examples/red/test_fixes_mock.py +148 -0
  107. synth_ai/environments/examples/red/units/__init__.py +1 -0
  108. synth_ai/environments/examples/red/units/test_basic_functionality.py +97 -0
  109. synth_ai/environments/examples/red/units/test_button_press_requirements.py +217 -0
  110. synth_ai/environments/examples/red/units/test_engine.py +192 -0
  111. synth_ai/environments/examples/red/units/test_environment.py +455 -0
  112. synth_ai/environments/examples/red/units/test_exploration_strategy.py +227 -0
  113. synth_ai/environments/examples/red/units/test_integration.py +217 -0
  114. synth_ai/environments/examples/red/units/test_memory_extraction.py +111 -0
  115. synth_ai/environments/examples/red/units/test_menu_bug_reproduction.py +1100 -0
  116. synth_ai/environments/examples/red/units/test_movement_debug.py +255 -0
  117. synth_ai/environments/examples/red/units/test_pokemon_mcts_debug.py +163 -0
  118. synth_ai/environments/examples/red/units/test_pokemon_mcts_verbose.py +117 -0
  119. synth_ai/environments/examples/red/units/test_red_basic.py +145 -0
  120. synth_ai/environments/examples/red/units/test_red_comprehensive.py +323 -0
  121. synth_ai/environments/examples/red/units/test_retry_movement.py +195 -0
  122. synth_ai/environments/examples/red/units/test_reward_components.py +186 -0
  123. synth_ai/environments/examples/red/units/test_rom_integration.py +260 -0
  124. synth_ai/environments/examples/red/units/test_taskset.py +116 -0
  125. synth_ai/environments/examples/red/units/test_tree.py +448 -0
  126. synth_ai/environments/examples/sokoban/__init__.py +1 -0
  127. synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +900 -0
  128. synth_ai/environments/examples/sokoban/agent_demos/test_dspy_react.py +1 -0
  129. synth_ai/environments/examples/sokoban/agent_demos/test_sokoban_react_agent.py +498 -0
  130. synth_ai/environments/examples/sokoban/agent_demos/test_synth_lats.py +1 -0
  131. synth_ai/environments/examples/sokoban/agent_demos/test_synth_react_locally.py +748 -0
  132. synth_ai/environments/examples/sokoban/agent_demos/test_synth_react_service.py +296 -0
  133. synth_ai/environments/examples/sokoban/engine.py +675 -0
  134. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +1 -0
  135. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +656 -0
  136. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +17 -0
  137. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +3 -0
  138. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +129 -0
  139. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +370 -0
  140. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +331 -0
  141. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +305 -0
  142. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +66 -0
  143. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +114 -0
  144. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +122 -0
  145. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +394 -0
  146. synth_ai/environments/examples/sokoban/environment.py +228 -0
  147. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +438 -0
  148. synth_ai/environments/examples/sokoban/puzzle_loader.py +311 -0
  149. synth_ai/environments/examples/sokoban/taskset.py +425 -0
  150. synth_ai/environments/examples/sokoban/units/astar_common.py +94 -0
  151. synth_ai/environments/examples/sokoban/units/test_building_task_set.py +49 -0
  152. synth_ai/environments/examples/sokoban/units/test_false_positive.py +120 -0
  153. synth_ai/environments/examples/sokoban/units/test_simple_run_through_environment.py +119 -0
  154. synth_ai/environments/examples/sokoban/units/test_sokoban_environment.py +98 -0
  155. synth_ai/environments/examples/sokoban/units/test_tree.py +364 -0
  156. synth_ai/environments/examples/tictactoe/__init__.py +1 -0
  157. synth_ai/environments/examples/tictactoe/agent_demos/test_synth_react.py +266 -0
  158. synth_ai/environments/examples/tictactoe/agent_demos/test_tictactoe_react_agent.py +470 -0
  159. synth_ai/environments/examples/tictactoe/engine.py +368 -0
  160. synth_ai/environments/examples/tictactoe/environment.py +239 -0
  161. synth_ai/environments/examples/tictactoe/taskset.py +214 -0
  162. synth_ai/environments/examples/tictactoe/units/test_tictactoe_engine.py +393 -0
  163. synth_ai/environments/examples/tictactoe/units/test_tictactoe_environment.py +493 -0
  164. synth_ai/environments/examples/tictactoe/units/test_tictactoe_taskset.py +191 -0
  165. synth_ai/environments/examples/verilog/__init__.py +10 -0
  166. synth_ai/environments/examples/verilog/agent_demos/test_synth_react.py +520 -0
  167. synth_ai/environments/examples/verilog/engine.py +328 -0
  168. synth_ai/environments/examples/verilog/environment.py +349 -0
  169. synth_ai/environments/examples/verilog/taskset.py +418 -0
  170. synth_ai/environments/examples/verilog/units/test_verilog_engine.py +466 -0
  171. synth_ai/environments/examples/verilog/units/test_verilog_environment.py +585 -0
  172. synth_ai/environments/examples/verilog/units/test_verilog_integration.py +383 -0
  173. synth_ai/environments/examples/verilog/units/test_verilog_taskset.py +457 -0
  174. synth_ai/environments/reproducibility/core.py +42 -0
  175. synth_ai/environments/reproducibility/tree.py +364 -0
  176. synth_ai/environments/service/app.py +78 -0
  177. synth_ai/environments/service/core_routes.py +775 -0
  178. synth_ai/environments/service/external_registry.py +57 -0
  179. synth_ai/environments/service/registry.py +9 -0
  180. synth_ai/environments/stateful/__init__.py +1 -0
  181. synth_ai/environments/stateful/core.py +28 -0
  182. synth_ai/environments/stateful/engine.py +21 -0
  183. synth_ai/environments/stateful/state.py +7 -0
  184. synth_ai/environments/tasks/api.py +19 -0
  185. synth_ai/environments/tasks/core.py +78 -0
  186. synth_ai/environments/tasks/filters.py +39 -0
  187. synth_ai/environments/tasks/utils.py +89 -0
  188. synth_ai/environments/v0_observability/history.py +3 -0
  189. synth_ai/environments/v0_observability/log.py +2 -0
  190. synth_ai/lm/caching/constants.py +1 -0
  191. synth_ai/{zyk/lms → lm}/caching/ephemeral.py +4 -8
  192. synth_ai/{zyk/lms → lm}/caching/handler.py +15 -15
  193. synth_ai/{zyk/lms → lm}/caching/initialize.py +2 -4
  194. synth_ai/{zyk/lms → lm}/caching/persistent.py +4 -10
  195. synth_ai/{zyk/lms → lm}/config.py +2 -1
  196. synth_ai/{zyk/lms → lm}/constants.py +2 -2
  197. synth_ai/{zyk/lms → lm}/core/all.py +10 -10
  198. synth_ai/{zyk/lms → lm}/core/main.py +57 -33
  199. synth_ai/{zyk/lms → lm}/core/vendor_clients.py +12 -10
  200. synth_ai/lm/cost/monitor.py +1 -0
  201. synth_ai/lm/cost/statefulness.py +1 -0
  202. synth_ai/lm/provider_support/__init__.py +8 -0
  203. synth_ai/lm/provider_support/anthropic.py +945 -0
  204. synth_ai/lm/provider_support/openai.py +1115 -0
  205. synth_ai/lm/provider_support/suppress_logging.py +31 -0
  206. synth_ai/{zyk/lms → lm}/structured_outputs/handler.py +58 -80
  207. synth_ai/{zyk/lms → lm}/structured_outputs/inject.py +6 -20
  208. synth_ai/{zyk/lms → lm}/structured_outputs/rehabilitate.py +6 -12
  209. synth_ai/{zyk/lms → lm}/vendors/core/anthropic_api.py +21 -30
  210. synth_ai/{zyk/lms → lm}/vendors/core/gemini_api.py +37 -32
  211. synth_ai/{zyk/lms → lm}/vendors/core/mistral_api.py +19 -28
  212. synth_ai/{zyk/lms → lm}/vendors/core/openai_api.py +26 -36
  213. synth_ai/{zyk/lms → lm}/vendors/openai_standard.py +29 -33
  214. synth_ai/{zyk/lms → lm}/vendors/retries.py +1 -1
  215. synth_ai/lm/vendors/supported/__init__.py +0 -0
  216. synth_ai/{zyk/lms → lm}/vendors/supported/custom_endpoint.py +131 -118
  217. synth_ai/{zyk/lms → lm}/vendors/supported/deepseek.py +4 -8
  218. synth_ai/{zyk/lms → lm}/vendors/supported/grok.py +6 -8
  219. synth_ai/{zyk/lms → lm}/vendors/supported/groq.py +1 -1
  220. synth_ai/{zyk/lms → lm}/vendors/supported/ollama.py +2 -2
  221. synth_ai/{zyk/lms → lm}/vendors/supported/openrouter.py +18 -16
  222. synth_ai/{zyk/lms → lm}/vendors/supported/together.py +1 -1
  223. synth_ai/tracing/__init__.py +0 -0
  224. synth_ai/tracing/abstractions.py +224 -0
  225. synth_ai/tracing/base_client.py +91 -0
  226. synth_ai/tracing/client_manager.py +131 -0
  227. synth_ai/tracing/config.py +140 -0
  228. synth_ai/tracing/context.py +146 -0
  229. synth_ai/tracing/decorators.py +679 -0
  230. synth_ai/tracing/events/__init__.py +0 -0
  231. synth_ai/tracing/events/manage.py +147 -0
  232. synth_ai/tracing/events/scope.py +86 -0
  233. synth_ai/tracing/events/store.py +227 -0
  234. synth_ai/tracing/immediate_client.py +152 -0
  235. synth_ai/tracing/local.py +18 -0
  236. synth_ai/tracing/log_client_base.py +74 -0
  237. synth_ai/tracing/retry_queue.py +187 -0
  238. synth_ai/tracing/trackers.py +515 -0
  239. synth_ai/tracing/upload.py +504 -0
  240. synth_ai/tracing/utils.py +9 -0
  241. synth_ai/zyk/__init__.py +28 -2
  242. synth_ai-0.2.1.dev0.dist-info/METADATA +349 -0
  243. synth_ai-0.2.1.dev0.dist-info/RECORD +261 -0
  244. synth_ai/zyk/lms/caching/constants.py +0 -1
  245. synth_ai/zyk/lms/cost/monitor.py +0 -1
  246. synth_ai/zyk/lms/cost/statefulness.py +0 -1
  247. synth_ai-0.1.9.dist-info/METADATA +0 -37
  248. synth_ai-0.1.9.dist-info/RECORD +0 -50
  249. /synth_ai/{zyk/lms/__init__.py → environments/reproducibility/helpers.py} +0 -0
  250. /synth_ai/{zyk/lms/caching → lm}/__init__.py +0 -0
  251. /synth_ai/{zyk/lms/core → lm/caching}/__init__.py +0 -0
  252. /synth_ai/{zyk/lms → lm}/caching/dbs.py +0 -0
  253. /synth_ai/{zyk/lms/cost → lm/core}/__init__.py +0 -0
  254. /synth_ai/{zyk/lms → lm}/core/exceptions.py +0 -0
  255. /synth_ai/{zyk/lms/structured_outputs → lm/cost}/__init__.py +0 -0
  256. /synth_ai/{zyk/lms/vendors → lm/structured_outputs}/__init__.py +0 -0
  257. /synth_ai/{zyk/lms → lm}/tools/__init__.py +0 -0
  258. /synth_ai/{zyk/lms → lm}/tools/base.py +0 -0
  259. /synth_ai/{zyk/lms/vendors/core → lm/vendors}/__init__.py +0 -0
  260. /synth_ai/{zyk/lms → lm}/vendors/base.py +0 -0
  261. /synth_ai/{zyk/lms/vendors/local → lm/vendors/core}/__init__.py +0 -0
  262. /synth_ai/{zyk/lms/vendors/supported → lm/vendors/local}/__init__.py +0 -0
  263. /synth_ai/{zyk/lms → lm}/vendors/local/ollama.py +0 -0
  264. {synth_ai-0.1.9.dist-info → synth_ai-0.2.1.dev0.dist-info}/WHEEL +0 -0
  265. {synth_ai-0.1.9.dist-info → synth_ai-0.2.1.dev0.dist-info}/licenses/LICENSE +0 -0
  266. {synth_ai-0.1.9.dist-info → synth_ai-0.2.1.dev0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,228 @@
1
+ from typing import List, Optional, Any, Dict, Union
2
+ from pydantic import BaseModel
3
+ import dataclasses
4
+
5
+ from synth_ai.environments.examples.sokoban.engine import (
6
+ SokobanEngine,
7
+ SynthSokobanObservationCallable,
8
+ SokobanPrivateState,
9
+ SokobanPublicState,
10
+ SynthSokobanCheckpointObservationCallable,
11
+ SokobanEngineSnapshot,
12
+ )
13
+ from synth_ai.environments.environment.shared_engine import (
14
+ GetObservationCallable,
15
+ InternalObservation,
16
+ )
17
+ from synth_ai.environments.reproducibility.core import ReproducibleEnvironment
18
+ from synth_ai.environments.stateful.core import StatefulEnvironment
19
+ from synth_ai.environments.tasks.core import TaskInstance
20
+ from synth_ai.environments.environment.tools import (
21
+ AbstractTool,
22
+ EnvToolCall,
23
+ ToolResult,
24
+ TOOL_REGISTRY,
25
+ register_tool,
26
+ )
27
+
28
+
29
+ # --- Tool Definition ---
30
+ class SokobanActionInput(BaseModel):
31
+ action: int
32
+
33
+
34
+ class SokobanInteractTool(AbstractTool):
35
+ name = "interact"
36
+ description = "Performs an action (e.g., move) in the Sokoban environment."
37
+ call_schema = SokobanActionInput
38
+ result_schema = ToolResult
39
+
40
+ def __init__(self, engine: SokobanEngine):
41
+ self.engine = engine
42
+
43
+ async def __call__(self, call: EnvToolCall) -> ToolResult:
44
+ try:
45
+ validated_args = self.call_schema(**call.args)
46
+ priv_state, pub_state = await self.engine._step_engine(validated_args.action)
47
+ return ToolResult(
48
+ ok=True,
49
+ payload={
50
+ "public": pub_state.to_dict(),
51
+ "private": priv_state.to_dict(),
52
+ },
53
+ )
54
+ except Exception as e:
55
+ # Add current public state to payload for context in case of error
56
+ _, pub_state_on_error = self.engine.get_current_states_for_observation()
57
+ return ToolResult(
58
+ ok=False,
59
+ error=str(e),
60
+ payload={"public": pub_state_on_error.to_dict()},
61
+ )
62
+
63
+
64
+ class SokobanEnvironment(StatefulEnvironment, ReproducibleEnvironment[SokobanEngine]):
65
+ def __init__(
66
+ self,
67
+ task_instance: TaskInstance,
68
+ custom_step_obs: Optional[GetObservationCallable] = None,
69
+ custom_ckpt_obs: Optional[GetObservationCallable] = None,
70
+ ):
71
+ self.name = "Sokoban"
72
+ self.task_instance = task_instance
73
+ # Default to SynthSokobanObservationCallable if none provided
74
+ self.custom_step_observation_callable = custom_step_obs or SynthSokobanObservationCallable()
75
+ self.custom_checkpoint_observation_callable = (
76
+ custom_ckpt_obs or SynthSokobanCheckpointObservationCallable()
77
+ )
78
+ self.engine: SokobanEngine = SokobanEngine(task_instance)
79
+
80
+ self._interact_tool = SokobanInteractTool(self.engine)
81
+ if self._interact_tool.name not in TOOL_REGISTRY:
82
+ register_tool(self._interact_tool)
83
+ # elif getattr(TOOL_REGISTRY[self._interact_tool.name], 'engine', None) is not self.engine:
84
+ # register_tool(self._interact_tool) # More robust check if tool has engine attr
85
+
86
+ async def initialize(self) -> InternalObservation:
87
+ priv, pub = await self.engine._reset_engine()
88
+ return await self._to_observation(priv, pub, self.custom_step_observation_callable)
89
+
90
+ async def terminate(self) -> InternalObservation:
91
+ priv, pub = self.engine.get_current_states_for_observation()
92
+ priv.terminated = True # Mark as terminated
93
+ obs_dict = {"terminated": True, "message": "Environment terminated."}
94
+ # Use _to_observation to format, including final state
95
+ return await self._to_observation(
96
+ priv, pub, self.custom_step_observation_callable, extra_obs=obs_dict
97
+ )
98
+
99
+ def validate_tool_calls(
100
+ self,
101
+ tool_calls: Union[
102
+ EnvToolCall,
103
+ List[Dict[str, Any]],
104
+ List[List[Dict[str, Any]]],
105
+ Dict[str, Any],
106
+ ],
107
+ ) -> EnvToolCall:
108
+ # Normalize and validate to a single EnvToolCall
109
+ raw_call_data: Dict[str, Any]
110
+ if isinstance(tool_calls, list):
111
+ if not tool_calls:
112
+ raise ValueError("Received empty list of tool calls.")
113
+ first_item = tool_calls[0]
114
+ if isinstance(first_item, list):
115
+ if not first_item:
116
+ raise ValueError("Received empty inner list of tool calls.")
117
+ raw_call_data = first_item[0]
118
+ elif isinstance(first_item, dict):
119
+ raw_call_data = first_item
120
+ elif isinstance(first_item, EnvToolCall): # Already an EnvToolCall instance
121
+ agent_call = first_item # Assuming direct single call if already instance
122
+ if agent_call.tool != "interact":
123
+ raise ValueError(f"Unknown tool: {agent_call.tool}. Expected 'interact'.")
124
+ return agent_call
125
+ else:
126
+ raise TypeError(f"Unexpected type in tool_calls list: {type(first_item)}")
127
+ elif isinstance(tool_calls, dict): # Single call passed as dict
128
+ raw_call_data = tool_calls
129
+ elif isinstance(tool_calls, EnvToolCall): # Single call already an instance
130
+ if tool_calls.tool != "interact":
131
+ raise ValueError(f"Unknown tool: {tool_calls.tool}. Expected 'interact'.")
132
+ return tool_calls
133
+ else:
134
+ raise TypeError(f"Unexpected type for tool_calls: {type(tool_calls)}")
135
+
136
+ if not isinstance(raw_call_data, dict):
137
+ raise TypeError(f"Processed call data is not a dict: {type(raw_call_data)}")
138
+
139
+ # Convert dict to EnvToolCall instance
140
+ tool_name = raw_call_data.get("tool")
141
+ tool_args = raw_call_data.get("args", {})
142
+ if tool_name != "interact":
143
+ raise ValueError(f"Unknown tool: {tool_name}. Expected 'interact'.")
144
+
145
+ agent_call = EnvToolCall(tool=tool_name, args=tool_args)
146
+ return agent_call
147
+
148
+ async def step(
149
+ self,
150
+ tool_calls: Union[
151
+ EnvToolCall,
152
+ List[Dict[str, Any]],
153
+ List[List[Dict[str, Any]]],
154
+ Dict[str, Any],
155
+ ],
156
+ ) -> InternalObservation:
157
+ agent_call = self.validate_tool_calls(tool_calls)
158
+ tool_result: ToolResult = await self._interact_tool(agent_call)
159
+
160
+ payload_dict = tool_result.payload
161
+ if not tool_result.ok or not isinstance(payload_dict, dict): # Check tool_result.ok
162
+ # Fallback if payload isn't as expected or tool reported an error
163
+ priv_state, pub_state = self.engine.get_current_states_for_observation()
164
+ if tool_result.error and hasattr(pub_state, "error_info"):
165
+ pub_state.error_info = tool_result.error
166
+ else:
167
+ # This block assumes tool_result.ok is True and payload is a dict
168
+ priv_dict = payload_dict.get("private")
169
+ pub_dict = payload_dict.get("public")
170
+
171
+ if priv_dict is None or pub_dict is None:
172
+ # This case should ideally not happen if tool_result.ok is True
173
+ # and the tool is well-behaved, but as a safeguard:
174
+ priv_state, pub_state = self.engine.get_current_states_for_observation()
175
+ if tool_result.error and hasattr(
176
+ pub_state, "error_info"
177
+ ): # Apply error even in this sub-optimal case
178
+ pub_state.error_info = tool_result.error
179
+ else:
180
+ priv_state = SokobanPrivateState(**priv_dict)
181
+ pub_state = SokobanPublicState(**pub_dict)
182
+ if tool_result.error and hasattr(pub_state, "error_info"):
183
+ pub_state.error_info = tool_result.error
184
+
185
+ return await self._to_observation(
186
+ priv_state, pub_state, self.custom_step_observation_callable
187
+ )
188
+
189
+ async def checkpoint(self) -> InternalObservation:
190
+ engine_snapshot: SokobanEngineSnapshot = await self.engine._serialize_engine()
191
+ # For checkpoint, we might want to convey the snapshot data differently.
192
+ # The existing _to_observation expects live priv/pub states.
193
+ # For now, using current live states for observation, plus snapshot.
194
+ priv, pub = self.engine.get_current_states_for_observation()
195
+ obs_data = await self._to_observation(
196
+ priv, pub, self.custom_checkpoint_observation_callable
197
+ )
198
+ if isinstance(obs_data, dict):
199
+ obs_data["engine_snapshot_data"] = (
200
+ engine_snapshot.model_dump()
201
+ ) # Add snapshot if obs is dict
202
+ return obs_data
203
+
204
+ async def _to_observation(
205
+ self,
206
+ priv: SokobanPrivateState,
207
+ pub: SokobanPublicState,
208
+ obs_cb: Optional[GetObservationCallable],
209
+ extra_obs: Optional[Dict[str, Any]] = None, # For adding things like termination messages
210
+ ) -> InternalObservation:
211
+ # Ensure obs_cb is not None; use a default if necessary (though __init__ sets one)
212
+ active_obs_cb = obs_cb or SynthSokobanObservationCallable()
213
+ observation = await active_obs_cb.get_observation(pub, priv)
214
+ if extra_obs and isinstance(observation, dict):
215
+ observation.update(extra_obs)
216
+ return observation
217
+
218
+ async def _serialize_engine(self) -> SokobanEngineSnapshot: # Changed type hint
219
+ return await self.engine._serialize_engine()
220
+
221
+ @classmethod
222
+ async def _deserialize_engine(
223
+ cls, snapshot: SokobanEngineSnapshot, task_instance: TaskInstance
224
+ ) -> "SokobanEnvironment": # Changed type hint
225
+ eng = await SokobanEngine._deserialize_engine(snapshot, task_instance)
226
+ env = cls(task_instance) # Uses task_instance from deserialized engine
227
+ env.engine = eng
228
+ return env
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate verified solvable Sokoban puzzles.
4
+
5
+ This script creates 500 solvable Sokoban puzzles (100 each for 5 difficulty levels)
6
+ and saves them as JSON. Each puzzle is verified to be solvable using BFS.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import numpy as np
12
+ from typing import Dict, List, Tuple, Optional, Any, Set
13
+ from pathlib import Path
14
+ from dataclasses import dataclass, asdict
15
+ from synth_ai.environments.examples.sokoban.engine_helpers.room_utils import (
16
+ generate_room,
17
+ get_shortest_action_path,
18
+ )
19
+
20
+ # Set up logging
21
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @dataclass
26
+ class SokobanPuzzle:
27
+ """Represents a verified solvable Sokoban puzzle."""
28
+
29
+ id: str
30
+ difficulty: str
31
+ num_boxes: int
32
+ dim_room: Tuple[int, int]
33
+ room_fixed: List[List[int]]
34
+ room_state: List[List[int]]
35
+ box_mapping: Dict[str, List[int]]
36
+ solution_path: List[int]
37
+ solution_length: int
38
+ generation_seed: int
39
+ max_steps: int
40
+
41
+
42
+ # Define difficulty configurations
43
+ DIFFICULTY_CONFIGS = {
44
+ "ultra_easy": {
45
+ "num_boxes": 1,
46
+ "dim_room": (5, 5),
47
+ "max_steps": 50,
48
+ "target_solution_length": (3, 8),
49
+ "search_depth": 30,
50
+ },
51
+ "easy": {
52
+ "num_boxes": 1,
53
+ "dim_room": (6, 6),
54
+ "max_steps": 80,
55
+ "target_solution_length": (8, 15),
56
+ "search_depth": 50,
57
+ },
58
+ "medium": {
59
+ "num_boxes": 2,
60
+ "dim_room": (7, 7),
61
+ "max_steps": 120,
62
+ "target_solution_length": (15, 30),
63
+ "search_depth": 80,
64
+ },
65
+ "hard": {
66
+ "num_boxes": 3,
67
+ "dim_room": (8, 8),
68
+ "max_steps": 200,
69
+ "target_solution_length": (30, 60),
70
+ "search_depth": 120,
71
+ },
72
+ }
73
+
74
+
75
+ def verify_puzzle_solvable(
76
+ room_fixed: np.ndarray, room_state: np.ndarray, max_depth: int = 200
77
+ ) -> Optional[List[int]]:
78
+ """
79
+ Verify that a puzzle is solvable using BFS and return the solution path.
80
+
81
+ Args:
82
+ room_fixed: The fixed room structure (walls, targets, floors)
83
+ room_state: The current room state (player, boxes)
84
+ max_depth: Maximum search depth
85
+
86
+ Returns:
87
+ List of actions if solvable, None if not solvable
88
+ """
89
+ try:
90
+ solution_path = get_shortest_action_path(room_fixed, room_state, MAX_DEPTH=max_depth)
91
+ return solution_path if solution_path else None
92
+ except Exception as e:
93
+ logger.warning(f"Error verifying puzzle: {e}")
94
+ return None
95
+
96
+
97
+ def setup_instances_directory() -> Path:
98
+ """Create the instances directory if it doesn't exist."""
99
+ instances_dir = Path(__file__).parent / "instances"
100
+ instances_dir.mkdir(exist_ok=True)
101
+ return instances_dir
102
+
103
+
104
+ def get_jsonl_path(instances_dir: Path, difficulty: str) -> Path:
105
+ """Get the JSONL file path for a difficulty level."""
106
+ return instances_dir / f"{difficulty}.jsonl"
107
+
108
+
109
+ def save_puzzle_to_jsonl(puzzle: SokobanPuzzle, jsonl_path: Path):
110
+ """Save a single puzzle to a JSONL file."""
111
+ with open(jsonl_path, "a") as f:
112
+ f.write(json.dumps(asdict(puzzle), default=convert_numpy_types) + "\n")
113
+
114
+
115
+ def convert_numpy_types(obj):
116
+ """Convert numpy types to Python types for JSON serialization."""
117
+ if isinstance(obj, np.integer):
118
+ return int(obj)
119
+ elif isinstance(obj, np.floating):
120
+ return float(obj)
121
+ elif isinstance(obj, np.ndarray):
122
+ return obj.tolist()
123
+ raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
124
+
125
+
126
+ def load_existing_puzzles(jsonl_path: Path) -> Set[str]:
127
+ """Load existing puzzle IDs from a JSONL file."""
128
+ existing_ids = set()
129
+ if jsonl_path.exists():
130
+ with open(jsonl_path, "r") as f:
131
+ for line in f:
132
+ try:
133
+ puzzle_data = json.loads(line.strip())
134
+ existing_ids.add(puzzle_data["id"])
135
+ except json.JSONDecodeError:
136
+ continue
137
+ return existing_ids
138
+
139
+
140
+ def count_existing_puzzles(jsonl_path: Path) -> int:
141
+ """Count existing puzzles in a JSONL file."""
142
+ if not jsonl_path.exists():
143
+ return 0
144
+ with open(jsonl_path, "r") as f:
145
+ return sum(1 for line in f if line.strip())
146
+
147
+
148
+ def generate_puzzle_for_difficulty(
149
+ difficulty: str, config: Dict, seed: int, puzzle_id: str
150
+ ) -> Optional[SokobanPuzzle]:
151
+ """
152
+ Generate a single puzzle for a given difficulty level.
153
+
154
+ Args:
155
+ difficulty: The difficulty level name
156
+ config: Configuration for this difficulty
157
+ seed: Random seed for generation
158
+ puzzle_id: Unique identifier for this puzzle
159
+
160
+ Returns:
161
+ SokobanPuzzle if successfully generated and verified, None otherwise
162
+ """
163
+ max_attempts = 20
164
+
165
+ for attempt in range(max_attempts):
166
+ current_seed = seed + attempt * 1000
167
+
168
+ try:
169
+ # Generate room
170
+ room_structure, room_state, box_mapping, action_sequence = generate_room(
171
+ dim=config["dim_room"],
172
+ initial_seed=current_seed,
173
+ num_boxes=config["num_boxes"],
174
+ search_depth=config["search_depth"],
175
+ num_steps=config["search_depth"] // 2,
176
+ )
177
+
178
+ # Verify solvability
179
+ solution_path = verify_puzzle_solvable(
180
+ room_structure, room_state, max_depth=config["max_steps"]
181
+ )
182
+
183
+ if solution_path is None:
184
+ logger.debug(f"Puzzle {puzzle_id} attempt {attempt + 1} not solvable")
185
+ continue
186
+
187
+ solution_length = len(solution_path)
188
+ target_min, target_max = config["target_solution_length"]
189
+
190
+ # Check if solution length is within desired range
191
+ if not (target_min <= solution_length <= target_max):
192
+ logger.debug(
193
+ f"Puzzle {puzzle_id} attempt {attempt + 1} solution length {solution_length} not in range {target_min}-{target_max}"
194
+ )
195
+ continue
196
+
197
+ # Convert numpy arrays to lists for JSON serialization
198
+ room_fixed_list = room_structure.tolist()
199
+ room_state_list = room_state.tolist()
200
+
201
+ # Convert box mapping to serializable format
202
+ box_mapping_serializable = {}
203
+ for key, value in box_mapping.items():
204
+ if isinstance(key, tuple):
205
+ # Convert numpy integers to regular integers
206
+ key_str = f"{int(key[0])},{int(key[1])}"
207
+ if isinstance(value, tuple):
208
+ box_mapping_serializable[key_str] = [int(value[0]), int(value[1])]
209
+ else:
210
+ box_mapping_serializable[key_str] = value
211
+ else:
212
+ box_mapping_serializable[str(key)] = value
213
+
214
+ puzzle = SokobanPuzzle(
215
+ id=puzzle_id,
216
+ difficulty=difficulty,
217
+ num_boxes=int(config["num_boxes"]),
218
+ dim_room=config["dim_room"],
219
+ room_fixed=room_fixed_list,
220
+ room_state=room_state_list,
221
+ box_mapping=box_mapping_serializable,
222
+ solution_path=[int(action) for action in solution_path], # Convert to regular ints
223
+ solution_length=int(solution_length),
224
+ generation_seed=int(current_seed),
225
+ max_steps=int(config["max_steps"]),
226
+ )
227
+
228
+ logger.info(
229
+ f"Generated {difficulty} puzzle {puzzle_id} (seed: {current_seed}, solution length: {solution_length})"
230
+ )
231
+ return puzzle
232
+
233
+ except Exception as e:
234
+ logger.warning(f"Error generating puzzle {puzzle_id} attempt {attempt + 1}: {e}")
235
+ continue
236
+
237
+ logger.error(f"Failed to generate puzzle {puzzle_id} after {max_attempts} attempts")
238
+ return None
239
+
240
+
241
+ def generate_all_puzzles(num_per_difficulty: int = 100) -> Dict[str, List[SokobanPuzzle]]:
242
+ """
243
+ Generate all puzzles for all difficulty levels with incremental saving.
244
+
245
+ Args:
246
+ num_per_difficulty: Number of puzzles to generate per difficulty level
247
+
248
+ Returns:
249
+ Dictionary mapping difficulty names to lists of puzzles
250
+ """
251
+ all_puzzles = {}
252
+ total_puzzles = 0
253
+
254
+ # Setup instances directory
255
+ instances_dir = setup_instances_directory()
256
+ logger.info(f"Using instances directory: {instances_dir}")
257
+
258
+ for difficulty, config in DIFFICULTY_CONFIGS.items():
259
+ jsonl_path = get_jsonl_path(instances_dir, difficulty)
260
+ existing_ids = load_existing_puzzles(jsonl_path)
261
+ existing_count = count_existing_puzzles(jsonl_path)
262
+
263
+ logger.info(f"Processing {difficulty} difficulty...")
264
+ logger.info(f" Found {existing_count} existing puzzles")
265
+ logger.info(f" Target: {num_per_difficulty} puzzles")
266
+
267
+ puzzles = []
268
+ base_seed = hash(difficulty) % 100000
269
+
270
+ # Generate puzzles until we have enough
271
+ i = 0
272
+ generated_this_run = 0
273
+ while (
274
+ len(puzzles) + existing_count < num_per_difficulty and i < num_per_difficulty * 5
275
+ ): # Safety limit
276
+ puzzle_id = f"{difficulty}_{i:03d}"
277
+
278
+ # Skip if already exists
279
+ if puzzle_id in existing_ids:
280
+ i += 1
281
+ continue
282
+
283
+ puzzle = generate_puzzle_for_difficulty(
284
+ difficulty=difficulty, config=config, seed=base_seed + i, puzzle_id=puzzle_id
285
+ )
286
+
287
+ if puzzle:
288
+ puzzles.append(puzzle)
289
+ # Save immediately to JSONL
290
+ save_puzzle_to_jsonl(puzzle, jsonl_path)
291
+ generated_this_run += 1
292
+ total_puzzles += 1
293
+ logger.info(
294
+ f"Generated and saved {difficulty} puzzle {puzzle_id} ({generated_this_run}/{num_per_difficulty - existing_count} new)"
295
+ )
296
+ else:
297
+ logger.warning(f"Failed to generate puzzle {puzzle_id}")
298
+
299
+ i += 1
300
+
301
+ all_puzzles[difficulty] = puzzles
302
+ logger.info(
303
+ f"Completed {difficulty}: {generated_this_run} new puzzles generated, {existing_count + len(puzzles)} total"
304
+ )
305
+
306
+ logger.info(f"Total new puzzles generated this run: {total_puzzles}")
307
+ return all_puzzles
308
+
309
+
310
+ def load_all_puzzles_from_jsonl(instances_dir: Path) -> Dict[str, List[SokobanPuzzle]]:
311
+ """Load all puzzles from JSONL files."""
312
+ all_puzzles = {}
313
+
314
+ for difficulty in DIFFICULTY_CONFIGS.keys():
315
+ jsonl_path = get_jsonl_path(instances_dir, difficulty)
316
+ puzzles = []
317
+
318
+ if jsonl_path.exists():
319
+ with open(jsonl_path, "r") as f:
320
+ for line in f:
321
+ try:
322
+ puzzle_data = json.loads(line.strip())
323
+ puzzle = SokobanPuzzle(
324
+ id=puzzle_data["id"],
325
+ difficulty=puzzle_data["difficulty"],
326
+ num_boxes=puzzle_data["num_boxes"],
327
+ dim_room=tuple(puzzle_data["dim_room"]),
328
+ room_fixed=puzzle_data["room_fixed"],
329
+ room_state=puzzle_data["room_state"],
330
+ box_mapping=puzzle_data["box_mapping"],
331
+ solution_path=puzzle_data["solution_path"],
332
+ solution_length=puzzle_data["solution_length"],
333
+ generation_seed=puzzle_data["generation_seed"],
334
+ max_steps=puzzle_data["max_steps"],
335
+ )
336
+ puzzles.append(puzzle)
337
+ except (json.JSONDecodeError, KeyError) as e:
338
+ logger.warning(f"Error loading puzzle from {jsonl_path}: {e}")
339
+ continue
340
+
341
+ all_puzzles[difficulty] = puzzles
342
+
343
+ return all_puzzles
344
+
345
+
346
+ def save_puzzles_to_json(puzzles: Dict[str, List[SokobanPuzzle]], output_path: Path):
347
+ """
348
+ Save puzzles to JSON file.
349
+
350
+ Args:
351
+ puzzles: Dictionary of puzzles by difficulty
352
+ output_path: Path to save the JSON file
353
+ """
354
+ # Convert to serializable format
355
+ serializable_puzzles = {}
356
+ for difficulty, puzzle_list in puzzles.items():
357
+ serializable_puzzles[difficulty] = [asdict(puzzle) for puzzle in puzzle_list]
358
+
359
+ # Add metadata
360
+ output_data = {
361
+ "metadata": {
362
+ "version": "1.0",
363
+ "total_puzzles": sum(len(puzzles) for puzzles in serializable_puzzles.values()),
364
+ "difficulties": list(DIFFICULTY_CONFIGS.keys()),
365
+ "generated_at": "2024-01-01T00:00:00Z", # Will be updated when actually generated
366
+ },
367
+ "puzzles": serializable_puzzles,
368
+ }
369
+
370
+ with open(output_path, "w") as f:
371
+ json.dump(output_data, f, indent=2, default=convert_numpy_types)
372
+
373
+ logger.info(f"Saved puzzles to {output_path}")
374
+
375
+
376
+ def create_unified_json_from_jsonl():
377
+ """Create a unified JSON file from all JSONL files for the puzzle loader."""
378
+ instances_dir = setup_instances_directory()
379
+ all_puzzles = load_all_puzzles_from_jsonl(instances_dir)
380
+
381
+ # Save to JSON
382
+ output_path = Path(__file__).parent / "verified_puzzles.json"
383
+ save_puzzles_to_json(all_puzzles, output_path)
384
+
385
+ return all_puzzles
386
+
387
+
388
+ def main():
389
+ """Main function to generate and save all puzzles."""
390
+ logger.info("Starting Sokoban puzzle generation with incremental saving...")
391
+
392
+ # Generate puzzles (saves incrementally to JSONL)
393
+ puzzles = generate_all_puzzles(num_per_difficulty=100)
394
+
395
+ # Print summary of this run
396
+ logger.info("Puzzle generation complete!")
397
+ logger.info("Summary of this run:")
398
+ for difficulty, puzzle_list in puzzles.items():
399
+ if puzzle_list:
400
+ avg_solution_length = sum(p.solution_length for p in puzzle_list) / len(puzzle_list)
401
+ logger.info(
402
+ f" {difficulty}: {len(puzzle_list)} new puzzles, avg solution length: {avg_solution_length:.1f}"
403
+ )
404
+ else:
405
+ logger.info(f" {difficulty}: 0 new puzzles")
406
+
407
+ # Show total counts from JSONL files
408
+ instances_dir = setup_instances_directory()
409
+ logger.info("Total puzzles saved:")
410
+ for difficulty in DIFFICULTY_CONFIGS.keys():
411
+ jsonl_path = get_jsonl_path(instances_dir, difficulty)
412
+ total_count = count_existing_puzzles(jsonl_path)
413
+ logger.info(f" {difficulty}: {total_count} total puzzles")
414
+
415
+ # Create unified JSON file for the puzzle loader
416
+ logger.info("Creating unified JSON file for puzzle loader...")
417
+ create_unified_json_from_jsonl()
418
+ logger.info("Unified JSON file created successfully!")
419
+
420
+
421
+ if __name__ == "__main__":
422
+ import sys
423
+
424
+ if len(sys.argv) > 1 and sys.argv[1] == "--create-json":
425
+ # Just create the unified JSON from existing JSONL files
426
+ logger.info("Creating unified JSON file from existing JSONL files...")
427
+ puzzles = create_unified_json_from_jsonl()
428
+ logger.info("Summary of loaded puzzles:")
429
+ for difficulty, puzzle_list in puzzles.items():
430
+ if puzzle_list:
431
+ avg_solution_length = sum(p.solution_length for p in puzzle_list) / len(puzzle_list)
432
+ logger.info(
433
+ f" {difficulty}: {len(puzzle_list)} puzzles, avg solution length: {avg_solution_length:.1f}"
434
+ )
435
+ else:
436
+ logger.info(f" {difficulty}: 0 puzzles")
437
+ else:
438
+ main()