synth-ai 0.2.14__py3-none-any.whl → 0.2.16__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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (236) hide show
  1. examples/README.md +1 -0
  2. examples/multi_step/SFT_README.md +147 -0
  3. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +9 -9
  4. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  5. examples/multi_step/convert_traces_to_sft.py +84 -0
  6. examples/multi_step/run_sft_qwen30b.sh +45 -0
  7. examples/qwen_coder/configs/coder_lora_30b.toml +2 -1
  8. examples/qwen_coder/configs/coder_lora_4b.toml +2 -1
  9. examples/qwen_coder/configs/coder_lora_small.toml +2 -1
  10. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  11. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  12. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  13. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  14. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  15. examples/qwen_vl/QUICKSTART.md +327 -0
  16. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  17. examples/qwen_vl/README.md +154 -0
  18. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  19. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  20. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  21. examples/qwen_vl/SETUP_COMPLETE.md +275 -0
  22. examples/qwen_vl/VISION_TESTS_COMPLETE.md +490 -0
  23. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  24. examples/qwen_vl/__init__.py +2 -0
  25. examples/qwen_vl/collect_data_via_cli.md +423 -0
  26. examples/qwen_vl/collect_vision_traces.py +368 -0
  27. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +127 -0
  28. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +60 -0
  29. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +43 -0
  30. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  31. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +45 -0
  32. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +44 -0
  33. examples/qwen_vl/configs/filter_qwen2vl_sft.toml +50 -0
  34. examples/qwen_vl/configs/filter_vision_sft.toml +53 -0
  35. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  36. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  37. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  38. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  39. examples/qwen_vl/run_vision_comparison.sh +62 -0
  40. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  41. examples/qwen_vl/test_image_validation.py +201 -0
  42. examples/qwen_vl/test_sft_vision_data.py +110 -0
  43. examples/rl/README.md +1 -1
  44. examples/rl/configs/eval_base_qwen.toml +17 -0
  45. examples/rl/configs/eval_rl_qwen.toml +13 -0
  46. examples/rl/configs/rl_from_base_qwen.toml +37 -0
  47. examples/rl/configs/rl_from_base_qwen17.toml +76 -0
  48. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  49. examples/rl/run_eval.py +436 -0
  50. examples/rl/run_rl_and_save.py +111 -0
  51. examples/rl/task_app/README.md +22 -0
  52. examples/rl/task_app/math_single_step.py +990 -0
  53. examples/rl/task_app/math_task_app.py +111 -0
  54. examples/sft/README.md +5 -5
  55. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -2
  56. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -3
  57. examples/sft/evaluate.py +2 -4
  58. examples/sft/export_dataset.py +7 -4
  59. examples/swe/task_app/README.md +1 -1
  60. examples/swe/task_app/grpo_swe_mini.py +0 -1
  61. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
  62. examples/swe/task_app/hosted/envs/mini_swe/environment.py +13 -13
  63. examples/swe/task_app/hosted/policy_routes.py +0 -2
  64. examples/swe/task_app/hosted/rollout.py +0 -8
  65. examples/task_apps/crafter/task_app/grpo_crafter.py +4 -7
  66. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +59 -1
  67. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +30 -0
  68. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +62 -31
  69. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +16 -14
  70. examples/task_apps/enron/__init__.py +1 -0
  71. examples/vlm/README.md +3 -3
  72. examples/vlm/configs/crafter_vlm_gpt4o.toml +2 -0
  73. examples/vlm/crafter_openai_vlm_agent.py +3 -5
  74. examples/vlm/filter_image_rows.py +1 -1
  75. examples/vlm/run_crafter_vlm_benchmark.py +2 -2
  76. examples/warming_up_to_rl/_utils.py +92 -0
  77. examples/warming_up_to_rl/analyze_trace_db.py +1 -1
  78. examples/warming_up_to_rl/configs/crafter_fft.toml +2 -0
  79. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +2 -0
  80. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
  81. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
  82. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
  83. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
  84. examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
  85. examples/warming_up_to_rl/export_trace_sft.py +174 -60
  86. examples/warming_up_to_rl/readme.md +63 -132
  87. examples/warming_up_to_rl/run_fft_and_save.py +1 -1
  88. examples/warming_up_to_rl/run_rl_and_save.py +1 -1
  89. examples/warming_up_to_rl/task_app/README.md +42 -0
  90. examples/warming_up_to_rl/task_app/grpo_crafter.py +696 -0
  91. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  92. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  93. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  94. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  95. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  96. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  97. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  98. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  99. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  100. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +478 -0
  101. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  102. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  103. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  104. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +204 -0
  105. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  106. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +618 -0
  107. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  108. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1081 -0
  109. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  110. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1861 -0
  111. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  112. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  113. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  114. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  115. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +62 -0
  116. synth_ai/__init__.py +44 -30
  117. synth_ai/_utils/__init__.py +47 -0
  118. synth_ai/_utils/base_url.py +10 -0
  119. synth_ai/_utils/http.py +10 -0
  120. synth_ai/_utils/prompts.py +10 -0
  121. synth_ai/_utils/task_app_state.py +12 -0
  122. synth_ai/_utils/user_config.py +10 -0
  123. synth_ai/api/models/supported.py +144 -7
  124. synth_ai/api/train/__init__.py +13 -1
  125. synth_ai/api/train/cli.py +30 -7
  126. synth_ai/api/train/config_finder.py +18 -11
  127. synth_ai/api/train/env_resolver.py +13 -10
  128. synth_ai/cli/__init__.py +62 -78
  129. synth_ai/cli/_modal_wrapper.py +7 -5
  130. synth_ai/cli/_typer_patch.py +0 -2
  131. synth_ai/cli/_validate_task_app.py +22 -4
  132. synth_ai/cli/legacy_root_backup.py +3 -1
  133. synth_ai/cli/lib/__init__.py +10 -0
  134. synth_ai/cli/lib/task_app_discovery.py +7 -0
  135. synth_ai/cli/lib/task_app_env.py +518 -0
  136. synth_ai/cli/recent.py +2 -1
  137. synth_ai/cli/setup.py +266 -0
  138. synth_ai/cli/status.py +1 -1
  139. synth_ai/cli/task_app_deploy.py +16 -0
  140. synth_ai/cli/task_app_list.py +25 -0
  141. synth_ai/cli/task_app_modal_serve.py +16 -0
  142. synth_ai/cli/task_app_serve.py +18 -0
  143. synth_ai/cli/task_apps.py +71 -31
  144. synth_ai/cli/traces.py +1 -1
  145. synth_ai/cli/train.py +18 -0
  146. synth_ai/cli/tui.py +7 -2
  147. synth_ai/cli/turso.py +1 -1
  148. synth_ai/cli/watch.py +1 -1
  149. synth_ai/demos/__init__.py +10 -0
  150. synth_ai/demos/core/__init__.py +28 -1
  151. synth_ai/demos/crafter/__init__.py +1 -0
  152. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  153. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  154. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  155. synth_ai/demos/demo_registry.py +176 -0
  156. synth_ai/demos/math/__init__.py +1 -0
  157. synth_ai/demos/math/_common.py +16 -0
  158. synth_ai/demos/math/app.py +38 -0
  159. synth_ai/demos/math/config.toml +76 -0
  160. synth_ai/demos/math/deploy_modal.py +54 -0
  161. synth_ai/demos/math/modal_task_app.py +702 -0
  162. synth_ai/demos/math/task_app_entry.py +51 -0
  163. synth_ai/environments/environment/core.py +7 -1
  164. synth_ai/environments/examples/bandit/engine.py +0 -1
  165. synth_ai/environments/examples/bandit/environment.py +0 -1
  166. synth_ai/environments/examples/wordle/environment.py +0 -1
  167. synth_ai/evals/base.py +16 -5
  168. synth_ai/evals/client.py +1 -1
  169. synth_ai/inference/client.py +1 -1
  170. synth_ai/judge_schemas.py +8 -8
  171. synth_ai/learning/client.py +1 -1
  172. synth_ai/learning/health.py +1 -1
  173. synth_ai/learning/jobs.py +1 -1
  174. synth_ai/learning/rl/client.py +1 -1
  175. synth_ai/learning/rl/env_keys.py +1 -1
  176. synth_ai/learning/rl/secrets.py +1 -1
  177. synth_ai/learning/sft/client.py +1 -1
  178. synth_ai/learning/sft/data.py +407 -4
  179. synth_ai/learning/validators.py +4 -1
  180. synth_ai/task/apps/__init__.py +4 -2
  181. synth_ai/task/config.py +6 -4
  182. synth_ai/task/rubrics/__init__.py +1 -2
  183. synth_ai/task/rubrics/loaders.py +14 -10
  184. synth_ai/task/rubrics.py +219 -0
  185. synth_ai/task/trace_correlation_helpers.py +24 -11
  186. synth_ai/task/tracing_utils.py +14 -3
  187. synth_ai/task/validators.py +2 -3
  188. synth_ai/tracing_v3/abstractions.py +3 -3
  189. synth_ai/tracing_v3/config.py +15 -13
  190. synth_ai/tracing_v3/constants.py +21 -0
  191. synth_ai/tracing_v3/db_config.py +3 -1
  192. synth_ai/tracing_v3/decorators.py +10 -7
  193. synth_ai/tracing_v3/llm_call_record_helpers.py +5 -5
  194. synth_ai/tracing_v3/session_tracer.py +7 -7
  195. synth_ai/tracing_v3/storage/base.py +29 -29
  196. synth_ai/tracing_v3/storage/config.py +3 -3
  197. synth_ai/tracing_v3/turso/daemon.py +8 -9
  198. synth_ai/tracing_v3/turso/native_manager.py +80 -72
  199. synth_ai/tracing_v3/utils.py +2 -2
  200. synth_ai/tui/cli/query_experiments.py +4 -4
  201. synth_ai/tui/cli/query_experiments_v3.py +4 -4
  202. synth_ai/tui/dashboard.py +14 -9
  203. synth_ai/utils/__init__.py +101 -0
  204. synth_ai/utils/base_url.py +94 -0
  205. synth_ai/utils/cli.py +131 -0
  206. synth_ai/utils/env.py +287 -0
  207. synth_ai/utils/http.py +169 -0
  208. synth_ai/utils/modal.py +308 -0
  209. synth_ai/utils/process.py +212 -0
  210. synth_ai/utils/prompts.py +39 -0
  211. synth_ai/utils/sqld.py +122 -0
  212. synth_ai/utils/task_app_discovery.py +882 -0
  213. synth_ai/utils/task_app_env.py +186 -0
  214. synth_ai/utils/task_app_state.py +318 -0
  215. synth_ai/utils/user_config.py +137 -0
  216. synth_ai/v0/config/__init__.py +1 -5
  217. synth_ai/v0/config/base_url.py +1 -7
  218. synth_ai/v0/tracing/config.py +1 -1
  219. synth_ai/v0/tracing/decorators.py +1 -1
  220. synth_ai/v0/tracing/upload.py +1 -1
  221. synth_ai/v0/tracing_v1/config.py +1 -1
  222. synth_ai/v0/tracing_v1/decorators.py +1 -1
  223. synth_ai/v0/tracing_v1/upload.py +1 -1
  224. {synth_ai-0.2.14.dist-info → synth_ai-0.2.16.dist-info}/METADATA +85 -31
  225. {synth_ai-0.2.14.dist-info → synth_ai-0.2.16.dist-info}/RECORD +229 -117
  226. synth_ai/cli/man.py +0 -106
  227. synth_ai/compound/cais.py +0 -0
  228. synth_ai/core/experiment.py +0 -13
  229. synth_ai/core/system.py +0 -15
  230. synth_ai/demo_registry.py +0 -295
  231. synth_ai/handshake.py +0 -109
  232. synth_ai/http.py +0 -26
  233. {synth_ai-0.2.14.dist-info → synth_ai-0.2.16.dist-info}/WHEEL +0 -0
  234. {synth_ai-0.2.14.dist-info → synth_ai-0.2.16.dist-info}/entry_points.txt +0 -0
  235. {synth_ai-0.2.14.dist-info → synth_ai-0.2.16.dist-info}/licenses/LICENSE +0 -0
  236. {synth_ai-0.2.14.dist-info → synth_ai-0.2.16.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from datetime import datetime
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  from ..abstractions import SessionTrace
8
8
 
@@ -28,7 +28,7 @@ class TraceStorage(ABC):
28
28
  pass
29
29
 
30
30
  @abstractmethod
31
- async def get_session_trace(self, session_id: str) -> Optional[dict[str, Any]]:
31
+ async def get_session_trace(self, session_id: str) -> dict[str, Any] | None:
32
32
  """Retrieve a session trace by ID.
33
33
 
34
34
  Args:
@@ -40,7 +40,7 @@ class TraceStorage(ABC):
40
40
  pass
41
41
 
42
42
  @abstractmethod
43
- async def query_traces(self, query: str, params: Optional[dict[str, Any]] = None) -> Any:
43
+ async def query_traces(self, query: str, params: dict[str, Any] | None = None) -> Any:
44
44
  """Execute a query and return results.
45
45
 
46
46
  Args:
@@ -55,9 +55,9 @@ class TraceStorage(ABC):
55
55
  @abstractmethod
56
56
  async def get_model_usage(
57
57
  self,
58
- start_date: Optional[datetime] = None,
59
- end_date: Optional[datetime] = None,
60
- model_name: Optional[str] = None,
58
+ start_date: datetime | None = None,
59
+ end_date: datetime | None = None,
60
+ model_name: str | None = None,
61
61
  ) -> Any:
62
62
  """Get model usage statistics.
63
63
 
@@ -95,8 +95,8 @@ class TraceStorage(ABC):
95
95
  self,
96
96
  session_id: str,
97
97
  *,
98
- created_at: Optional[datetime] = None,
99
- metadata: Optional[dict[str, Any]] = None,
98
+ created_at: datetime | None = None,
99
+ metadata: dict[str, Any] | None = None,
100
100
  ) -> None:
101
101
  """Ensure a session row exists for the given session id."""
102
102
  pass
@@ -108,10 +108,10 @@ class TraceStorage(ABC):
108
108
  *,
109
109
  step_id: str,
110
110
  step_index: int,
111
- turn_number: Optional[int] = None,
112
- started_at: Optional[datetime] = None,
113
- completed_at: Optional[datetime] = None,
114
- metadata: Optional[dict[str, Any]] = None,
111
+ turn_number: int | None = None,
112
+ started_at: datetime | None = None,
113
+ completed_at: datetime | None = None,
114
+ metadata: dict[str, Any] | None = None,
115
115
  ) -> int:
116
116
  """Ensure a timestep row exists and return its database id."""
117
117
  pass
@@ -121,9 +121,9 @@ class TraceStorage(ABC):
121
121
  self,
122
122
  session_id: str,
123
123
  *,
124
- timestep_db_id: Optional[int],
124
+ timestep_db_id: int | None,
125
125
  event: Any,
126
- metadata_override: Optional[dict[str, Any]] = None,
126
+ metadata_override: dict[str, Any] | None = None,
127
127
  ) -> int:
128
128
  """Insert an event and return its database id."""
129
129
  pass
@@ -133,12 +133,12 @@ class TraceStorage(ABC):
133
133
  self,
134
134
  session_id: str,
135
135
  *,
136
- timestep_db_id: Optional[int],
136
+ timestep_db_id: int | None,
137
137
  message_type: str,
138
138
  content: Any,
139
- event_time: Optional[float] = None,
140
- message_time: Optional[int] = None,
141
- metadata: Optional[dict[str, Any]] = None,
139
+ event_time: float | None = None,
140
+ message_time: int | None = None,
141
+ metadata: dict[str, Any] | None = None,
142
142
  ) -> int:
143
143
  """Insert a message row linked to a session/timestep."""
144
144
  pass
@@ -151,7 +151,7 @@ class TraceStorage(ABC):
151
151
  total_reward: int,
152
152
  achievements_count: int,
153
153
  total_steps: int,
154
- reward_metadata: Optional[dict] = None,
154
+ reward_metadata: dict | None = None,
155
155
  ) -> int:
156
156
  """Record an outcome reward for a session."""
157
157
  pass
@@ -162,13 +162,13 @@ class TraceStorage(ABC):
162
162
  session_id: str,
163
163
  *,
164
164
  event_id: int,
165
- message_id: Optional[int] = None,
166
- turn_number: Optional[int] = None,
165
+ message_id: int | None = None,
166
+ turn_number: int | None = None,
167
167
  reward_value: float = 0.0,
168
- reward_type: Optional[str] = None,
169
- key: Optional[str] = None,
170
- annotation: Optional[dict[str, Any]] = None,
171
- source: Optional[str] = None,
168
+ reward_type: str | None = None,
169
+ key: str | None = None,
170
+ annotation: dict[str, Any] | None = None,
171
+ source: str | None = None,
172
172
  ) -> int:
173
173
  """Record a reward tied to a specific event."""
174
174
  pass
@@ -178,8 +178,8 @@ class TraceStorage(ABC):
178
178
  self,
179
179
  experiment_id: str,
180
180
  name: str,
181
- description: Optional[str] = None,
182
- configuration: Optional[dict[str, Any]] = None,
181
+ description: str | None = None,
182
+ configuration: dict[str, Any] | None = None,
183
183
  ) -> str:
184
184
  """Create a new experiment."""
185
185
  raise NotImplementedError("Experiment management not supported by this backend")
@@ -189,14 +189,14 @@ class TraceStorage(ABC):
189
189
  raise NotImplementedError("Experiment management not supported by this backend")
190
190
 
191
191
  async def get_sessions_by_experiment(
192
- self, experiment_id: str, limit: Optional[int] = None
192
+ self, experiment_id: str, limit: int | None = None
193
193
  ) -> list[dict[str, Any]]:
194
194
  """Get all sessions for an experiment."""
195
195
  raise NotImplementedError("Experiment management not supported by this backend")
196
196
 
197
197
  # Batch operations
198
198
  async def batch_insert_sessions(
199
- self, traces: list[SessionTrace], batch_size: Optional[int] = 1000
199
+ self, traces: list[SessionTrace], batch_size: int | None = 1000
200
200
  ) -> list[str]:
201
201
  """Batch insert multiple session traces.
202
202
 
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  from dataclasses import dataclass
5
5
  from enum import Enum
6
- from typing import Any, Optional
6
+ from typing import Any
7
7
 
8
8
 
9
9
  class StorageBackend(str, Enum):
@@ -14,7 +14,7 @@ class StorageBackend(str, Enum):
14
14
  POSTGRES = "postgres" # Future support
15
15
 
16
16
 
17
- def _is_enabled(value: Optional[str]) -> bool:
17
+ def _is_enabled(value: str | None) -> bool:
18
18
  if value is None:
19
19
  return False
20
20
  return value.lower() in {"1", "true", "yes", "on"}
@@ -25,7 +25,7 @@ class StorageConfig:
25
25
  """Configuration for storage backend."""
26
26
 
27
27
  backend: StorageBackend = StorageBackend.TURSO_NATIVE
28
- connection_string: Optional[str] = None
28
+ connection_string: str | None = None
29
29
 
30
30
  # Turso-specific settings
31
31
  turso_url: str = os.getenv("TURSO_DATABASE_URL", "sqlite+libsql://http://127.0.0.1:8080")
@@ -7,7 +7,6 @@ import time
7
7
 
8
8
  import requests
9
9
  from requests import RequestException
10
- from typing import Any, Optional
11
10
 
12
11
  from ..config import CONFIG
13
12
 
@@ -17,9 +16,9 @@ class SqldDaemon:
17
16
 
18
17
  def __init__(
19
18
  self,
20
- db_path: Optional[str] = None,
21
- http_port: Optional[int] = None,
22
- binary_path: Optional[str] = None,
19
+ db_path: str | None = None,
20
+ http_port: int | None = None,
21
+ binary_path: str | None = None,
23
22
  ):
24
23
  """Initialize sqld daemon manager.
25
24
 
@@ -31,7 +30,7 @@ class SqldDaemon:
31
30
  self.db_path = db_path or CONFIG.sqld_db_path
32
31
  self.http_port = http_port or CONFIG.sqld_http_port
33
32
  self.binary_path = binary_path or self._find_binary()
34
- self.process: Optional[Any] = None
33
+ self.process: subprocess.Popen[str] | None = None
35
34
 
36
35
  def _find_binary(self) -> str:
37
36
  """Find sqld binary in PATH."""
@@ -85,7 +84,7 @@ class SqldDaemon:
85
84
  pass
86
85
 
87
86
  # Check if process crashed
88
- if self.process.poll() is not None:
87
+ if self.process and self.process.poll() is not None:
89
88
  stdout, stderr = self.process.communicate()
90
89
  raise RuntimeError(
91
90
  f"sqld daemon failed to start:\nstdout: {stdout}\nstderr: {stderr}"
@@ -124,10 +123,10 @@ class SqldDaemon:
124
123
 
125
124
 
126
125
  # Convenience functions
127
- _daemon: Optional[SqldDaemon] = None
126
+ _daemon: SqldDaemon | None = None
128
127
 
129
128
 
130
- def start_sqld(db_path: Optional[str] = None, port: Optional[int] = None) -> SqldDaemon:
129
+ def start_sqld(db_path: str | None = None, port: int | None = None) -> SqldDaemon:
131
130
  """Start a global sqld daemon instance."""
132
131
  global _daemon
133
132
  if _daemon and _daemon.is_running():
@@ -146,6 +145,6 @@ def stop_sqld():
146
145
  _daemon = None
147
146
 
148
147
 
149
- def get_daemon() -> Optional[SqldDaemon]:
148
+ def get_daemon() -> SqldDaemon | None:
150
149
  """Get the global daemon instance."""
151
150
  return _daemon
@@ -13,7 +13,7 @@ import logging
13
13
  import re
14
14
  from collections.abc import Callable
15
15
  from dataclasses import asdict, dataclass
16
- from datetime import datetime, timezone
16
+ from datetime import UTC, datetime
17
17
  from typing import TYPE_CHECKING, Any, cast
18
18
 
19
19
  import libsql
@@ -378,8 +378,10 @@ class NativeLibsqlTraceManager(TraceStorage):
378
378
  session_exists = await self._session_exists(trace.session_id)
379
379
  _logger.info(f"[TRACE_DEBUG] Session exists: {session_exists}")
380
380
 
381
+ step_id_map: dict[str, int] = {}
382
+
381
383
  if session_exists:
382
- _logger.warning(f"[TRACE_DEBUG] Session {trace.session_id} already exists, need to save messages anyway!")
384
+ _logger.warning(f"[TRACE_DEBUG] Session {trace.session_id} already exists, skipping events/timesteps, only updating messages!")
383
385
  # Don't return early - we need to save messages!
384
386
  # Just update metadata
385
387
  async with self._op_lock:
@@ -390,10 +392,9 @@ class NativeLibsqlTraceManager(TraceStorage):
390
392
  (_json_dumps(trace.metadata or {}), trace.session_id),
391
393
  )
392
394
  conn.commit()
393
- # Continue to save messages instead of returning
394
-
395
- if not session_exists:
396
- created_at = trace.created_at or datetime.now(timezone.utc)
395
+ # Skip events and timesteps to ensure idempotency
396
+ else:
397
+ created_at = trace.created_at or datetime.now(UTC)
397
398
 
398
399
  async with self._op_lock:
399
400
  conn = self._conn
@@ -417,73 +418,76 @@ class NativeLibsqlTraceManager(TraceStorage):
417
418
  ),
418
419
  )
419
420
  conn.commit()
420
- _logger.info(f"[TRACE_DEBUG] Session row inserted")
421
-
422
- step_id_map: dict[str, int] = {}
421
+ _logger.info("[TRACE_DEBUG] Session row inserted")
423
422
 
424
- for step in trace.session_time_steps:
425
- step_db_id = await self.ensure_timestep(
426
- trace.session_id,
427
- step_id=step.step_id,
428
- step_index=step.step_index,
429
- turn_number=step.turn_number,
430
- started_at=step.timestamp,
431
- completed_at=step.completed_at,
432
- metadata=step.step_metadata or {},
433
- )
434
- step_id_map[step.step_id] = step_db_id
435
-
436
- for event in trace.event_history:
437
- step_ref = None
438
- metadata = event.metadata or {}
439
- if isinstance(metadata, dict):
440
- step_ref = metadata.get("step_id")
441
- timestep_db_id = step_id_map.get(step_ref) if step_ref else None
442
- await self.insert_event_row(
443
- trace.session_id,
444
- timestep_db_id=timestep_db_id,
445
- event=event,
446
- metadata_override=event.metadata or {},
447
- )
423
+ # Only insert timesteps and events if this is a new session
424
+ for step in trace.session_time_steps:
425
+ step_db_id = await self.ensure_timestep(
426
+ trace.session_id,
427
+ step_id=step.step_id,
428
+ step_index=step.step_index,
429
+ turn_number=step.turn_number,
430
+ started_at=step.timestamp,
431
+ completed_at=step.completed_at,
432
+ metadata=step.step_metadata or {},
433
+ )
434
+ step_id_map[step.step_id] = step_db_id
435
+
436
+ for event in trace.event_history:
437
+ step_ref = None
438
+ metadata = event.metadata or {}
439
+ if isinstance(metadata, dict):
440
+ step_ref = metadata.get("step_id")
441
+ timestep_db_id = step_id_map.get(step_ref) if step_ref else None
442
+ await self.insert_event_row(
443
+ trace.session_id,
444
+ timestep_db_id=timestep_db_id,
445
+ event=event,
446
+ metadata_override=event.metadata or {},
447
+ )
448
448
 
449
449
  import logging as _logging
450
450
  _logger = _logging.getLogger(__name__)
451
- _logger.info(f"[TRACE_DEBUG] insert_session_trace: saving {len(trace.markov_blanket_message_history)} messages")
451
+ _logger.info(f"[TRACE_DEBUG] insert_session_trace: saving {len(trace.markov_blanket_message_history)} messages (session_exists={session_exists})")
452
452
 
453
- for idx, msg in enumerate(trace.markov_blanket_message_history):
454
- metadata = dict(getattr(msg, "metadata", {}) or {})
455
- step_ref = metadata.get("step_id")
456
- content_value = msg.content
457
- if isinstance(msg.content, SessionMessageContent):
458
- if msg.content.json_payload:
459
- metadata.setdefault("json_payload", msg.content.json_payload)
460
- content_value = msg.content.json_payload
461
- else:
462
- content_value = msg.content.as_text()
463
- if msg.content.text:
464
- metadata.setdefault("text", msg.content.text)
465
- elif not isinstance(content_value, str):
453
+ # Only insert messages if this is a new session (for idempotency)
454
+ if not session_exists:
455
+ for idx, msg in enumerate(trace.markov_blanket_message_history):
456
+ metadata = dict(getattr(msg, "metadata", {}) or {})
457
+ step_ref = metadata.get("step_id")
458
+ content_value = msg.content
459
+ if isinstance(msg.content, SessionMessageContent):
460
+ if msg.content.json_payload:
461
+ metadata.setdefault("json_payload", msg.content.json_payload)
462
+ content_value = msg.content.json_payload
463
+ else:
464
+ content_value = msg.content.as_text()
465
+ if msg.content.text:
466
+ metadata.setdefault("text", msg.content.text)
467
+ elif not isinstance(content_value, str):
468
+ try:
469
+ content_value = json.dumps(content_value, ensure_ascii=False)
470
+ except (TypeError, ValueError):
471
+ content_value = str(content_value)
472
+
473
+ _logger.info(f"[TRACE_DEBUG] Message {idx+1}: type={msg.message_type}, content_len={len(str(content_value))}")
474
+
466
475
  try:
467
- content_value = json.dumps(content_value, ensure_ascii=False)
468
- except (TypeError, ValueError):
469
- content_value = str(content_value)
470
-
471
- _logger.info(f"[TRACE_DEBUG] Message {idx+1}: type={msg.message_type}, content_len={len(str(content_value))}")
472
-
473
- try:
474
- await self.insert_message_row(
475
- trace.session_id,
476
- timestep_db_id=step_id_map.get(step_ref) if step_ref else None,
477
- message_type=msg.message_type,
478
- content=content_value,
479
- event_time=msg.time_record.event_time,
480
- message_time=msg.time_record.message_time,
481
- metadata=metadata,
482
- )
483
- _logger.info(f"[TRACE_DEBUG] Message {idx+1}: saved successfully")
484
- except Exception as exc:
485
- _logger.error(f"[TRACE_DEBUG] Message {idx+1}: FAILED TO SAVE: {exc}", exc_info=True)
486
- raise
476
+ await self.insert_message_row(
477
+ trace.session_id,
478
+ timestep_db_id=step_id_map.get(step_ref) if step_ref else None,
479
+ message_type=msg.message_type,
480
+ content=content_value,
481
+ event_time=msg.time_record.event_time,
482
+ message_time=msg.time_record.message_time,
483
+ metadata=metadata,
484
+ )
485
+ _logger.info(f"[TRACE_DEBUG] Message {idx+1}: saved successfully")
486
+ except Exception as exc:
487
+ _logger.error(f"[TRACE_DEBUG] Message {idx+1}: FAILED TO SAVE: {exc}", exc_info=True)
488
+ raise
489
+ else:
490
+ _logger.info("[TRACE_DEBUG] Skipping message insertion for existing session (idempotency)")
487
491
 
488
492
  async with self._op_lock:
489
493
  conn = self._conn
@@ -806,7 +810,7 @@ class NativeLibsqlTraceManager(TraceStorage):
806
810
  ) -> None:
807
811
  await self.initialize()
808
812
 
809
- created_at_val = (created_at or datetime.now(timezone.utc)).isoformat()
813
+ created_at_val = (created_at or datetime.now(UTC)).isoformat()
810
814
  metadata_json = _json_dumps(metadata or {})
811
815
 
812
816
  async with self._op_lock:
@@ -838,7 +842,7 @@ class NativeLibsqlTraceManager(TraceStorage):
838
842
  ) -> int:
839
843
  await self.initialize()
840
844
 
841
- started_at_val = (started_at or datetime.now(timezone.utc)).isoformat()
845
+ started_at_val = (started_at or datetime.now(UTC)).isoformat()
842
846
  completed_at_val = completed_at.isoformat() if completed_at else None
843
847
  metadata_json = _json_dumps(metadata or {})
844
848
 
@@ -927,7 +931,11 @@ class NativeLibsqlTraceManager(TraceStorage):
927
931
  if isinstance(event, LMCAISEvent):
928
932
  call_records = None
929
933
  if getattr(event, "call_records", None):
930
- call_records = [asdict(record) for record in event.call_records]
934
+ # Handle both dataclass instances and dicts (from deserialization)
935
+ call_records = [
936
+ asdict(record) if not isinstance(record, dict) else record
937
+ for record in event.call_records
938
+ ]
931
939
  payload.update(
932
940
  {
933
941
  "event_type": "cais",
@@ -1150,7 +1158,7 @@ class NativeLibsqlTraceManager(TraceStorage):
1150
1158
  total_reward,
1151
1159
  achievements_count,
1152
1160
  total_steps,
1153
- datetime.now(timezone.utc).isoformat(),
1161
+ datetime.now(UTC).isoformat(),
1154
1162
  _json_dumps(reward_metadata),
1155
1163
  ),
1156
1164
  )
@@ -1202,7 +1210,7 @@ class NativeLibsqlTraceManager(TraceStorage):
1202
1210
  key,
1203
1211
  _json_dumps(annotation),
1204
1212
  source,
1205
- datetime.now(timezone.utc).isoformat(),
1213
+ datetime.now(UTC).isoformat(),
1206
1214
  ),
1207
1215
  )
1208
1216
  conn.commit()
@@ -5,13 +5,13 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import json
7
7
  import uuid
8
- from datetime import datetime, timezone
8
+ from datetime import UTC, datetime
9
9
  from typing import Any
10
10
 
11
11
 
12
12
  def iso_now() -> str:
13
13
  """Get current timezone.utc time as ISO format string."""
14
- return datetime.now(timezone.utc).isoformat()
14
+ return datetime.now(UTC).isoformat()
15
15
 
16
16
 
17
17
  def json_dumps(obj: Any) -> str:
@@ -6,12 +6,12 @@ Query experiments and sessions from Turso/sqld using v3 tracing.
6
6
  import argparse
7
7
  import asyncio
8
8
 
9
- from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
9
+ from synth_ai.tracing_v3.turso import NativeLibsqlTraceManager
10
10
 
11
11
 
12
12
  async def list_experiments(db_url: str):
13
13
  """List all experiments in the database."""
14
- db = AsyncSQLTraceManager(db_url)
14
+ db = NativeLibsqlTraceManager(db_url)
15
15
  await db.initialize()
16
16
 
17
17
  try:
@@ -57,7 +57,7 @@ async def list_experiments(db_url: str):
57
57
 
58
58
  async def show_experiment_details(db_url: str, experiment_id: str):
59
59
  """Show detailed information about a specific experiment."""
60
- db = AsyncSQLTraceManager(db_url)
60
+ db = NativeLibsqlTraceManager(db_url)
61
61
  await db.initialize()
62
62
 
63
63
  try:
@@ -120,7 +120,7 @@ async def show_experiment_details(db_url: str, experiment_id: str):
120
120
 
121
121
  async def show_model_usage(db_url: str, model_name: str | None = None):
122
122
  """Show model usage statistics."""
123
- db = AsyncSQLTraceManager(db_url)
123
+ db = NativeLibsqlTraceManager(db_url)
124
124
  await db.initialize()
125
125
 
126
126
  try:
@@ -6,12 +6,12 @@ Query experiments and sessions from Turso/sqld using v3 tracing.
6
6
  import argparse
7
7
  import asyncio
8
8
 
9
- from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
9
+ from synth_ai.tracing_v3.turso import NativeLibsqlTraceManager
10
10
 
11
11
 
12
12
  async def list_experiments(db_url: str):
13
13
  """List all experiments in the database."""
14
- db = AsyncSQLTraceManager(db_url)
14
+ db = NativeLibsqlTraceManager(db_url)
15
15
  await db.initialize()
16
16
 
17
17
  try:
@@ -57,7 +57,7 @@ async def list_experiments(db_url: str):
57
57
 
58
58
  async def show_experiment_details(db_url: str, experiment_id: str):
59
59
  """Show detailed information about a specific experiment."""
60
- db = AsyncSQLTraceManager(db_url)
60
+ db = NativeLibsqlTraceManager(db_url)
61
61
  await db.initialize()
62
62
 
63
63
  try:
@@ -120,7 +120,7 @@ async def show_experiment_details(db_url: str, experiment_id: str):
120
120
 
121
121
  async def show_model_usage(db_url: str, model_name: str | None = None):
122
122
  """Show model usage statistics."""
123
- db = AsyncSQLTraceManager(db_url)
123
+ db = NativeLibsqlTraceManager(db_url)
124
124
  await db.initialize()
125
125
 
126
126
  try:
synth_ai/tui/dashboard.py CHANGED
@@ -32,7 +32,10 @@ except (ImportError, ModuleNotFoundError):
32
32
  ComposeResult = object # type: ignore
33
33
  Binding = object # type: ignore
34
34
  Container = object # type: ignore
35
- reactive = lambda x: x # type: ignore
35
+
36
+ def reactive(value, *_, **__):
37
+ return value
38
+
36
39
  Timer = object # type: ignore
37
40
  DataTable = object # type: ignore
38
41
  Footer = object # type: ignore
@@ -42,17 +45,19 @@ except (ImportError, ModuleNotFoundError):
42
45
 
43
46
  # Import database manager with graceful fallback
44
47
  try:
45
- from synth_ai.tracing_v3.turso.native_manager import NativeLibsqlTraceManager # type: ignore[import-untyped]
48
+ from synth_ai.tracing_v3.turso.native_manager import (
49
+ NativeLibsqlTraceManager, # type: ignore[import-untyped]
50
+ )
46
51
  _DB_AVAILABLE = True
47
52
  except (ImportError, ModuleNotFoundError, TypeError):
48
53
  # Database manager not available - provide dummy class
49
54
  NativeLibsqlTraceManager = object # type: ignore
50
55
  _DB_AVAILABLE = False
51
56
 
52
- import asyncio
53
- import requests
54
57
  from datetime import timedelta
55
58
 
59
+ import requests
60
+
56
61
 
57
62
  class ExperimentRow:
58
63
  """Data structure for experiment display."""
@@ -218,8 +223,8 @@ class DatabaseStatus(Static):
218
223
  path_part = url.split("///")[-1]
219
224
  filename = Path(path_part).name
220
225
  self.connection_status = f"🟢 {filename}"
221
- except:
222
- self.connection_status = f"🟢 Connected"
226
+ except Exception:
227
+ self.connection_status = "🟢 Connected"
223
228
  else:
224
229
  host_info = f"{parsed.hostname}:{parsed.port}" if parsed.port else str(parsed.hostname)
225
230
  self.connection_status = f"🟢 {host_info}"
@@ -317,10 +322,10 @@ class BalanceStatus(Static):
317
322
 
318
323
  def set_global_error(self, error: str):
319
324
  """Show error state for global data."""
320
- self.global_balance = f"Error"
325
+ self.global_balance = "Error"
321
326
  self.global_spend_24h = "-"
322
327
  self.global_spend_7d = "-"
323
- self.global_status = f"❌"
328
+ self.global_status = "❌"
324
329
 
325
330
  def set_local_error(self, error: str):
326
331
  """Show error state for local data."""
@@ -328,7 +333,7 @@ class BalanceStatus(Static):
328
333
  self.local_cost = "Error"
329
334
  self.local_tokens = 0
330
335
  self.local_tasks = []
331
- self.local_status = f"❌"
336
+ self.local_status = "❌"
332
337
 
333
338
  def set_global_unavailable(self):
334
339
  """Mark global data as unavailable (no API key)."""