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
@@ -1,31 +1,26 @@
1
- from fastapi import APIRouter, HTTPException, Body
2
- from uuid import uuid4
3
- from typing import Dict, Any, List, Optional
4
- from types import SimpleNamespace
5
- from pydantic import BaseModel
1
+ import base64
2
+ import logging
6
3
  import os
7
- import json
8
4
  import pickle
9
- import base64
10
- import numpy as np
11
5
  import tempfile
12
- from dataclasses import dataclass
13
6
  import time
14
- import logging
7
+ from dataclasses import dataclass
8
+ from io import BytesIO
9
+ from types import SimpleNamespace
10
+ from typing import Any
11
+ from uuid import uuid4
15
12
 
13
+ import numpy as np
14
+ from fastapi import APIRouter, Body, HTTPException
15
+ from pydantic import BaseModel
16
+ from synth_ai.environments.environment.tools import EnvToolCall
16
17
  from synth_ai.environments.service.registry import get_environment_cls, list_supported_env_types
17
18
  from synth_ai.environments.stateful.core import StatefulEnvironment
18
- from synth_ai.environments.environment.tools import EnvToolCall
19
19
 
20
20
  # Set up logging
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
23
  # Import tracing abstractions from v3
24
- from synth_ai.tracing_v3.abstractions import (
25
- RuntimeEvent,
26
- SessionEventMarkovBlanketMessage,
27
- TimeRecord,
28
- )
29
24
 
30
25
  # Try to import Redis for persistent storage
31
26
  try:
@@ -52,7 +47,7 @@ if os.getenv("SYNTH_USE_INMEM", "1") == "1":
52
47
  api_router = APIRouter()
53
48
 
54
49
  # Fallback in-memory store if Redis is not available
55
- instances: Dict[str, StatefulEnvironment] = {}
50
+ instances: dict[str, StatefulEnvironment] = {}
56
51
 
57
52
 
58
53
  # Environment-specific task instance creation
@@ -67,9 +62,9 @@ class MinimalTaskInstanceMetadata:
67
62
  class MinimalIntent:
68
63
  """Minimal intent for environments that need it."""
69
64
 
70
- rubric: Dict[str, Any]
71
- gold_trajectories: Optional[Any] = None
72
- gold_state_diff: Dict = None
65
+ rubric: dict[str, Any]
66
+ gold_trajectories: Any | None = None
67
+ gold_state_diff: dict = None
73
68
  deterministic_eval_functions: list = None
74
69
 
75
70
  def __post_init__(self):
@@ -88,8 +83,8 @@ class MinimalImpetus:
88
83
 
89
84
  def create_task_instance_for_environment(
90
85
  env_name: str,
91
- initial_state: Optional[Dict[str, Any]] = None,
92
- config: Optional[Dict[str, Any]] = None,
86
+ initial_state: dict[str, Any] | None = None,
87
+ config: dict[str, Any] | None = None,
93
88
  ) -> Any:
94
89
  """Create appropriate task instance for different environments."""
95
90
 
@@ -172,7 +167,7 @@ def create_task_instance_for_environment(
172
167
 
173
168
 
174
169
  async def reconstruct_task_instance_from_serialized(
175
- env_name: str, serialized_data: Dict[str, Any]
170
+ env_name: str, serialized_data: dict[str, Any]
176
171
  ) -> Any:
177
172
  """Reconstruct a task instance from serialized data for specific environment types."""
178
173
 
@@ -218,9 +213,9 @@ async def reconstruct_task_instance_from_serialized(
218
213
 
219
214
  elif env_name == "Verilog":
220
215
  # Verilog needs special handling with snapshot_dir
216
+ import tempfile
221
217
  from types import SimpleNamespace
222
218
  from uuid import UUID
223
- import tempfile
224
219
 
225
220
  task = SimpleNamespace()
226
221
  task.id = UUID(serialized_data.get("id", str(uuid4())))
@@ -248,10 +243,11 @@ async def reconstruct_task_instance_from_serialized(
248
243
 
249
244
  elif env_name == "NetHack":
250
245
  # NetHack needs proper TaskInstance structure with NetHackTaskInstanceMetadata
251
- from synth_ai.environments.examples.nethack.taskset import NetHackTaskInstanceMetadata
252
246
  from types import SimpleNamespace
253
247
  from uuid import UUID
254
248
 
249
+ from synth_ai.environments.examples.nethack.taskset import NetHackTaskInstanceMetadata
250
+
255
251
  # Extract metadata from serialized data
256
252
  metadata_data = serialized_data.get("metadata", {})
257
253
  metadata = NetHackTaskInstanceMetadata(
@@ -381,7 +377,7 @@ class InstanceStorage:
381
377
  else:
382
378
  print(f"✅ Stored environment {env_id} in-memory (Redis not available)")
383
379
 
384
- async def get(self, env_id: str) -> Optional[StatefulEnvironment]:
380
+ async def get(self, env_id: str) -> StatefulEnvironment | None:
385
381
  """Retrieve an environment instance"""
386
382
  # Try in-memory first (most reliable)
387
383
  if env_id in instances:
@@ -405,7 +401,7 @@ class InstanceStorage:
405
401
  print(f"❌ Environment {env_id} not found in either store")
406
402
  return None
407
403
 
408
- async def remove(self, env_id: str) -> Optional[StatefulEnvironment]:
404
+ async def remove(self, env_id: str) -> StatefulEnvironment | None:
409
405
  """Remove and return an environment instance"""
410
406
  # Get the environment first
411
407
  env = await self.get(env_id)
@@ -432,7 +428,6 @@ storage = InstanceStorage()
432
428
 
433
429
  def convert_numpy_types(obj):
434
430
  """Convert numpy types to native Python types for JSON serialization"""
435
- import numpy as np
436
431
  from dataclasses import is_dataclass
437
432
 
438
433
  if isinstance(obj, dict):
@@ -484,15 +479,15 @@ def convert_numpy_types(obj):
484
479
 
485
480
  # Request/Response models for better API documentation
486
481
  class InitializeRequest(BaseModel):
487
- initial_state: Optional[Dict[str, Any]] = None
488
- config: Optional[Dict[str, Any]] = None
489
- task_instance: Optional[Dict[str, Any]] = None # Add task_instance field
482
+ initial_state: dict[str, Any] | None = None
483
+ config: dict[str, Any] | None = None
484
+ task_instance: dict[str, Any] | None = None # Add task_instance field
490
485
 
491
486
 
492
487
  class StepRequest(BaseModel):
493
488
  env_id: str
494
- request_id: Optional[str] = None
495
- action: Dict[str, Any]
489
+ request_id: str | None = None
490
+ action: dict[str, Any]
496
491
 
497
492
 
498
493
  class TerminateRequest(BaseModel):
@@ -505,7 +500,7 @@ async def get_health():
505
500
 
506
501
 
507
502
  @api_router.post("/env/{env_name}/initialize")
508
- async def initialize_env(env_name: str, request: InitializeRequest = Body(...)) -> Dict[str, Any]:
503
+ async def initialize_env(env_name: str, request: InitializeRequest = Body(...)) -> dict[str, Any]:
509
504
  """Initialize a new environment instance."""
510
505
  import traceback
511
506
 
@@ -517,11 +512,11 @@ async def initialize_env(env_name: str, request: InitializeRequest = Body(...))
517
512
 
518
513
  # Handle task_instance parameter - use it if provided, otherwise create a new one
519
514
  if request.task_instance:
520
- print(f"🔍 Using provided task_instance...")
515
+ print("🔍 Using provided task_instance...")
521
516
  task = await reconstruct_task_instance_from_serialized(env_name, request.task_instance)
522
517
  print(f"✅ Reconstructed task instance: {type(task)}")
523
518
  else:
524
- print(f"🔍 Creating new task instance...")
519
+ print("🔍 Creating new task instance...")
525
520
  # Create environment-specific task instance
526
521
  task = create_task_instance_for_environment(
527
522
  env_name, request.initial_state, request.config
@@ -529,28 +524,28 @@ async def initialize_env(env_name: str, request: InitializeRequest = Body(...))
529
524
  print(f"✅ Created task instance: {type(task)}")
530
525
 
531
526
  # This is where recursion might happen for Sokoban
532
- print(f"🔍 Creating environment instance...")
527
+ print("🔍 Creating environment instance...")
533
528
  env = cls(task)
534
- print(f"✅ Created environment instance")
529
+ print("✅ Created environment instance")
535
530
 
536
531
  # Generate unique environment ID
537
532
  env_id = str(uuid4())
538
533
  print(f"✅ Generated env_id: {env_id}")
539
534
 
540
535
  # Initialize and get first observation - this might also cause recursion
541
- print(f"🔍 Calling env.initialize()...")
536
+ print("🔍 Calling env.initialize()...")
542
537
  obs = await env.initialize()
543
538
  print(f"✅ Environment initialized, observation type: {type(obs)}")
544
539
 
545
540
  # Store the fully initialized environment (fixes Redis initialization bug)
546
- print(f"🔍 Storing environment...")
541
+ print("🔍 Storing environment...")
547
542
  await storage.store(env_id, env)
548
- print(f"✅ Environment stored")
543
+ print("✅ Environment stored")
549
544
 
550
545
  # Convert numpy types to Python types for JSON serialization
551
- print(f"🔍 Converting numpy types...")
546
+ print("🔍 Converting numpy types...")
552
547
  obs_serializable = convert_numpy_types(obs)
553
- print(f"✅ Numpy types converted")
548
+ print("✅ Numpy types converted")
554
549
 
555
550
  return {"env_id": env_id, "observation": obs_serializable, "done": False, "info": {}}
556
551
 
@@ -561,7 +556,7 @@ async def initialize_env(env_name: str, request: InitializeRequest = Body(...))
561
556
  print(stack_trace)
562
557
  raise HTTPException(
563
558
  status_code=400, detail=f"Recursion error during {env_name} initialization: {str(e)}"
564
- )
559
+ ) from e
565
560
 
566
561
  except Exception as e:
567
562
  # Capture all other errors
@@ -570,14 +565,14 @@ async def initialize_env(env_name: str, request: InitializeRequest = Body(...))
570
565
  print(stack_trace)
571
566
  raise HTTPException(
572
567
  status_code=400, detail=f"Error during {env_name} initialization: {str(e)}"
573
- )
568
+ ) from e
574
569
 
575
570
 
576
571
  @api_router.post("/env/{env_name}/step")
577
- async def step_env(env_name: str, request: StepRequest = Body(...)) -> Dict[str, Any]:
572
+ async def step_env(env_name: str, request: StepRequest = Body(...)) -> dict[str, Any]:
578
573
  """Execute a step in the environment."""
579
- import uuid as uuid_module
580
574
  import sys
575
+ import uuid as uuid_module
581
576
 
582
577
  # Use provided request_id or generate one
583
578
  request_id = request.request_id or str(uuid_module.uuid4())[:8]
@@ -700,11 +695,11 @@ async def step_env(env_name: str, request: StepRequest = Body(...)) -> Dict[str,
700
695
  logger.error(
701
696
  f"🌐 [{request_id}] STEP FAILED - env: {env_name}, time: {elapsed_time:.3f}s, error: {type(e).__name__} - {e}"
702
697
  )
703
- raise HTTPException(status_code=400, detail=str(e))
698
+ raise HTTPException(status_code=400, detail=str(e)) from e
704
699
 
705
700
 
706
701
  @api_router.post("/env/{env_name}/terminate")
707
- async def terminate_env(env_name: str, request: TerminateRequest = Body(...)) -> Dict[str, Any]:
702
+ async def terminate_env(env_name: str, request: TerminateRequest = Body(...)) -> dict[str, Any]:
708
703
  """Terminate an environment instance."""
709
704
  logger.info(f"🚪 Terminating environment: {env_name}, env_id: {request.env_id}")
710
705
  env = await storage.remove(request.env_id)
@@ -724,11 +719,53 @@ async def terminate_env(env_name: str, request: TerminateRequest = Body(...)) ->
724
719
  "private": {"instance_id": request.env_id},
725
720
  }
726
721
  except Exception as e:
727
- raise HTTPException(status_code=400, detail=str(e))
722
+ raise HTTPException(status_code=400, detail=str(e)) from e
723
+
724
+
725
+ @api_router.get("/env/{env_name}/frame")
726
+ async def get_env_frame(env_name: str, env_id: str) -> dict[str, Any]:
727
+ """Return the current rendered frame of the environment as base64 PNG.
728
+
729
+ This provides a lightweight way for clients to capture before/after snapshots
730
+ around steps without modifying core step responses.
731
+ """
732
+ env = await storage.get(env_id)
733
+ if not env:
734
+ raise HTTPException(status_code=404, detail=f"Environment instance {env_id} not found")
735
+
736
+ try:
737
+ # For CrafterClassic, underlying engine exposes env.render() -> RGB ndarray
738
+ if (
739
+ hasattr(env, "engine")
740
+ and hasattr(env.engine, "env")
741
+ and hasattr(env.engine.env, "render")
742
+ ):
743
+ rgb = env.engine.env.render()
744
+ else:
745
+ raise RuntimeError("Environment does not support render()")
746
+
747
+ if rgb is None:
748
+ raise RuntimeError("render() returned None")
749
+
750
+ # Encode to PNG base64
751
+ try:
752
+ from PIL import Image # type: ignore
753
+
754
+ img = Image.fromarray(rgb.astype("uint8"), "RGB")
755
+ buf = BytesIO()
756
+ img.save(buf, format="PNG")
757
+ b64 = base64.b64encode(buf.getvalue()).decode("ascii")
758
+ except Exception as e:
759
+ raise RuntimeError(f"failed to encode frame: {e}") from e
760
+
761
+ return {"env_id": env_id, "image_base64": b64}
762
+ except Exception as e:
763
+ logger.error(f"Error rendering frame for {env_id}: {e}")
764
+ raise HTTPException(status_code=500, detail=str(e)) from e
728
765
 
729
766
 
730
767
  @api_router.get("/env/{env_name}/metadata")
731
- async def get_env_metadata(env_name: str, env_id: str) -> Dict[str, Any]:
768
+ async def get_env_metadata(env_name: str, env_id: str) -> dict[str, Any]:
732
769
  """Get metadata about an environment instance."""
733
770
  env = await storage.get(env_id)
734
771
  if not env:
@@ -769,16 +806,16 @@ async def get_env_metadata(env_name: str, env_id: str) -> Dict[str, Any]:
769
806
  return metadata
770
807
  except Exception as e:
771
808
  logger.error(f"Error getting metadata for environment {env_id}: {e}")
772
- raise HTTPException(status_code=500, detail=str(e))
809
+ raise HTTPException(status_code=500, detail=str(e)) from e
773
810
 
774
811
 
775
812
  # Keep backward compatibility endpoints but mark as deprecated
776
813
  @api_router.post("/{env_type}/create", deprecated=True)
777
814
  async def create_env_legacy(
778
815
  env_type: str,
779
- config: Optional[Dict[str, Any]] = None,
780
- initial_state: Optional[Dict[str, Any]] = None,
781
- ) -> Dict[str, str]:
816
+ config: dict[str, Any] | None = None,
817
+ initial_state: dict[str, Any] | None = None,
818
+ ) -> dict[str, str]:
782
819
  """[DEPRECATED] Use /env/{env_name}/initialize instead."""
783
820
  cls = get_environment_cls(env_type)
784
821
  task = create_task_instance_for_environment(env_type, initial_state, config)
@@ -793,8 +830,8 @@ async def create_env_legacy(
793
830
 
794
831
  @api_router.post("/{env_type}/{instance_id}/reset", deprecated=True)
795
832
  async def reset_env_legacy(
796
- env_type: str, instance_id: str, seed: Optional[int] = None
797
- ) -> Dict[str, Any]:
833
+ env_type: str, instance_id: str, seed: int | None = None
834
+ ) -> dict[str, Any]:
798
835
  """[DEPRECATED] Use /env/{env_name}/initialize instead."""
799
836
  env = await storage.get(instance_id)
800
837
  if not env:
@@ -805,7 +842,7 @@ async def reset_env_legacy(
805
842
 
806
843
 
807
844
  @api_router.post("/{env_type}/{instance_id}/step", deprecated=True)
808
- async def step_env_legacy(env_type: str, instance_id: str, calls: List[Any]) -> Dict[str, Any]:
845
+ async def step_env_legacy(env_type: str, instance_id: str, calls: list[Any]) -> dict[str, Any]:
809
846
  """[DEPRECATED] Use /env/{env_name}/step instead."""
810
847
  env = await storage.get(instance_id)
811
848
  if not env:
@@ -827,7 +864,7 @@ async def terminate_env_legacy(env_type: str, instance_id: str) -> Any:
827
864
 
828
865
 
829
866
  @api_router.get("/{env_type}/{instance_id}/checkpoint")
830
- async def checkpoint_env(env_type: str, instance_id: str) -> Dict[str, Any]:
867
+ async def checkpoint_env(env_type: str, instance_id: str) -> dict[str, Any]:
831
868
  """Get a checkpoint of the environment state."""
832
869
  env = await storage.get(instance_id)
833
870
  if not env:
@@ -839,11 +876,12 @@ async def checkpoint_env(env_type: str, instance_id: str) -> Dict[str, Any]:
839
876
 
840
877
  # ===== Dynamic Environment Registration API =====
841
878
 
879
+
842
880
  class RegisterEnvironmentRequest(BaseModel):
843
881
  name: str
844
882
  module_path: str
845
883
  class_name: str
846
- description: Optional[str] = None
884
+ description: str | None = None
847
885
 
848
886
 
849
887
  class UnregisterEnvironmentRequest(BaseModel):
@@ -851,13 +889,13 @@ class UnregisterEnvironmentRequest(BaseModel):
851
889
 
852
890
 
853
891
  @api_router.post("/registry/environments")
854
- async def register_environment_api(request: RegisterEnvironmentRequest) -> Dict[str, Any]:
892
+ async def register_environment_api(request: RegisterEnvironmentRequest) -> dict[str, Any]:
855
893
  """
856
894
  Dynamically register a new environment at runtime.
857
-
895
+
858
896
  This endpoint allows third-party packages to register environments without
859
897
  restarting the service. The environment class will be imported and validated.
860
-
898
+
861
899
  Example:
862
900
  POST /registry/environments
863
901
  {
@@ -870,119 +908,113 @@ async def register_environment_api(request: RegisterEnvironmentRequest) -> Dict[
870
908
  try:
871
909
  # Import the module
872
910
  import importlib
911
+
873
912
  module = importlib.import_module(request.module_path)
874
-
913
+
875
914
  # Get the class from the module
876
915
  if not hasattr(module, request.class_name):
877
916
  raise HTTPException(
878
917
  status_code=400,
879
- detail=f"Class '{request.class_name}' not found in module '{request.module_path}'"
918
+ detail=f"Class '{request.class_name}' not found in module '{request.module_path}'",
880
919
  )
881
-
920
+
882
921
  env_cls = getattr(module, request.class_name)
883
-
922
+
884
923
  # Validate that it's a StatefulEnvironment subclass
885
924
  from synth_ai.environments.stateful.core import StatefulEnvironment
925
+
886
926
  if not issubclass(env_cls, StatefulEnvironment):
887
927
  raise HTTPException(
888
928
  status_code=400,
889
- detail=f"Class '{request.class_name}' is not a subclass of StatefulEnvironment"
929
+ detail=f"Class '{request.class_name}' is not a subclass of StatefulEnvironment",
890
930
  )
891
-
931
+
892
932
  # Register the environment
893
933
  from synth_ai.environments.environment.registry import register_environment
934
+
894
935
  register_environment(request.name, env_cls)
895
-
936
+
896
937
  logger.info(f"Dynamically registered environment: {request.name}")
897
-
938
+
898
939
  return {
899
940
  "success": True,
900
941
  "message": f"Environment '{request.name}' registered successfully",
901
942
  "name": request.name,
902
943
  "module_path": request.module_path,
903
944
  "class_name": request.class_name,
904
- "description": request.description
945
+ "description": request.description,
905
946
  }
906
-
947
+
907
948
  except ImportError as e:
908
949
  raise HTTPException(
909
- status_code=400,
910
- detail=f"Failed to import module '{request.module_path}': {str(e)}"
911
- )
950
+ status_code=400, detail=f"Failed to import module '{request.module_path}': {str(e)}"
951
+ ) from e
912
952
  except Exception as e:
913
953
  logger.error(f"Failed to register environment {request.name}: {e}")
914
- raise HTTPException(
915
- status_code=500,
916
- detail=f"Failed to register environment: {str(e)}"
917
- )
954
+ raise HTTPException(status_code=500, detail=f"Failed to register environment: {str(e)}") from e
918
955
 
919
956
 
920
957
  @api_router.delete("/registry/environments/{env_name}")
921
- async def unregister_environment_api(env_name: str) -> Dict[str, Any]:
958
+ async def unregister_environment_api(env_name: str) -> dict[str, Any]:
922
959
  """
923
960
  Unregister an environment from the registry.
924
-
961
+
925
962
  This removes the environment from the in-memory registry, making it
926
963
  unavailable for new instances. Existing instances are not affected.
927
964
  """
928
965
  try:
929
966
  from synth_ai.environments.environment.registry import ENV_REGISTRY
930
-
967
+
931
968
  if env_name not in ENV_REGISTRY:
932
969
  raise HTTPException(
933
- status_code=404,
934
- detail=f"Environment '{env_name}' not found in registry"
970
+ status_code=404, detail=f"Environment '{env_name}' not found in registry"
935
971
  )
936
-
972
+
937
973
  # Remove from registry
938
974
  removed_cls = ENV_REGISTRY.pop(env_name)
939
-
975
+
940
976
  logger.info(f"Unregistered environment: {env_name}")
941
-
977
+
942
978
  return {
943
979
  "success": True,
944
980
  "message": f"Environment '{env_name}' unregistered successfully",
945
981
  "name": env_name,
946
- "class_name": removed_cls.__name__
982
+ "class_name": removed_cls.__name__,
947
983
  }
948
-
984
+
949
985
  except Exception as e:
950
986
  logger.error(f"Failed to unregister environment {env_name}: {e}")
951
- raise HTTPException(
952
- status_code=500,
953
- detail=f"Failed to unregister environment: {str(e)}"
954
- )
987
+ raise HTTPException(status_code=500, detail=f"Failed to unregister environment: {str(e)}") from e
955
988
 
956
989
 
957
990
  @api_router.get("/registry/environments")
958
- async def list_registered_environments() -> Dict[str, Any]:
991
+ async def list_registered_environments() -> dict[str, Any]:
959
992
  """
960
993
  List all registered environments with their details.
961
-
994
+
962
995
  Returns information about all available environments in the registry,
963
996
  including both built-in and dynamically registered environments.
964
997
  """
965
998
  try:
966
999
  from synth_ai.environments.environment.registry import ENV_REGISTRY
967
-
1000
+
968
1001
  environments = []
969
1002
  for name, env_cls in ENV_REGISTRY.items():
970
1003
  env_info = {
971
1004
  "name": name,
972
1005
  "class_name": env_cls.__name__,
973
1006
  "module": env_cls.__module__,
974
- "description": getattr(env_cls, "__doc__", "").split("\n")[0] if env_cls.__doc__ else None
1007
+ "description": getattr(env_cls, "__doc__", "").split("\n")[0]
1008
+ if env_cls.__doc__
1009
+ else None,
975
1010
  }
976
1011
  environments.append(env_info)
977
-
1012
+
978
1013
  return {
979
1014
  "environments": sorted(environments, key=lambda x: x["name"]),
980
- "total_count": len(environments)
1015
+ "total_count": len(environments),
981
1016
  }
982
-
1017
+
983
1018
  except Exception as e:
984
1019
  logger.error(f"Failed to list environments: {e}")
985
- raise HTTPException(
986
- status_code=500,
987
- detail=f"Failed to list environments: {str(e)}"
988
- )
1020
+ raise HTTPException(status_code=500, detail=f"Failed to list environments: {str(e)}") from e
@@ -6,7 +6,6 @@ This module provides functionality to register environments from external packag
6
6
 
7
7
  import importlib
8
8
  import logging
9
- from typing import List, Dict
10
9
 
11
10
  logger = logging.getLogger(__name__)
12
11
 
@@ -14,7 +13,7 @@ logger = logging.getLogger(__name__)
14
13
  class ExternalRegistryConfig:
15
14
  """Configuration for external environment registries."""
16
15
 
17
- def __init__(self, external_environments: List[Dict[str, str]] = None):
16
+ def __init__(self, external_environments: list[dict[str, str]] = None):
18
17
  self.external_environments = external_environments or []
19
18
 
20
19
 
@@ -1,9 +1,9 @@
1
1
  # This file re-exports the actual registry functions from synth_ai.environments.environment.registry
2
2
  # to be used by the service layer, maintaining a clean separation if needed.
3
3
  from synth_ai.environments.environment.registry import (
4
- register_environment,
5
4
  get_environment_cls,
6
5
  list_supported_env_types,
6
+ register_environment,
7
7
  )
8
8
 
9
9
  __all__ = ["register_environment", "get_environment_cls", "list_supported_env_types"]
@@ -1,5 +1,4 @@
1
1
  from abc import abstractmethod
2
- from typing import List
3
2
 
4
3
  from synth_ai.environments.environment.shared_engine import Engine, InternalObservation
5
4
  from synth_ai.environments.environment.tools import EnvToolCall
@@ -108,7 +107,7 @@ class StatefulEnvironment(Engine):
108
107
  pass
109
108
 
110
109
  @abstractmethod
111
- async def step(self, tool_calls: List[EnvToolCall]) -> InternalObservation:
110
+ async def step(self, tool_calls: list[EnvToolCall]) -> InternalObservation:
112
111
  """
113
112
  Execute tool calls and return the resulting observation.
114
113
 
@@ -1,6 +1,6 @@
1
- from synth_ai.environments.environment.shared_engine import Engine
2
1
  from typing import TypeVar
3
2
 
3
+ from synth_ai.environments.environment.shared_engine import Engine
4
4
 
5
5
  SnapshotType = TypeVar("SnapshotType", bound="StatefulEngineSnapshot")
6
6
 
@@ -1,11 +1,11 @@
1
1
  from synth_ai.environments.tasks.core import (
2
+ Impetus,
3
+ Intent,
4
+ SplitInfo,
2
5
  Task,
3
6
  TaskInstance,
4
- TaskInstanceSet,
5
7
  TaskInstanceMetadata,
6
- SplitInfo,
7
- Impetus,
8
- Intent,
8
+ TaskInstanceSet,
9
9
  )
10
10
 
11
11
  __all__ = [
@@ -1,8 +1,10 @@
1
- from typing import Optional, Dict, List, Callable, Set, Any
2
- from synth_ai.environments.v0_observability.history import SynthGlobalTrajectory
3
- from uuid import UUID
4
1
  from abc import abstractmethod
2
+ from collections.abc import Callable
5
3
  from dataclasses import dataclass, field
4
+ from typing import Any, Optional
5
+ from uuid import UUID
6
+
7
+ from synth_ai.environments.v0_observability.history import SynthGlobalTrajectory
6
8
 
7
9
 
8
10
  @dataclass
@@ -11,7 +13,7 @@ class Task:
11
13
  global_constraints: str
12
14
  global_objectives: str
13
15
 
14
- shared_env_params: Optional[Dict]
16
+ shared_env_params: dict | None
15
17
 
16
18
 
17
19
  @dataclass
@@ -21,10 +23,10 @@ class TaskInstanceMetadata:
21
23
 
22
24
  @dataclass
23
25
  class Intent:
24
- rubric: Dict[str, Any]
25
- gold_trajectories: Optional[SynthGlobalTrajectory]
26
- gold_state_diff: Dict
27
- deterministic_eval_functions: List[Callable] = field(default_factory=list)
26
+ rubric: dict[str, Any]
27
+ gold_trajectories: SynthGlobalTrajectory | None
28
+ gold_state_diff: dict
29
+ deterministic_eval_functions: list[Callable] = field(default_factory=list)
28
30
 
29
31
 
30
32
  @dataclass
@@ -44,7 +46,7 @@ class TaskInstance:
44
46
  initial_engine_snapshot: Optional["StatefulEngineSnapshot"]
45
47
 
46
48
  @abstractmethod
47
- async def serialize(self) -> Dict:
49
+ async def serialize(self) -> dict:
48
50
  pass
49
51
 
50
52
  @abstractmethod
@@ -65,8 +67,8 @@ class TaskInstanceMetadataFilter:
65
67
 
66
68
  @dataclass
67
69
  class SplitInfo:
68
- val_instance_ids: Set[str]
69
- test_instance_ids: Set[str]
70
+ val_instance_ids: set[str]
71
+ test_instance_ids: set[str]
70
72
  _is_split_defined: bool
71
73
 
72
74
 
@@ -74,5 +76,5 @@ class SplitInfo:
74
76
  class TaskInstanceSet:
75
77
  name: str
76
78
  description: str
77
- instances: List[TaskInstance]
79
+ instances: list[TaskInstance]
78
80
  split_info: SplitInfo