synth-ai 0.2.4.dev5__py3-none-any.whl → 0.2.4.dev7__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 (229) hide show
  1. synth_ai/__init__.py +18 -9
  2. synth_ai/cli/__init__.py +10 -5
  3. synth_ai/cli/balance.py +22 -17
  4. synth_ai/cli/calc.py +2 -3
  5. synth_ai/cli/demo.py +3 -5
  6. synth_ai/cli/legacy_root_backup.py +58 -32
  7. synth_ai/cli/man.py +22 -19
  8. synth_ai/cli/recent.py +9 -8
  9. synth_ai/cli/root.py +58 -13
  10. synth_ai/cli/status.py +13 -6
  11. synth_ai/cli/traces.py +45 -21
  12. synth_ai/cli/watch.py +40 -37
  13. synth_ai/config/base_url.py +1 -3
  14. synth_ai/core/experiment.py +1 -2
  15. synth_ai/environments/__init__.py +2 -6
  16. synth_ai/environments/environment/artifacts/base.py +3 -1
  17. synth_ai/environments/environment/db/sqlite.py +1 -1
  18. synth_ai/environments/environment/registry.py +19 -20
  19. synth_ai/environments/environment/resources/sqlite.py +2 -3
  20. synth_ai/environments/environment/rewards/core.py +3 -2
  21. synth_ai/environments/environment/tools/__init__.py +6 -4
  22. synth_ai/environments/examples/crafter_classic/__init__.py +1 -1
  23. synth_ai/environments/examples/crafter_classic/engine.py +21 -17
  24. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +1 -0
  25. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +2 -1
  26. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +2 -1
  27. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +3 -2
  28. synth_ai/environments/examples/crafter_classic/environment.py +16 -15
  29. synth_ai/environments/examples/crafter_classic/taskset.py +2 -2
  30. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +2 -3
  31. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +2 -1
  32. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +2 -2
  33. synth_ai/environments/examples/crafter_custom/crafter/config.py +2 -2
  34. synth_ai/environments/examples/crafter_custom/crafter/env.py +1 -5
  35. synth_ai/environments/examples/crafter_custom/crafter/objects.py +1 -2
  36. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +1 -2
  37. synth_ai/environments/examples/crafter_custom/dataset_builder.py +5 -5
  38. synth_ai/environments/examples/crafter_custom/environment.py +13 -13
  39. synth_ai/environments/examples/crafter_custom/run_dataset.py +5 -5
  40. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +2 -2
  41. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +5 -4
  42. synth_ai/environments/examples/enron/art_helpers/types_enron.py +2 -1
  43. synth_ai/environments/examples/enron/engine.py +18 -14
  44. synth_ai/environments/examples/enron/environment.py +12 -11
  45. synth_ai/environments/examples/enron/taskset.py +7 -7
  46. synth_ai/environments/examples/minigrid/__init__.py +6 -6
  47. synth_ai/environments/examples/minigrid/engine.py +6 -6
  48. synth_ai/environments/examples/minigrid/environment.py +6 -6
  49. synth_ai/environments/examples/minigrid/puzzle_loader.py +3 -2
  50. synth_ai/environments/examples/minigrid/taskset.py +13 -13
  51. synth_ai/environments/examples/nethack/achievements.py +1 -1
  52. synth_ai/environments/examples/nethack/engine.py +8 -7
  53. synth_ai/environments/examples/nethack/environment.py +10 -9
  54. synth_ai/environments/examples/nethack/helpers/__init__.py +8 -9
  55. synth_ai/environments/examples/nethack/helpers/action_mapping.py +1 -1
  56. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +2 -1
  57. synth_ai/environments/examples/nethack/helpers/observation_utils.py +1 -1
  58. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +3 -4
  59. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +6 -5
  60. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +5 -5
  61. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +7 -6
  62. synth_ai/environments/examples/nethack/taskset.py +5 -5
  63. synth_ai/environments/examples/red/engine.py +9 -8
  64. synth_ai/environments/examples/red/engine_helpers/reward_components.py +2 -1
  65. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +7 -7
  66. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +2 -1
  67. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +2 -1
  68. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +2 -1
  69. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +2 -1
  70. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +2 -1
  71. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +2 -1
  72. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +2 -1
  73. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +2 -1
  74. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +2 -1
  75. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +2 -1
  76. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +2 -1
  77. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +3 -2
  78. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +2 -1
  79. synth_ai/environments/examples/red/environment.py +18 -15
  80. synth_ai/environments/examples/red/taskset.py +5 -3
  81. synth_ai/environments/examples/sokoban/engine.py +16 -13
  82. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +3 -2
  83. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +2 -1
  84. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +1 -1
  85. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +7 -5
  86. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +1 -1
  87. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +2 -1
  88. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +5 -4
  89. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +3 -2
  90. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +2 -1
  91. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +5 -4
  92. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +1 -1
  93. synth_ai/environments/examples/sokoban/environment.py +15 -14
  94. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +5 -3
  95. synth_ai/environments/examples/sokoban/puzzle_loader.py +3 -2
  96. synth_ai/environments/examples/sokoban/taskset.py +13 -10
  97. synth_ai/environments/examples/tictactoe/engine.py +6 -6
  98. synth_ai/environments/examples/tictactoe/environment.py +8 -7
  99. synth_ai/environments/examples/tictactoe/taskset.py +6 -5
  100. synth_ai/environments/examples/verilog/engine.py +4 -3
  101. synth_ai/environments/examples/verilog/environment.py +11 -10
  102. synth_ai/environments/examples/verilog/taskset.py +14 -12
  103. synth_ai/environments/examples/wordle/__init__.py +29 -0
  104. synth_ai/environments/examples/wordle/engine.py +398 -0
  105. synth_ai/environments/examples/wordle/environment.py +159 -0
  106. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +75 -0
  107. synth_ai/environments/examples/wordle/taskset.py +230 -0
  108. synth_ai/environments/reproducibility/core.py +1 -1
  109. synth_ai/environments/reproducibility/tree.py +21 -21
  110. synth_ai/environments/service/app.py +11 -2
  111. synth_ai/environments/service/core_routes.py +137 -105
  112. synth_ai/environments/service/external_registry.py +1 -2
  113. synth_ai/environments/service/registry.py +1 -1
  114. synth_ai/environments/stateful/core.py +1 -2
  115. synth_ai/environments/stateful/engine.py +1 -1
  116. synth_ai/environments/tasks/api.py +4 -4
  117. synth_ai/environments/tasks/core.py +14 -12
  118. synth_ai/environments/tasks/filters.py +6 -4
  119. synth_ai/environments/tasks/utils.py +13 -11
  120. synth_ai/evals/base.py +2 -3
  121. synth_ai/experimental/synth_oss.py +4 -4
  122. synth_ai/learning/gateway.py +1 -3
  123. synth_ai/learning/prompts/banking77_injection_eval.py +168 -0
  124. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +213 -0
  125. synth_ai/learning/prompts/mipro.py +282 -1
  126. synth_ai/learning/prompts/random_search.py +246 -0
  127. synth_ai/learning/prompts/run_mipro_banking77.py +172 -0
  128. synth_ai/learning/prompts/run_random_search_banking77.py +324 -0
  129. synth_ai/lm/__init__.py +5 -5
  130. synth_ai/lm/caching/ephemeral.py +9 -9
  131. synth_ai/lm/caching/handler.py +20 -20
  132. synth_ai/lm/caching/persistent.py +10 -10
  133. synth_ai/lm/config.py +3 -3
  134. synth_ai/lm/constants.py +7 -7
  135. synth_ai/lm/core/all.py +17 -3
  136. synth_ai/lm/core/exceptions.py +0 -2
  137. synth_ai/lm/core/main.py +26 -41
  138. synth_ai/lm/core/main_v3.py +20 -10
  139. synth_ai/lm/core/vendor_clients.py +18 -17
  140. synth_ai/lm/injection.py +80 -0
  141. synth_ai/lm/overrides.py +206 -0
  142. synth_ai/lm/provider_support/__init__.py +1 -1
  143. synth_ai/lm/provider_support/anthropic.py +51 -24
  144. synth_ai/lm/provider_support/openai.py +51 -22
  145. synth_ai/lm/structured_outputs/handler.py +34 -32
  146. synth_ai/lm/structured_outputs/inject.py +24 -27
  147. synth_ai/lm/structured_outputs/rehabilitate.py +19 -15
  148. synth_ai/lm/tools/base.py +17 -16
  149. synth_ai/lm/unified_interface.py +17 -18
  150. synth_ai/lm/vendors/base.py +20 -18
  151. synth_ai/lm/vendors/core/anthropic_api.py +50 -25
  152. synth_ai/lm/vendors/core/gemini_api.py +31 -36
  153. synth_ai/lm/vendors/core/mistral_api.py +19 -19
  154. synth_ai/lm/vendors/core/openai_api.py +11 -10
  155. synth_ai/lm/vendors/openai_standard.py +144 -88
  156. synth_ai/lm/vendors/openai_standard_responses.py +74 -61
  157. synth_ai/lm/vendors/retries.py +9 -1
  158. synth_ai/lm/vendors/supported/custom_endpoint.py +26 -26
  159. synth_ai/lm/vendors/supported/deepseek.py +10 -10
  160. synth_ai/lm/vendors/supported/grok.py +8 -8
  161. synth_ai/lm/vendors/supported/ollama.py +2 -1
  162. synth_ai/lm/vendors/supported/openrouter.py +11 -9
  163. synth_ai/lm/vendors/synth_client.py +69 -63
  164. synth_ai/lm/warmup.py +8 -7
  165. synth_ai/tracing/__init__.py +22 -10
  166. synth_ai/tracing_v1/__init__.py +22 -20
  167. synth_ai/tracing_v3/__init__.py +7 -7
  168. synth_ai/tracing_v3/abstractions.py +56 -52
  169. synth_ai/tracing_v3/config.py +4 -2
  170. synth_ai/tracing_v3/db_config.py +6 -8
  171. synth_ai/tracing_v3/decorators.py +29 -30
  172. synth_ai/tracing_v3/examples/basic_usage.py +12 -12
  173. synth_ai/tracing_v3/hooks.py +21 -21
  174. synth_ai/tracing_v3/llm_call_record_helpers.py +85 -98
  175. synth_ai/tracing_v3/lm_call_record_abstractions.py +2 -4
  176. synth_ai/tracing_v3/migration_helper.py +3 -5
  177. synth_ai/tracing_v3/replica_sync.py +30 -32
  178. synth_ai/tracing_v3/session_tracer.py +35 -29
  179. synth_ai/tracing_v3/storage/__init__.py +1 -1
  180. synth_ai/tracing_v3/storage/base.py +8 -7
  181. synth_ai/tracing_v3/storage/config.py +4 -4
  182. synth_ai/tracing_v3/storage/factory.py +4 -4
  183. synth_ai/tracing_v3/storage/utils.py +9 -9
  184. synth_ai/tracing_v3/turso/__init__.py +3 -3
  185. synth_ai/tracing_v3/turso/daemon.py +9 -9
  186. synth_ai/tracing_v3/turso/manager.py +60 -48
  187. synth_ai/tracing_v3/turso/models.py +24 -19
  188. synth_ai/tracing_v3/utils.py +5 -5
  189. synth_ai/tui/__main__.py +1 -1
  190. synth_ai/tui/cli/query_experiments.py +2 -3
  191. synth_ai/tui/cli/query_experiments_v3.py +2 -3
  192. synth_ai/tui/dashboard.py +97 -86
  193. synth_ai/v0/tracing/abstractions.py +28 -28
  194. synth_ai/v0/tracing/base_client.py +9 -9
  195. synth_ai/v0/tracing/client_manager.py +7 -7
  196. synth_ai/v0/tracing/config.py +7 -7
  197. synth_ai/v0/tracing/context.py +6 -6
  198. synth_ai/v0/tracing/decorators.py +6 -5
  199. synth_ai/v0/tracing/events/manage.py +1 -1
  200. synth_ai/v0/tracing/events/store.py +5 -4
  201. synth_ai/v0/tracing/immediate_client.py +4 -5
  202. synth_ai/v0/tracing/local.py +3 -3
  203. synth_ai/v0/tracing/log_client_base.py +4 -5
  204. synth_ai/v0/tracing/retry_queue.py +5 -6
  205. synth_ai/v0/tracing/trackers.py +25 -25
  206. synth_ai/v0/tracing/upload.py +6 -0
  207. synth_ai/v0/tracing_v1/__init__.py +1 -1
  208. synth_ai/v0/tracing_v1/abstractions.py +28 -28
  209. synth_ai/v0/tracing_v1/base_client.py +9 -9
  210. synth_ai/v0/tracing_v1/client_manager.py +7 -7
  211. synth_ai/v0/tracing_v1/config.py +7 -7
  212. synth_ai/v0/tracing_v1/context.py +6 -6
  213. synth_ai/v0/tracing_v1/decorators.py +7 -6
  214. synth_ai/v0/tracing_v1/events/manage.py +1 -1
  215. synth_ai/v0/tracing_v1/events/store.py +5 -4
  216. synth_ai/v0/tracing_v1/immediate_client.py +4 -5
  217. synth_ai/v0/tracing_v1/local.py +3 -3
  218. synth_ai/v0/tracing_v1/log_client_base.py +4 -5
  219. synth_ai/v0/tracing_v1/retry_queue.py +5 -6
  220. synth_ai/v0/tracing_v1/trackers.py +25 -25
  221. synth_ai/v0/tracing_v1/upload.py +25 -24
  222. synth_ai/zyk/__init__.py +1 -0
  223. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/METADATA +2 -11
  224. synth_ai-0.2.4.dev7.dist-info/RECORD +299 -0
  225. synth_ai-0.2.4.dev5.dist-info/RECORD +0 -287
  226. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/WHEEL +0 -0
  227. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/entry_points.txt +0 -0
  228. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/licenses/LICENSE +0 -0
  229. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,398 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ import string
5
+ from collections import Counter
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from synth_ai.environments.environment.rewards.core import RewardComponent, RewardStack
10
+ from synth_ai.environments.environment.shared_engine import (
11
+ GetObservationCallable,
12
+ InternalObservation,
13
+ )
14
+ from synth_ai.environments.reproducibility.core import IReproducibleEngine
15
+ from synth_ai.environments.stateful.engine import StatefulEngine, StatefulEngineSnapshot
16
+ from synth_ai.environments.tasks.core import TaskInstance
17
+
18
+ DEFAULT_SOLUTIONS = [
19
+ "cigar",
20
+ "rebut",
21
+ "sissy",
22
+ "humph",
23
+ "awake",
24
+ "blush",
25
+ "focal",
26
+ "evade",
27
+ "naval",
28
+ "serve",
29
+ "heath",
30
+ "dwarf",
31
+ "model",
32
+ "karma",
33
+ "stink",
34
+ "grade",
35
+ "quiet",
36
+ "bench",
37
+ "abate",
38
+ "feign",
39
+ "major",
40
+ "death",
41
+ "fresh",
42
+ "crust",
43
+ "stool",
44
+ "colon",
45
+ "abase",
46
+ "marry",
47
+ "react",
48
+ "batty",
49
+ "pride",
50
+ "floss",
51
+ "helix",
52
+ "croak",
53
+ "staff",
54
+ "paper",
55
+ "unfed",
56
+ "whelp",
57
+ "trawl",
58
+ "outdo",
59
+ "adobe",
60
+ "crazy",
61
+ "sower",
62
+ "repay",
63
+ "digit",
64
+ "crate",
65
+ "cluck",
66
+ "spike",
67
+ "mimic",
68
+ "pound",
69
+ ]
70
+
71
+
72
+ def _sanitize(word: str) -> str:
73
+ w = word.strip().lower()
74
+ if not w or not all(c in string.ascii_lowercase for c in w):
75
+ raise ValueError("word must contain only a–z letters")
76
+ return w
77
+
78
+
79
+ def _score_guess(guess: str, target: str) -> str:
80
+ res = ["B"] * len(target)
81
+ counts = Counter(target)
82
+ for i, ch in enumerate(guess):
83
+ if ch == target[i]:
84
+ res[i] = "G"
85
+ counts[ch] -= 1
86
+ for i, ch in enumerate(guess):
87
+ if res[i] == "G":
88
+ continue
89
+ if counts.get(ch, 0) > 0:
90
+ res[i] = "Y"
91
+ counts[ch] -= 1
92
+ return "".join(res)
93
+
94
+
95
+ @dataclass
96
+ class WordlePublicState:
97
+ word_length: int
98
+ remaining_guesses: int
99
+ max_guesses: int
100
+ guesses: list[str]
101
+ feedback: list[str] # Parallel to guesses; strings of 'G/Y/B'
102
+ last_feedback: str | None
103
+ last_guess: str | None
104
+ terminated: bool
105
+ status: str # "in_progress" | "won" | "lost"
106
+
107
+ @property
108
+ def board_text(self) -> str:
109
+ if not self.guesses:
110
+ return "(no guesses yet)"
111
+ lines = []
112
+ for g, fb in zip(self.guesses, self.feedback, strict=False):
113
+ spaced = " ".join(list(fb))
114
+ lines.append(f"{g.upper()} | {spaced}")
115
+ return "\n".join(lines)
116
+
117
+
118
+ @dataclass
119
+ class WordlePrivateState:
120
+ reward_last: float
121
+ total_reward: float
122
+ terminated: bool
123
+ truncated: bool
124
+
125
+
126
+ @dataclass
127
+ class WordleEngineSnapshot(StatefulEngineSnapshot):
128
+ task_instance_dict: dict
129
+ engine_snapshot: dict
130
+
131
+
132
+ class WordleWinComponent(RewardComponent):
133
+ async def score(self, state: WordlePublicState, action: Any) -> float:
134
+ return 1.0 if state.status == "won" else 0.0
135
+
136
+
137
+ class WordleInvalidGuessComponent(RewardComponent):
138
+ def __init__(self) -> None:
139
+ self.invalid_attempted = False
140
+
141
+ async def score(self, state: WordlePublicState, action: Any) -> float:
142
+ if self.invalid_attempted:
143
+ self.invalid_attempted = False
144
+ return -1.0
145
+ return 0.0
146
+
147
+
148
+ class WordleEngine(StatefulEngine, IReproducibleEngine):
149
+ def __init__(self, task_instance: TaskInstance):
150
+ self.task_instance = task_instance
151
+
152
+ # Read config from metadata
153
+ md = getattr(task_instance, "metadata", None)
154
+ self.word_length: int = getattr(md, "word_length", 5) if md else 5
155
+ self.max_guesses: int = getattr(md, "max_guesses", 6) if md else 6
156
+ self.enforce_wordlist: bool = getattr(md, "enforce_wordlist", False) if md else False
157
+ # Toggle: whether invalid actions consume a turn (default True)
158
+ self.consume_invalid_attempts: bool = (
159
+ getattr(md, "consume_invalid_attempts", True) if md else True
160
+ )
161
+
162
+ self.base_word_list: list[str] = [
163
+ w for w in DEFAULT_SOLUTIONS if len(w) == self.word_length
164
+ ] or [w for w in DEFAULT_SOLUTIONS if len(w) == 5]
165
+
166
+ # Target selection: prefer explicit target_word in metadata; else pick deterministically by seed
167
+ self.fixed_target: str | None = (
168
+ _sanitize(getattr(md, "target_word", ""))
169
+ if md and getattr(md, "target_word", None)
170
+ else None
171
+ )
172
+ self.seed: int | None = getattr(md, "seed", None) if md else None
173
+
174
+ # Runtime state
175
+ self.target: str | None = None
176
+ self.guesses: list[str] = []
177
+ self.feedback: list[str] = []
178
+ self.remaining_guesses: int = self.max_guesses
179
+ self.status: str = "in_progress"
180
+ self.terminated: bool = False
181
+ self.total_reward: float = 0.0
182
+
183
+ # Rewards
184
+ self.invalid_component = WordleInvalidGuessComponent()
185
+ self.reward_stack = RewardStack([WordleWinComponent(), self.invalid_component])
186
+
187
+ async def _reset_engine(
188
+ self, *, seed: int | None = None
189
+ ) -> tuple[WordlePrivateState, WordlePublicState]:
190
+ if seed is None:
191
+ seed = self.seed
192
+ if seed is not None and self.fixed_target is None:
193
+ random.seed(seed)
194
+ self.target = self.fixed_target or random.choice(self.base_word_list)
195
+ self.guesses = []
196
+ self.feedback = []
197
+ self.remaining_guesses = self.max_guesses
198
+ self.status = "in_progress"
199
+ self.terminated = False
200
+ self.total_reward = 0.0
201
+
202
+ pub = WordlePublicState(
203
+ word_length=self.word_length,
204
+ remaining_guesses=self.remaining_guesses,
205
+ max_guesses=self.max_guesses,
206
+ guesses=[],
207
+ feedback=[],
208
+ last_feedback=None,
209
+ last_guess=None,
210
+ terminated=False,
211
+ status=self.status,
212
+ )
213
+ priv = WordlePrivateState(
214
+ reward_last=0.0,
215
+ total_reward=0.0,
216
+ terminated=False,
217
+ truncated=False,
218
+ )
219
+ return priv, pub
220
+
221
+ async def _step_engine(self, action: str) -> tuple[WordlePrivateState, WordlePublicState]:
222
+ assert self.target is not None
223
+ guess = _sanitize(action)
224
+
225
+ # Validate
226
+ if len(guess) != self.word_length or (
227
+ self.enforce_wordlist and guess not in self.base_word_list
228
+ ):
229
+ # Penalize invalid action; do not consume a guess
230
+ self.invalid_component.invalid_attempted = True
231
+ if self.consume_invalid_attempts:
232
+ # consume a turn on invalid guesses
233
+ if self.remaining_guesses > 0:
234
+ self.remaining_guesses -= 1
235
+ if self.remaining_guesses == 0:
236
+ self.status = "lost"
237
+ self.terminated = True
238
+ pub = WordlePublicState(
239
+ word_length=self.word_length,
240
+ remaining_guesses=self.remaining_guesses,
241
+ max_guesses=self.max_guesses,
242
+ guesses=self.guesses.copy(),
243
+ feedback=self.feedback.copy(),
244
+ last_feedback=self.feedback[-1] if self.feedback else None,
245
+ last_guess=self.guesses[-1] if self.guesses else None,
246
+ terminated=self.terminated,
247
+ status=self.status,
248
+ )
249
+ reward = await self.reward_stack.step_reward(pub, action)
250
+ self.total_reward += reward
251
+ priv = WordlePrivateState(
252
+ reward_last=reward,
253
+ total_reward=self.total_reward,
254
+ terminated=self.terminated,
255
+ truncated=False,
256
+ )
257
+ return priv, pub
258
+
259
+ fb = _score_guess(guess, self.target)
260
+ self.guesses.append(guess)
261
+ self.feedback.append(fb)
262
+ self.remaining_guesses -= 1
263
+
264
+ if guess == self.target:
265
+ self.status = "won"
266
+ self.terminated = True
267
+ elif self.remaining_guesses == 0:
268
+ self.status = "lost"
269
+ self.terminated = True
270
+ else:
271
+ self.status = "in_progress"
272
+
273
+ pub = WordlePublicState(
274
+ word_length=self.word_length,
275
+ remaining_guesses=self.remaining_guesses,
276
+ max_guesses=self.max_guesses,
277
+ guesses=self.guesses.copy(),
278
+ feedback=self.feedback.copy(),
279
+ last_feedback=fb,
280
+ last_guess=guess,
281
+ terminated=self.terminated,
282
+ status=self.status,
283
+ )
284
+
285
+ reward = await self.reward_stack.step_reward(pub, action)
286
+ self.total_reward += reward
287
+ priv = WordlePrivateState(
288
+ reward_last=reward,
289
+ total_reward=self.total_reward,
290
+ terminated=self.terminated,
291
+ truncated=False,
292
+ )
293
+ return priv, pub
294
+
295
+ async def _serialize_engine(self) -> WordleEngineSnapshot:
296
+ return WordleEngineSnapshot(
297
+ task_instance_dict=await self.task_instance.serialize(),
298
+ engine_snapshot={
299
+ "word_length": self.word_length,
300
+ "max_guesses": self.max_guesses,
301
+ "enforce_wordlist": self.enforce_wordlist,
302
+ "consume_invalid_attempts": self.consume_invalid_attempts,
303
+ "base_word_list": self.base_word_list,
304
+ "fixed_target": self.fixed_target,
305
+ "seed": self.seed,
306
+ "target": self.target,
307
+ "guesses": self.guesses,
308
+ "feedback": self.feedback,
309
+ "remaining_guesses": self.remaining_guesses,
310
+ "status": self.status,
311
+ "terminated": self.terminated,
312
+ "total_reward": self.total_reward,
313
+ },
314
+ )
315
+
316
+ @classmethod
317
+ async def _deserialize_engine(cls, snapshot: WordleEngineSnapshot) -> WordleEngine:
318
+ task_instance = await TaskInstance.deserialize(snapshot.task_instance_dict)
319
+ engine = cls(task_instance)
320
+ s = snapshot.engine_snapshot
321
+ engine.word_length = s["word_length"]
322
+ engine.max_guesses = s["max_guesses"]
323
+ engine.enforce_wordlist = s["enforce_wordlist"]
324
+ engine.consume_invalid_attempts = s.get("consume_invalid_attempts", True)
325
+ engine.base_word_list = s.get("base_word_list", engine.base_word_list)
326
+ engine.fixed_target = s.get("fixed_target")
327
+ engine.seed = s.get("seed")
328
+ engine.target = s.get("target")
329
+ engine.guesses = s.get("guesses", [])
330
+ engine.feedback = s.get("feedback", [])
331
+ engine.remaining_guesses = s.get("remaining_guesses", engine.max_guesses)
332
+ engine.status = s.get("status", "in_progress")
333
+ engine.terminated = s.get("terminated", False)
334
+ engine.total_reward = s.get("total_reward", 0.0)
335
+ return engine
336
+
337
+ def get_current_states_for_observation(self) -> tuple[WordlePrivateState, WordlePublicState]:
338
+ pub = WordlePublicState(
339
+ word_length=self.word_length,
340
+ remaining_guesses=self.remaining_guesses,
341
+ max_guesses=self.max_guesses,
342
+ guesses=self.guesses.copy(),
343
+ feedback=self.feedback.copy(),
344
+ last_feedback=self.feedback[-1] if self.feedback else None,
345
+ last_guess=self.guesses[-1] if self.guesses else None,
346
+ terminated=self.terminated,
347
+ status=self.status,
348
+ )
349
+ priv = WordlePrivateState(
350
+ reward_last=0.0,
351
+ total_reward=self.total_reward,
352
+ terminated=self.terminated,
353
+ truncated=False,
354
+ )
355
+ return priv, pub
356
+
357
+
358
+ class SynthWordleObservationCallable(GetObservationCallable):
359
+ async def get_observation(
360
+ self, pub: WordlePublicState, priv: WordlePrivateState
361
+ ) -> InternalObservation:
362
+ header = f"WORDLE ({pub.word_length} letters, {pub.max_guesses} max guesses)"
363
+ lines = [
364
+ header,
365
+ "Submit a single English word (letters only).",
366
+ "",
367
+ pub.board_text,
368
+ "",
369
+ ]
370
+ if pub.status == "in_progress":
371
+ lines.append(f"You have {pub.remaining_guesses} guesses left.")
372
+ elif pub.status == "won":
373
+ lines.append("You guessed the word! ✅")
374
+ else:
375
+ lines.append("Out of guesses. ❌")
376
+
377
+ return {
378
+ "text": "\n".join(lines),
379
+ "status": pub.status,
380
+ "remaining_guesses": pub.remaining_guesses,
381
+ "guesses": pub.guesses,
382
+ "feedback": pub.feedback,
383
+ "reward_last": priv.reward_last,
384
+ "total_reward": priv.total_reward,
385
+ "terminated": pub.terminated,
386
+ }
387
+
388
+
389
+ class SynthWordleCheckpointObservationCallable(GetObservationCallable):
390
+ async def get_observation(
391
+ self, pub: WordlePublicState, priv: WordlePrivateState
392
+ ) -> InternalObservation:
393
+ return {
394
+ "board_text_final": pub.board_text,
395
+ "status_final": pub.status,
396
+ "total_reward": priv.total_reward,
397
+ "terminated": pub.terminated,
398
+ }
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from synth_ai.environments.environment.shared_engine import (
8
+ GetObservationCallable,
9
+ InternalObservation,
10
+ )
11
+ from synth_ai.environments.environment.tools import (
12
+ AbstractTool,
13
+ EnvToolCall,
14
+ ToolResult,
15
+ )
16
+ from synth_ai.environments.reproducibility.core import ReproducibleEnvironment
17
+ from synth_ai.environments.stateful.core import StatefulEnvironment
18
+ from synth_ai.environments.tasks.core import TaskInstance
19
+
20
+ from .engine import (
21
+ SynthWordleCheckpointObservationCallable,
22
+ SynthWordleObservationCallable,
23
+ WordleEngine,
24
+ WordleEngineSnapshot,
25
+ WordlePrivateState,
26
+ WordlePublicState,
27
+ )
28
+
29
+
30
+ class WordleActionInput(BaseModel):
31
+ guess: str = Field(..., description="Your word guess (letters only)")
32
+
33
+
34
+ class WordleInteractTool(AbstractTool):
35
+ name = "interact"
36
+ description = "Submit a word guess to the Wordle environment."
37
+ call_schema = WordleActionInput
38
+ result_schema = ToolResult
39
+
40
+ def __init__(self, engine: WordleEngine):
41
+ self.engine = engine
42
+
43
+ async def __call__(self, call: EnvToolCall) -> ToolResult:
44
+ try:
45
+ validated = self.call_schema(**call.args)
46
+ priv, pub = await self.engine._step_engine(validated.guess)
47
+ return ToolResult(ok=True, payload={"public_state": pub, "private_state": priv})
48
+ except Exception as e:
49
+ # Return current state with error message
50
+ priv, pub = self.engine.get_current_states_for_observation()
51
+ return ToolResult(
52
+ ok=False, error=str(e), payload={"public_state": pub, "private_state": priv}
53
+ )
54
+
55
+
56
+ class WordleEnvironment(StatefulEnvironment, ReproducibleEnvironment[WordleEngine]):
57
+ def __init__(
58
+ self,
59
+ task_instance: TaskInstance,
60
+ custom_step_obs: GetObservationCallable | None = None,
61
+ custom_ckpt_obs: GetObservationCallable | None = None,
62
+ ) -> None:
63
+ self.name = "Wordle"
64
+ self.task_instance = task_instance
65
+ self.custom_step_observation_callable = custom_step_obs or SynthWordleObservationCallable()
66
+ self.custom_checkpoint_observation_callable = (
67
+ custom_ckpt_obs or SynthWordleCheckpointObservationCallable()
68
+ )
69
+ self.engine = WordleEngine(task_instance)
70
+ self._interact_tool = WordleInteractTool(self.engine)
71
+
72
+ async def initialize(self) -> InternalObservation:
73
+ priv, pub = await self.engine._reset_engine()
74
+ return await self._to_observation(priv, pub, self.custom_step_observation_callable)
75
+
76
+ async def step(self, tool_calls) -> InternalObservation:
77
+ validated_call = self.validate_tool_calls(tool_calls)
78
+ result = await self._interact_tool(validated_call)
79
+ if result.ok:
80
+ priv = result.payload["private_state"]
81
+ pub = result.payload["public_state"]
82
+ return await self._to_observation(priv, pub, self.custom_step_observation_callable)
83
+ else:
84
+ priv, pub = self.engine.get_current_states_for_observation()
85
+ return await self._to_observation(
86
+ priv, pub, self.custom_step_observation_callable, extra_obs={"error": result.error}
87
+ )
88
+
89
+ async def checkpoint(self) -> InternalObservation:
90
+ priv, pub = self.engine.get_current_states_for_observation()
91
+ return await self._to_observation(priv, pub, self.custom_checkpoint_observation_callable)
92
+
93
+ async def terminate(self) -> InternalObservation:
94
+ priv, pub = self.engine.get_current_states_for_observation()
95
+ pub.terminated = True
96
+ priv.terminated = True
97
+ return await self._to_observation(priv, pub, self.custom_checkpoint_observation_callable)
98
+
99
+ def validate_tool_calls(self, tool_calls) -> EnvToolCall:
100
+ # Accept EnvToolCall, dict-like, or list formats similar to other envs
101
+ if isinstance(tool_calls, EnvToolCall):
102
+ validated = tool_calls
103
+ elif isinstance(tool_calls, dict):
104
+ if "tool" in tool_calls:
105
+ validated = EnvToolCall(tool=tool_calls["tool"], args=tool_calls.get("args", {}))
106
+ elif "name" in tool_calls:
107
+ validated = EnvToolCall(
108
+ tool=tool_calls["name"], args=tool_calls.get("parameters", {})
109
+ )
110
+ elif "function" in tool_calls:
111
+ validated = EnvToolCall(
112
+ tool=tool_calls["function"]["name"],
113
+ args=tool_calls["function"].get("arguments", {}),
114
+ )
115
+ else:
116
+ # Treat remaining keys as args; default tool name
117
+ validated = EnvToolCall(tool="interact", args=tool_calls)
118
+ elif isinstance(tool_calls, list):
119
+ if len(tool_calls) == 0:
120
+ raise ValueError("Empty tool calls list")
121
+ validated = self.validate_tool_calls(tool_calls[0])
122
+ else:
123
+ # Assume it's a raw guess string
124
+ validated = EnvToolCall(tool="interact", args={"guess": str(tool_calls)})
125
+
126
+ if validated.tool != "interact":
127
+ raise ValueError(f"Unknown tool: {validated.tool}")
128
+ # Normalize: allow 'action' key synonymous with 'guess'
129
+ args = validated.args
130
+ if "action" in args and "guess" not in args:
131
+ args = {"guess": args["action"]}
132
+ return EnvToolCall(tool="interact", args=args)
133
+
134
+ async def _to_observation(
135
+ self,
136
+ priv: WordlePrivateState,
137
+ pub: WordlePublicState,
138
+ obs_cb: GetObservationCallable | None,
139
+ extra_obs: dict[str, Any] | None = None,
140
+ ) -> InternalObservation:
141
+ if obs_cb:
142
+ obs = await obs_cb.get_observation(pub, priv)
143
+ else:
144
+ obs: InternalObservation = {}
145
+ if extra_obs and isinstance(obs, dict):
146
+ obs.update(extra_obs)
147
+ return obs
148
+
149
+ async def _serialize_engine(self) -> WordleEngineSnapshot:
150
+ return await self.engine._serialize_engine()
151
+
152
+ @classmethod
153
+ async def _deserialize_engine(
154
+ cls, snapshot: WordleEngineSnapshot, task_instance: TaskInstance
155
+ ) -> WordleEnvironment:
156
+ env = cls(task_instance)
157
+ env.engine = await WordleEngine._deserialize_engine(snapshot)
158
+ env._interact_tool = WordleInteractTool(env.engine)
159
+ return env
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate a fixed Wordle instances.json using the "wordfreq" package.
4
+
5
+ Usage:
6
+ pip install wordfreq
7
+ python -m synth_ai.environments.examples.wordle.helpers.generate_instances_wordfreq \
8
+ --count 500 --min-zipf 3.0 --outfile synth_ai/environments/examples/wordle/instances.json
9
+
10
+ This script writes a deterministic list of 5-letter English words ranked by frequency.
11
+ Commit the resulting instances.json to remove runtime dependencies.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import re
19
+
20
+ from wordfreq import top_n_list, zipf_frequency
21
+
22
+
23
+ def build_word_list(count: int, length: int, min_zipf: float, wordlist: str = "large") -> list[str]:
24
+ n_candidates = max(count * 20, 5000)
25
+ cands = [w.lower() for w in top_n_list("en", n_candidates, wordlist=wordlist)]
26
+ cands = [w for w in cands if len(w) == length and re.fullmatch(r"[a-z]+", w)]
27
+ scored = [(w, zipf_frequency(w, "en")) for w in cands]
28
+ scored = [p for p in scored if p[1] >= float(min_zipf)]
29
+ scored.sort(key=lambda t: (-t[1], t[0]))
30
+ out: list[str] = []
31
+ seen = set()
32
+ for w, _ in scored:
33
+ if w in seen:
34
+ continue
35
+ seen.add(w)
36
+ out.append(w)
37
+ if len(out) >= count:
38
+ break
39
+ if len(out) < count:
40
+ raise RuntimeError(
41
+ f"Insufficient {length}-letter words from wordfreq after filtering ({len(out)} < {count})."
42
+ )
43
+ return out
44
+
45
+
46
+ def main():
47
+ ap = argparse.ArgumentParser()
48
+ ap.add_argument("--count", type=int, default=500)
49
+ ap.add_argument("--length", type=int, default=5)
50
+ ap.add_argument("--min-zipf", type=float, default=3.0)
51
+ ap.add_argument("--wordlist", type=str, default="large")
52
+ ap.add_argument("--outfile", type=str, required=True)
53
+ args = ap.parse_args()
54
+
55
+ words = build_word_list(args.count, args.length, args.min_zipf, args.wordlist)
56
+
57
+ data = {
58
+ "name": f"Wordle Fixed TaskSet ({args.count} English words)",
59
+ "description": f"{len(words)} {args.length}-letter English words ranked by frequency (wordfreq).",
60
+ "defaults": {
61
+ "word_length": args.length,
62
+ "max_guesses": 6,
63
+ "enforce_wordlist": True,
64
+ "consume_invalid_attempts": True,
65
+ },
66
+ "instances": [{"target_word": w} for w in words],
67
+ }
68
+
69
+ with open(args.outfile, "w") as f:
70
+ json.dump(data, f, indent=2)
71
+ print(f"Wrote {len(words)} words to {args.outfile}")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()