synth-ai 0.2.4.dev6__py3-none-any.whl → 0.2.4.dev8__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 (256) hide show
  1. synth_ai/__init__.py +18 -9
  2. synth_ai/cli/__init__.py +10 -5
  3. synth_ai/cli/balance.py +25 -32
  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 +47 -2
  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 +13 -13
  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 +5 -5
  104. synth_ai/environments/examples/wordle/engine.py +32 -25
  105. synth_ai/environments/examples/wordle/environment.py +21 -16
  106. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +6 -6
  107. synth_ai/environments/examples/wordle/taskset.py +20 -12
  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 +3 -2
  111. synth_ai/environments/service/core_routes.py +104 -110
  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/http.py +102 -0
  123. synth_ai/inference/__init__.py +7 -0
  124. synth_ai/inference/client.py +20 -0
  125. synth_ai/jobs/client.py +246 -0
  126. synth_ai/learning/__init__.py +24 -0
  127. synth_ai/learning/client.py +149 -0
  128. synth_ai/learning/config.py +43 -0
  129. synth_ai/learning/constants.py +29 -0
  130. synth_ai/learning/ft_client.py +59 -0
  131. synth_ai/learning/gateway.py +1 -3
  132. synth_ai/learning/health.py +43 -0
  133. synth_ai/learning/jobs.py +205 -0
  134. synth_ai/learning/prompts/banking77_injection_eval.py +15 -10
  135. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +26 -14
  136. synth_ai/learning/prompts/mipro.py +61 -52
  137. synth_ai/learning/prompts/random_search.py +42 -43
  138. synth_ai/learning/prompts/run_mipro_banking77.py +32 -20
  139. synth_ai/learning/prompts/run_random_search_banking77.py +71 -52
  140. synth_ai/learning/rl_client.py +256 -0
  141. synth_ai/learning/sse.py +58 -0
  142. synth_ai/learning/validators.py +48 -0
  143. synth_ai/lm/__init__.py +5 -5
  144. synth_ai/lm/caching/ephemeral.py +9 -9
  145. synth_ai/lm/caching/handler.py +20 -20
  146. synth_ai/lm/caching/persistent.py +10 -10
  147. synth_ai/lm/config.py +3 -3
  148. synth_ai/lm/constants.py +7 -7
  149. synth_ai/lm/core/all.py +17 -3
  150. synth_ai/lm/core/exceptions.py +0 -2
  151. synth_ai/lm/core/main.py +26 -41
  152. synth_ai/lm/core/main_v3.py +33 -10
  153. synth_ai/lm/core/synth_models.py +48 -0
  154. synth_ai/lm/core/vendor_clients.py +26 -22
  155. synth_ai/lm/injection.py +7 -8
  156. synth_ai/lm/overrides.py +21 -19
  157. synth_ai/lm/provider_support/__init__.py +1 -1
  158. synth_ai/lm/provider_support/anthropic.py +15 -15
  159. synth_ai/lm/provider_support/openai.py +23 -21
  160. synth_ai/lm/structured_outputs/handler.py +34 -32
  161. synth_ai/lm/structured_outputs/inject.py +24 -27
  162. synth_ai/lm/structured_outputs/rehabilitate.py +19 -15
  163. synth_ai/lm/tools/base.py +17 -16
  164. synth_ai/lm/unified_interface.py +17 -18
  165. synth_ai/lm/vendors/base.py +20 -18
  166. synth_ai/lm/vendors/core/anthropic_api.py +36 -27
  167. synth_ai/lm/vendors/core/gemini_api.py +31 -36
  168. synth_ai/lm/vendors/core/mistral_api.py +19 -19
  169. synth_ai/lm/vendors/core/openai_api.py +42 -13
  170. synth_ai/lm/vendors/openai_standard.py +158 -101
  171. synth_ai/lm/vendors/openai_standard_responses.py +74 -61
  172. synth_ai/lm/vendors/retries.py +9 -1
  173. synth_ai/lm/vendors/supported/custom_endpoint.py +38 -28
  174. synth_ai/lm/vendors/supported/deepseek.py +10 -10
  175. synth_ai/lm/vendors/supported/grok.py +8 -8
  176. synth_ai/lm/vendors/supported/ollama.py +2 -1
  177. synth_ai/lm/vendors/supported/openrouter.py +11 -9
  178. synth_ai/lm/vendors/synth_client.py +425 -75
  179. synth_ai/lm/warmup.py +8 -7
  180. synth_ai/rl/__init__.py +30 -0
  181. synth_ai/rl/contracts.py +32 -0
  182. synth_ai/rl/env_keys.py +137 -0
  183. synth_ai/rl/secrets.py +19 -0
  184. synth_ai/scripts/verify_rewards.py +100 -0
  185. synth_ai/task/__init__.py +10 -0
  186. synth_ai/task/contracts.py +120 -0
  187. synth_ai/task/health.py +28 -0
  188. synth_ai/task/validators.py +12 -0
  189. synth_ai/tracing/__init__.py +22 -10
  190. synth_ai/tracing_v1/__init__.py +22 -20
  191. synth_ai/tracing_v3/__init__.py +7 -7
  192. synth_ai/tracing_v3/abstractions.py +56 -52
  193. synth_ai/tracing_v3/config.py +4 -2
  194. synth_ai/tracing_v3/db_config.py +6 -8
  195. synth_ai/tracing_v3/decorators.py +29 -30
  196. synth_ai/tracing_v3/examples/basic_usage.py +12 -12
  197. synth_ai/tracing_v3/hooks.py +24 -22
  198. synth_ai/tracing_v3/llm_call_record_helpers.py +85 -98
  199. synth_ai/tracing_v3/lm_call_record_abstractions.py +2 -4
  200. synth_ai/tracing_v3/migration_helper.py +3 -5
  201. synth_ai/tracing_v3/replica_sync.py +30 -32
  202. synth_ai/tracing_v3/session_tracer.py +158 -31
  203. synth_ai/tracing_v3/storage/__init__.py +1 -1
  204. synth_ai/tracing_v3/storage/base.py +8 -7
  205. synth_ai/tracing_v3/storage/config.py +4 -4
  206. synth_ai/tracing_v3/storage/factory.py +4 -4
  207. synth_ai/tracing_v3/storage/utils.py +9 -9
  208. synth_ai/tracing_v3/turso/__init__.py +3 -3
  209. synth_ai/tracing_v3/turso/daemon.py +9 -9
  210. synth_ai/tracing_v3/turso/manager.py +278 -48
  211. synth_ai/tracing_v3/turso/models.py +77 -19
  212. synth_ai/tracing_v3/utils.py +5 -5
  213. synth_ai/v0/tracing/abstractions.py +28 -28
  214. synth_ai/v0/tracing/base_client.py +9 -9
  215. synth_ai/v0/tracing/client_manager.py +7 -7
  216. synth_ai/v0/tracing/config.py +7 -7
  217. synth_ai/v0/tracing/context.py +6 -6
  218. synth_ai/v0/tracing/decorators.py +6 -5
  219. synth_ai/v0/tracing/events/manage.py +1 -1
  220. synth_ai/v0/tracing/events/store.py +5 -4
  221. synth_ai/v0/tracing/immediate_client.py +4 -5
  222. synth_ai/v0/tracing/local.py +3 -3
  223. synth_ai/v0/tracing/log_client_base.py +4 -5
  224. synth_ai/v0/tracing/retry_queue.py +5 -6
  225. synth_ai/v0/tracing/trackers.py +25 -25
  226. synth_ai/v0/tracing/upload.py +6 -0
  227. synth_ai/v0/tracing_v1/__init__.py +1 -1
  228. synth_ai/v0/tracing_v1/abstractions.py +28 -28
  229. synth_ai/v0/tracing_v1/base_client.py +9 -9
  230. synth_ai/v0/tracing_v1/client_manager.py +7 -7
  231. synth_ai/v0/tracing_v1/config.py +7 -7
  232. synth_ai/v0/tracing_v1/context.py +6 -6
  233. synth_ai/v0/tracing_v1/decorators.py +7 -6
  234. synth_ai/v0/tracing_v1/events/manage.py +1 -1
  235. synth_ai/v0/tracing_v1/events/store.py +5 -4
  236. synth_ai/v0/tracing_v1/immediate_client.py +4 -5
  237. synth_ai/v0/tracing_v1/local.py +3 -3
  238. synth_ai/v0/tracing_v1/log_client_base.py +4 -5
  239. synth_ai/v0/tracing_v1/retry_queue.py +5 -6
  240. synth_ai/v0/tracing_v1/trackers.py +25 -25
  241. synth_ai/v0/tracing_v1/upload.py +25 -24
  242. synth_ai/zyk/__init__.py +1 -0
  243. synth_ai-0.2.4.dev8.dist-info/METADATA +635 -0
  244. synth_ai-0.2.4.dev8.dist-info/RECORD +317 -0
  245. synth_ai/tui/__init__.py +0 -1
  246. synth_ai/tui/__main__.py +0 -13
  247. synth_ai/tui/cli/__init__.py +0 -1
  248. synth_ai/tui/cli/query_experiments.py +0 -165
  249. synth_ai/tui/cli/query_experiments_v3.py +0 -165
  250. synth_ai/tui/dashboard.py +0 -329
  251. synth_ai-0.2.4.dev6.dist-info/METADATA +0 -203
  252. synth_ai-0.2.4.dev6.dist-info/RECORD +0 -299
  253. {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/WHEEL +0 -0
  254. {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/entry_points.txt +0 -0
  255. {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/licenses/LICENSE +0 -0
  256. {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/top_level.txt +0 -0
synth_ai/lm/warmup.py CHANGED
@@ -3,13 +3,14 @@ Model warmup utilities for Synth backend.
3
3
  Handles model preloading and warmup polling.
4
4
  """
5
5
 
6
- import httpx
7
6
  import asyncio
8
7
  import logging
9
8
  import sys
10
9
  import time
11
- from typing import Optional, Dict, Any
12
10
  from datetime import datetime, timedelta
11
+
12
+ import httpx
13
+
13
14
  from .config import SynthConfig
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -19,7 +20,7 @@ class WarmupStatus:
19
20
  """Track warmup status for models with TTL."""
20
21
 
21
22
  def __init__(self):
22
- self._warmed_models: Dict[str, datetime] = {}
23
+ self._warmed_models: dict[str, datetime] = {}
23
24
  self._ttl = timedelta(minutes=10) # Consider models warm for 10 minutes
24
25
 
25
26
  def is_warm(self, model_name: str) -> bool:
@@ -47,11 +48,11 @@ _warmup_status = WarmupStatus()
47
48
 
48
49
  async def warmup_synth_model(
49
50
  model_name: str,
50
- config: Optional[SynthConfig] = None,
51
- max_attempts: Optional[int] = None,
51
+ config: SynthConfig | None = None,
52
+ max_attempts: int | None = None,
52
53
  force: bool = False,
53
54
  verbose: bool = True,
54
- gpu_preference: Optional[str] = None,
55
+ gpu_preference: str | None = None,
55
56
  ) -> bool:
56
57
  """
57
58
  Warm up a model on the Synth backend using fire-and-forget approach.
@@ -161,7 +162,7 @@ async def warmup_synth_model(
161
162
  )
162
163
  sys.stdout.flush()
163
164
  await asyncio.sleep(1.0)
164
- except Exception as e:
165
+ except Exception:
165
166
  # Continue polling; update spinner line with error label
166
167
  elapsed = int(time.time() - start_time)
167
168
  wheel = spinner[spin_idx % len(spinner)]
@@ -0,0 +1,30 @@
1
+ from .contracts import (
2
+ RolloutEnvSpec,
3
+ RolloutPolicySpec,
4
+ RolloutRecordConfig,
5
+ RolloutSafetyConfig,
6
+ RolloutRequest,
7
+ RolloutStep,
8
+ RolloutTrajectory,
9
+ RolloutMetrics,
10
+ RolloutResponse,
11
+ )
12
+ from .env_keys import MAX_ENVIRONMENT_API_KEY_BYTES, encrypt_for_backend, setup_environment_api_key
13
+ from .secrets import mint_environment_api_key
14
+
15
+ __all__ = [
16
+ "RolloutEnvSpec",
17
+ "RolloutPolicySpec",
18
+ "RolloutRecordConfig",
19
+ "RolloutSafetyConfig",
20
+ "RolloutRequest",
21
+ "RolloutStep",
22
+ "RolloutTrajectory",
23
+ "RolloutMetrics",
24
+ "RolloutResponse",
25
+ "encrypt_for_backend",
26
+ "setup_environment_api_key",
27
+ "mint_environment_api_key",
28
+ "MAX_ENVIRONMENT_API_KEY_BYTES",
29
+ ]
30
+
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Compatibility layer: re-export Task App rollout contracts from synth_ai.task.contracts
5
+ so existing imports continue to work while consolidating under synth_ai.task.
6
+ """
7
+
8
+ from synth_ai.task.contracts import (
9
+ RolloutEnvSpec,
10
+ RolloutPolicySpec,
11
+ RolloutRecordConfig,
12
+ RolloutSafetyConfig,
13
+ RolloutRequest,
14
+ RolloutStep,
15
+ RolloutTrajectory,
16
+ RolloutMetrics,
17
+ RolloutResponse,
18
+ )
19
+
20
+ __all__ = [
21
+ "RolloutEnvSpec",
22
+ "RolloutPolicySpec",
23
+ "RolloutRecordConfig",
24
+ "RolloutSafetyConfig",
25
+ "RolloutRequest",
26
+ "RolloutStep",
27
+ "RolloutTrajectory",
28
+ "RolloutMetrics",
29
+ "RolloutResponse",
30
+ ]
31
+
32
+
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ """Helpers for uploading RL environment credentials to the backend."""
4
+
5
+ import base64
6
+ import binascii
7
+ import json
8
+ from typing import Any, Dict
9
+ import os
10
+
11
+ import requests
12
+ from nacl.public import PublicKey, SealedBox
13
+
14
+ __all__ = ["encrypt_for_backend", "setup_environment_api_key", "MAX_ENVIRONMENT_API_KEY_BYTES"]
15
+
16
+ MAX_ENVIRONMENT_API_KEY_BYTES = 8 * 1024
17
+ _ALGORITHM = "libsodium.sealedbox.v1"
18
+
19
+
20
+ def encrypt_for_backend(pubkey_b64: str, secret: str | bytes) -> str:
21
+ """Encrypt ``secret`` for storage by the backend using libsodium sealed boxes."""
22
+
23
+ if not isinstance(pubkey_b64, str) or not pubkey_b64.strip():
24
+ raise ValueError("public key must be a non-empty base64 string")
25
+
26
+ try:
27
+ key_bytes = base64.b64decode(pubkey_b64, validate=True)
28
+ except binascii.Error as exc: # pragma: no cover - defensive guard
29
+ raise ValueError("public key must be base64-encoded") from exc
30
+
31
+ if len(key_bytes) != 32:
32
+ raise ValueError("public key must be 32 bytes for X25519")
33
+
34
+ if isinstance(secret, str):
35
+ secret_bytes = secret.encode("utf-8")
36
+ elif isinstance(secret, bytes):
37
+ secret_bytes = secret
38
+ else: # pragma: no cover - type guard
39
+ raise TypeError("secret must be str or bytes")
40
+
41
+ if not secret_bytes:
42
+ raise ValueError("secret must not be empty")
43
+
44
+ box = SealedBox(PublicKey(key_bytes))
45
+ ciphertext = box.encrypt(secret_bytes)
46
+ return base64.b64encode(ciphertext).decode("ascii")
47
+
48
+
49
+ def setup_environment_api_key(
50
+ backend_base: str,
51
+ synth_api_key: str,
52
+ token: str | None = None,
53
+ *,
54
+ timeout: float = 15.0,
55
+ ) -> Dict[str, Any]:
56
+ """Upload an ENVIRONMENT_API_KEY to the backend."""
57
+
58
+ backend = backend_base.rstrip("/")
59
+ if not backend:
60
+ raise ValueError("backend_base must be provided")
61
+ if not synth_api_key:
62
+ raise ValueError("synth_api_key must be provided")
63
+
64
+ # Require caller-provided plaintext. If not provided, read from ENVIRONMENT_API_KEY.
65
+ plaintext = token if token is not None else os.getenv("ENVIRONMENT_API_KEY", "").strip()
66
+ if not plaintext:
67
+ raise ValueError("ENVIRONMENT_API_KEY must be set (or pass token=...) to upload")
68
+ if not isinstance(plaintext, str): # pragma: no cover - defensive guard
69
+ raise TypeError("token must be a string")
70
+
71
+ token_bytes = plaintext.encode("utf-8")
72
+ if not token_bytes:
73
+ raise ValueError("ENVIRONMENT_API_KEY token must not be empty")
74
+ if len(token_bytes) > MAX_ENVIRONMENT_API_KEY_BYTES:
75
+ raise ValueError("ENVIRONMENT_API_KEY token exceeds 8 KiB limit")
76
+
77
+ headers = {"Authorization": f"Bearer {synth_api_key}"}
78
+ pub_url = f"{backend}/api/v1/crypto/public-key"
79
+ response = requests.get(pub_url, headers=headers, timeout=timeout)
80
+ _raise_with_detail(response)
81
+
82
+ try:
83
+ doc = response.json()
84
+ except ValueError as exc: # pragma: no cover - backend invariant
85
+ raise RuntimeError("backend returned invalid JSON for public key") from exc
86
+
87
+ if not isinstance(doc, dict):
88
+ raise RuntimeError("backend public key response must be an object")
89
+
90
+ pubkey = doc.get("public_key")
91
+ if not isinstance(pubkey, str) or not pubkey:
92
+ raise RuntimeError("backend response missing public_key")
93
+
94
+ # The backend currently returns a single algorithm identifier; keep a guard in
95
+ # case future versions change the value and we need to surface that to callers.
96
+ alg = doc.get("alg")
97
+ if alg is not None and alg != _ALGORITHM:
98
+ raise RuntimeError(f"unsupported sealed box algorithm: {alg}")
99
+
100
+ ciphertext_b64 = encrypt_for_backend(pubkey, token_bytes)
101
+
102
+ body = {"name": "ENVIRONMENT_API_KEY", "ciphertext_b64": ciphertext_b64}
103
+ post_url = f"{backend}/api/v1/env-keys"
104
+ response2 = requests.post(post_url, headers={**headers, "Content-Type": "application/json"}, json=body, timeout=timeout)
105
+ _raise_with_detail(response2)
106
+
107
+ try:
108
+ upload_doc = response2.json()
109
+ except ValueError:
110
+ upload_doc = {}
111
+
112
+ if not isinstance(upload_doc, dict):
113
+ upload_doc = {}
114
+
115
+ return {
116
+ "stored": True,
117
+ "id": upload_doc.get("id"),
118
+ "name": upload_doc.get("name"),
119
+ "updated_at": upload_doc.get("updated_at"),
120
+ }
121
+
122
+
123
+ def _raise_with_detail(response: requests.Response) -> None:
124
+ try:
125
+ response.raise_for_status()
126
+ except requests.HTTPError as exc:
127
+ detail_snippet: str | None = None
128
+ try:
129
+ detail = response.json()
130
+ detail_snippet = json.dumps(detail, separators=(",", ":"))[:200]
131
+ except Exception:
132
+ body = response.text if response.text is not None else ""
133
+ detail_snippet = body[:200] if body else None
134
+ message = str(exc)
135
+ if detail_snippet:
136
+ message = f"{message} | body={detail_snippet}"
137
+ raise requests.HTTPError(message, request=exc.request, response=exc.response) from None
synth_ai/rl/secrets.py ADDED
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ """Helpers for generating RL environment credentials."""
4
+
5
+ import secrets
6
+
7
+ __all__ = ["mint_environment_api_key"]
8
+
9
+
10
+ def mint_environment_api_key() -> str:
11
+ """Mint a random ENVIRONMENT_API_KEY value.
12
+
13
+ The current format is 64 hexadecimal characters (256 bits of entropy), which
14
+ matches the shell helpers used by the RL examples. This keeps the token easy
15
+ to copy while remaining suitably strong for authentication.
16
+ """
17
+
18
+ # secrets.token_hex(32) → 32 random bytes rendered as 64 hex characters.
19
+ return secrets.token_hex(32)
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Verify reward persistence in a traces database.
4
+
5
+ Usage:
6
+ uv run python -m synth_ai.scripts.verify_rewards --db /path/to/db.sqlite --min-reward 1
7
+ """
8
+
9
+ import argparse
10
+ import asyncio
11
+ import os
12
+ from typing import Dict
13
+
14
+ from sqlalchemy import text
15
+
16
+ from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
17
+
18
+
19
+ async def verify(db_path: str, min_reward: int) -> int:
20
+ db_url = db_path
21
+ if not db_url.startswith("sqlite+aiosqlite:///"):
22
+ db_url = f"sqlite+aiosqlite:///{os.path.abspath(db_path)}"
23
+
24
+ mgr = AsyncSQLTraceManager(db_url=db_url)
25
+ await mgr.initialize()
26
+
27
+ try:
28
+ async with mgr.session() as session:
29
+ # Sessions with outcome_rewards
30
+ q_good = text(
31
+ """
32
+ SELECT session_id, MAX(total_reward) as total_reward
33
+ FROM outcome_rewards
34
+ GROUP BY session_id
35
+ """
36
+ )
37
+ res = await session.execute(q_good)
38
+ outcomes = {row[0]: int(row[1]) for row in res.fetchall()}
39
+
40
+ # Sessions without outcome_rewards
41
+ q_missing = text(
42
+ """
43
+ SELECT s.session_id
44
+ FROM session_traces s
45
+ LEFT JOIN outcome_rewards o ON s.session_id = o.session_id
46
+ WHERE o.session_id IS NULL
47
+ """
48
+ )
49
+ res2 = await session.execute(q_missing)
50
+ missing = [row[0] for row in res2.fetchall()]
51
+
52
+ # Aggregate event_rewards per session (informational)
53
+ q_event = text(
54
+ """
55
+ SELECT session_id, COALESCE(SUM(reward_value), 0.0) as sum_rewards
56
+ FROM event_rewards
57
+ GROUP BY session_id
58
+ """
59
+ )
60
+ res3 = await session.execute(q_event)
61
+ event_sums: Dict[str, float] = {row[0]: float(row[1]) for row in res3.fetchall()}
62
+
63
+ print(f"Sessions with outcome_rewards: {len(outcomes)}")
64
+ print(f"Sessions missing outcome_rewards: {len(missing)}")
65
+ if missing:
66
+ print("Missing session_ids:", ", ".join(missing[:10]) + (" ..." if len(missing) > 10 else ""))
67
+
68
+ # Threshold check
69
+ qualifying = {sid: r for sid, r in outcomes.items() if r >= min_reward}
70
+ print(f"Sessions with total_reward >= {min_reward}: {len(qualifying)}")
71
+
72
+ # Show a small comparison snapshot
73
+ sample = list(qualifying.items())[:5]
74
+ for sid, tot in sample:
75
+ er = event_sums.get(sid, 0.0)
76
+ print(f" {sid}: outcome={tot}, sum(event_rewards)={er:.2f}")
77
+
78
+ # Exit non-zero if any sessions are missing outcome rewards
79
+ if missing:
80
+ return 2
81
+ if min_reward > 0 and not qualifying:
82
+ return 3
83
+ return 0
84
+ finally:
85
+ await mgr.close()
86
+
87
+
88
+ def main() -> int:
89
+ ap = argparse.ArgumentParser(description="Verify reward persistence in traces DB")
90
+ ap.add_argument("--db", required=True, help="Path to traces SQLite DB (aiosqlite)")
91
+ ap.add_argument("--min-reward", type=int, default=0, help="Minimum total_reward to consider qualifying")
92
+ args = ap.parse_args()
93
+
94
+ return asyncio.run(verify(args.db, args.min_reward))
95
+
96
+
97
+ if __name__ == "__main__":
98
+ raise SystemExit(main())
99
+
100
+
@@ -0,0 +1,10 @@
1
+ from .validators import validate_task_app_url
2
+ from .health import task_app_health
3
+ from .contracts import TaskAppContract, TaskAppEndpoints
4
+
5
+ __all__ = [
6
+ "validate_task_app_url",
7
+ "task_app_health",
8
+ "TaskAppContract",
9
+ "TaskAppEndpoints",
10
+ ]
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Any, Dict, List
5
+ from pydantic import BaseModel
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class TaskAppEndpoints:
10
+ """Canonical Task App endpoint shapes used by RL trainers.
11
+
12
+ The Task App is an HTTP service (often deployed on Modal) that exposes:
13
+ - Health: GET /health
14
+ • Requires header X-API-Key (when ENVIRONMENT_API_KEY is configured)
15
+ • Returns { healthy: true }
16
+ - Environment lifecycle:
17
+ • POST /env/{env_name}/initialize → { env_id, observation }
18
+ • POST /env/{env_name}/step → { observation, reward, done, info }
19
+ • POST /env/{env_name}/terminate → { ok: true }
20
+ - Rollout (optional, unified schema):
21
+ • POST /rollout → { run_id, trajectories[], metrics, ... }
22
+ - Proxy (optional):
23
+ • POST /proxy/v1/chat/completions (for direct OpenAI calls from Task App)
24
+ """
25
+
26
+ health: str = "/health"
27
+ rollout: str = "/rollout"
28
+ proxy_chat_completions: str = "/proxy/v1/chat/completions"
29
+ env_initialize: str = "/env/{env_name}/initialize"
30
+ env_step: str = "/env/{env_name}/step"
31
+ env_terminate: str = "/env/{env_name}/terminate"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class TaskAppContract:
36
+ """Requirements and expectations for a Task App used by RL trainers.
37
+
38
+ - Auth: ENVIRONMENT_API_KEY must be set in the Task App environment; requests include X-API-Key.
39
+ - Health: /health returns 200 and JSON; may verify X-API-Key header.
40
+ - Env API: initialize/step/terminate are present for the target env (e.g., CrafterClassic).
41
+ - Rollout API: optional; provides a single-call rollout for convenience/testing.
42
+ - Inference routing: policy config passes an inference_url (Synth backend or OpenAI proxy).
43
+ - URL: base must be reachable via HTTPS and should be under .modal.run in production.
44
+ """
45
+
46
+ base_url: str
47
+ env_name: Optional[str] = None
48
+ requires_api_key_header: bool = True
49
+
50
+
51
+ # --- Unified rollout schema used by Task App services and SDK utilities ---
52
+
53
+ class RolloutEnvSpec(BaseModel):
54
+ env_id: Optional[str] = None
55
+ env_name: Optional[str] = None
56
+ config: Dict[str, Any] = {}
57
+ seed: Optional[int] = None
58
+
59
+
60
+ class RolloutPolicySpec(BaseModel):
61
+ policy_id: Optional[str] = None
62
+ policy_name: Optional[str] = None
63
+ config: Dict[str, Any] = {}
64
+
65
+
66
+ class RolloutRecordConfig(BaseModel):
67
+ trajectories: bool = True
68
+ logprobs: bool = False
69
+ value: bool = False
70
+
71
+
72
+ class RolloutSafetyConfig(BaseModel):
73
+ max_ops: int = 100000
74
+ max_time_s: float = 3600.0
75
+
76
+
77
+ class RolloutRequest(BaseModel):
78
+ run_id: str
79
+ env: RolloutEnvSpec
80
+ policy: RolloutPolicySpec
81
+ ops: List[Dict[str, Any]] | List[str]
82
+ record: RolloutRecordConfig = RolloutRecordConfig()
83
+ on_done: str = "reset"
84
+ safety: RolloutSafetyConfig = RolloutSafetyConfig()
85
+ training_session_id: Optional[str] = None
86
+ synth_base_url: Optional[str] = None
87
+
88
+
89
+ class RolloutStep(BaseModel):
90
+ obs: Dict[str, Any]
91
+ tool_calls: List[Dict[str, Any]]
92
+ reward: Optional[float] = None
93
+ done: bool = False
94
+ truncated: Optional[bool] = None
95
+ info: Optional[Dict[str, Any]] = None
96
+
97
+
98
+ class RolloutTrajectory(BaseModel):
99
+ env_id: str
100
+ policy_id: str
101
+ steps: List[RolloutStep]
102
+ final: Optional[Dict[str, Any]] = None
103
+ length: int
104
+
105
+
106
+ class RolloutMetrics(BaseModel):
107
+ episode_returns: List[float]
108
+ mean_return: float
109
+ num_steps: int
110
+ num_episodes: int = 0
111
+
112
+
113
+ class RolloutResponse(BaseModel):
114
+ run_id: str
115
+ trajectories: List[RolloutTrajectory]
116
+ branches: Dict[str, List[str]] = {}
117
+ metrics: RolloutMetrics
118
+ aborted: bool = False
119
+ ops_executed: int = 0
120
+
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+ import aiohttp
5
+
6
+
7
+ async def task_app_health(task_app_url: str) -> Dict[str, Any]:
8
+ """Probe a Task App base URL for basic reachability.
9
+
10
+ Behavior:
11
+ - Try HEAD first (follows redirects)
12
+ - Fallback to GET if HEAD is unsupported
13
+ - Returns {ok: bool, status?: int, error?: str}
14
+ """
15
+ try:
16
+ async with aiohttp.ClientSession() as session:
17
+ async with session.head(task_app_url, allow_redirects=True) as r:
18
+ if 200 <= r.status < 400:
19
+ return {"ok": True, "status": r.status}
20
+ async with aiohttp.ClientSession() as session:
21
+ async with session.get(task_app_url, allow_redirects=True) as r2:
22
+ if 200 <= r2.status < 400:
23
+ return {"ok": True, "status": r2.status}
24
+ return {"ok": False, "status": None}
25
+ except Exception as e:
26
+ return {"ok": False, "error": f"{type(e).__name__}: {e}"}
27
+
28
+
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import urlparse
4
+
5
+
6
+ def validate_task_app_url(url: str, *, name: str = "TASK_APP_BASE_URL") -> None:
7
+ """Validate a Task App base URL (scheme + host present)."""
8
+
9
+ p = urlparse(url)
10
+ if p.scheme not in ("http", "https") or not p.netloc:
11
+ raise ValueError(f"Invalid {name}: malformed: {url}")
12
+
@@ -1,18 +1,30 @@
1
- import sys as _sys
2
1
  import importlib as _importlib
2
+ import sys as _sys
3
3
 
4
- _pkg = _importlib.import_module('synth_ai.v0.tracing')
4
+ _pkg = _importlib.import_module("synth_ai.v0.tracing")
5
5
  _sys.modules[__name__] = _pkg
6
6
 
7
7
  _SUBMODULES = [
8
- 'abstractions', 'base_client', 'client_manager', 'config', 'context',
9
- 'decorators', 'immediate_client', 'local', 'log_client_base', 'retry_queue',
10
- 'trackers', 'upload', 'utils'
8
+ "abstractions",
9
+ "base_client",
10
+ "client_manager",
11
+ "config",
12
+ "context",
13
+ "decorators",
14
+ "immediate_client",
15
+ "local",
16
+ "log_client_base",
17
+ "retry_queue",
18
+ "trackers",
19
+ "upload",
20
+ "utils",
11
21
  ]
12
22
  for _m in _SUBMODULES:
13
- _sys.modules[f'{__name__}.{_m}'] = _importlib.import_module(f'synth_ai.v0.tracing.{_m}')
23
+ _sys.modules[f"{__name__}.{_m}"] = _importlib.import_module(f"synth_ai.v0.tracing.{_m}")
14
24
 
15
- _events_pkg = _importlib.import_module('synth_ai.v0.tracing.events')
16
- _sys.modules[f'{__name__}.events'] = _events_pkg
17
- for _m in ['manage', 'scope', 'store']:
18
- _sys.modules[f'{__name__}.events.{_m}'] = _importlib.import_module(f'synth_ai.v0.tracing.events.{_m}')
25
+ _events_pkg = _importlib.import_module("synth_ai.v0.tracing.events")
26
+ _sys.modules[f"{__name__}.events"] = _events_pkg
27
+ for _m in ["manage", "scope", "store"]:
28
+ _sys.modules[f"{__name__}.events.{_m}"] = _importlib.import_module(
29
+ f"synth_ai.v0.tracing.events.{_m}"
30
+ )
@@ -1,31 +1,33 @@
1
- import sys as _sys
2
1
  import importlib as _importlib
2
+ import sys as _sys
3
3
 
4
4
  # Forward top-level package
5
- _pkg = _importlib.import_module('synth_ai.v0.tracing_v1')
5
+ _pkg = _importlib.import_module("synth_ai.v0.tracing_v1")
6
6
  _sys.modules[__name__] = _pkg
7
7
 
8
8
  # Explicitly forward submodules so `synth_ai.tracing_v1.X` works
9
9
  _SUBMODULES = [
10
- 'abstractions',
11
- 'base_client',
12
- 'client_manager',
13
- 'config',
14
- 'context',
15
- 'decorators',
16
- 'immediate_client',
17
- 'local',
18
- 'log_client_base',
19
- 'retry_queue',
20
- 'trackers',
21
- 'upload',
22
- 'utils',
10
+ "abstractions",
11
+ "base_client",
12
+ "client_manager",
13
+ "config",
14
+ "context",
15
+ "decorators",
16
+ "immediate_client",
17
+ "local",
18
+ "log_client_base",
19
+ "retry_queue",
20
+ "trackers",
21
+ "upload",
22
+ "utils",
23
23
  ]
24
24
  for _m in _SUBMODULES:
25
- _sys.modules[f'{__name__}.{_m}'] = _importlib.import_module(f'synth_ai.v0.tracing_v1.{_m}')
25
+ _sys.modules[f"{__name__}.{_m}"] = _importlib.import_module(f"synth_ai.v0.tracing_v1.{_m}")
26
26
 
27
27
  # Forward events package and its submodules
28
- _events_pkg = _importlib.import_module('synth_ai.v0.tracing_v1.events')
29
- _sys.modules[f'{__name__}.events'] = _events_pkg
30
- for _m in ['manage', 'scope', 'store']:
31
- _sys.modules[f'{__name__}.events.{_m}'] = _importlib.import_module(f'synth_ai.v0.tracing_v1.events.{_m}')
28
+ _events_pkg = _importlib.import_module("synth_ai.v0.tracing_v1.events")
29
+ _sys.modules[f"{__name__}.events"] = _events_pkg
30
+ for _m in ["manage", "scope", "store"]:
31
+ _sys.modules[f"{__name__}.events.{_m}"] = _importlib.import_module(
32
+ f"synth_ai.v0.tracing_v1.events.{_m}"
33
+ )
@@ -1,7 +1,7 @@
1
1
  """Tracing v3 - Turso/sqld based tracing implementation.
2
2
 
3
3
  This module provides a modern, async-first tracing system for capturing and storing
4
- detailed execution traces from AI systems. It's designed to handle high-throughput
4
+ detailed execution traces from AI systems. It's designed to handle high-throughput
5
5
  scenarios with proper async/await patterns throughout.
6
6
 
7
7
  Architecture Overview:
@@ -49,10 +49,10 @@ Key Features:
49
49
  Usage Example:
50
50
  -------------
51
51
  from synth_ai.tracing_v3 import SessionTracer
52
-
52
+
53
53
  tracer = SessionTracer()
54
54
  await tracer.initialize()
55
-
55
+
56
56
  async with tracer.session() as session_id:
57
57
  async with tracer.timestep("step1", turn_number=1):
58
58
  # Record events during execution
@@ -70,17 +70,17 @@ The system uses environment variables for configuration:
70
70
  See `config.py` for full configuration options.
71
71
  """
72
72
 
73
- from .session_tracer import SessionTracer
74
73
  from .abstractions import (
75
- SessionTrace,
76
- SessionTimeStep,
77
74
  BaseEvent,
78
- RuntimeEvent,
79
75
  EnvironmentEvent,
76
+ RuntimeEvent,
80
77
  SessionEventMarkovBlanketMessage,
78
+ SessionTimeStep,
79
+ SessionTrace,
81
80
  TimeRecord,
82
81
  )
83
82
  from .config import TursoConfig
83
+ from .session_tracer import SessionTracer
84
84
 
85
85
  __all__ = [
86
86
  "SessionTracer",