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
@@ -5,37 +5,35 @@ format and compute aggregates from call records.
5
5
  """
6
6
 
7
7
  import uuid
8
- import json
9
8
  from datetime import datetime
10
- from typing import Any, Dict, List, Optional, Union
9
+ from typing import Any
11
10
 
11
+ from synth_ai.lm.vendors.base import BaseLMResponse
12
12
  from synth_ai.tracing_v3.lm_call_record_abstractions import (
13
13
  LLMCallRecord,
14
- LLMUsage,
15
- LLMRequestParams,
16
- LLMMessage,
14
+ LLMChunk,
17
15
  LLMContentPart,
16
+ LLMMessage,
17
+ LLMRequestParams,
18
+ LLMUsage,
18
19
  ToolCallSpec,
19
- ToolCallResult,
20
- LLMChunk,
21
20
  )
22
- from synth_ai.lm.vendors.base import BaseLMResponse
23
21
 
24
22
 
25
23
  def create_llm_call_record_from_response(
26
24
  response: BaseLMResponse,
27
25
  model_name: str,
28
26
  provider: str,
29
- messages: List[Dict[str, Any]],
27
+ messages: list[dict[str, Any]],
30
28
  temperature: float = 0.8,
31
- request_params: Optional[Dict[str, Any]] = None,
32
- tools: Optional[List] = None,
33
- started_at: Optional[datetime] = None,
34
- completed_at: Optional[datetime] = None,
35
- latency_ms: Optional[int] = None,
29
+ request_params: dict[str, Any] | None = None,
30
+ tools: list | None = None,
31
+ started_at: datetime | None = None,
32
+ completed_at: datetime | None = None,
33
+ latency_ms: int | None = None,
36
34
  ) -> LLMCallRecord:
37
35
  """Create an LLMCallRecord from a vendor response.
38
-
36
+
39
37
  Args:
40
38
  response: The vendor response object
41
39
  model_name: Name of the model used
@@ -47,27 +45,27 @@ def create_llm_call_record_from_response(
47
45
  started_at: When the request started
48
46
  completed_at: When the request completed
49
47
  latency_ms: End-to-end latency in milliseconds
50
-
48
+
51
49
  Returns:
52
50
  A populated LLMCallRecord instance
53
51
  """
54
52
  # Generate call ID
55
53
  call_id = str(uuid.uuid4())
56
-
54
+
57
55
  # Determine API type from response
58
56
  api_type = "chat_completions" # Default
59
- if hasattr(response, 'api_type'):
57
+ if hasattr(response, "api_type"):
60
58
  if response.api_type == "responses":
61
59
  api_type = "responses"
62
60
  elif response.api_type == "completions":
63
61
  api_type = "completions"
64
-
62
+
65
63
  # Convert input messages to LLMMessage format
66
64
  input_messages = []
67
65
  for msg in messages:
68
66
  role = msg.get("role", "user")
69
67
  content = msg.get("content", "")
70
-
68
+
71
69
  # Handle different content formats
72
70
  if isinstance(content, str):
73
71
  parts = [LLMContentPart(type="text", text=content)]
@@ -78,41 +76,42 @@ def create_llm_call_record_from_response(
78
76
  if item.get("type") == "text":
79
77
  parts.append(LLMContentPart(type="text", text=item.get("text", "")))
80
78
  elif item.get("type") == "image_url":
81
- parts.append(LLMContentPart(
82
- type="image",
83
- uri=item.get("image_url", {}).get("url", ""),
84
- mime_type="image/jpeg"
85
- ))
79
+ parts.append(
80
+ LLMContentPart(
81
+ type="image",
82
+ uri=item.get("image_url", {}).get("url", ""),
83
+ mime_type="image/jpeg",
84
+ )
85
+ )
86
86
  elif item.get("type") == "image":
87
- parts.append(LLMContentPart(
88
- type="image",
89
- data=item.get("source", {}),
90
- mime_type=item.get("source", {}).get("media_type", "image/jpeg")
91
- ))
87
+ parts.append(
88
+ LLMContentPart(
89
+ type="image",
90
+ data=item.get("source", {}),
91
+ mime_type=item.get("source", {}).get("media_type", "image/jpeg"),
92
+ )
93
+ )
92
94
  else:
93
95
  parts.append(LLMContentPart(type="text", text=str(item)))
94
96
  else:
95
97
  parts = [LLMContentPart(type="text", text=str(content))]
96
-
98
+
97
99
  input_messages.append(LLMMessage(role=role, parts=parts))
98
-
100
+
99
101
  # Extract output messages from response
100
102
  output_messages = []
101
103
  output_text = None
102
-
103
- if hasattr(response, 'raw_response'):
104
+
105
+ if hasattr(response, "raw_response"):
104
106
  # Extract assistant message
105
107
  output_text = response.raw_response
106
108
  output_messages.append(
107
- LLMMessage(
108
- role="assistant",
109
- parts=[LLMContentPart(type="text", text=output_text)]
110
- )
109
+ LLMMessage(role="assistant", parts=[LLMContentPart(type="text", text=output_text)])
111
110
  )
112
-
111
+
113
112
  # Extract tool calls if present
114
113
  output_tool_calls = []
115
- if hasattr(response, 'tool_calls') and response.tool_calls:
114
+ if hasattr(response, "tool_calls") and response.tool_calls:
116
115
  for idx, tool_call in enumerate(response.tool_calls):
117
116
  if isinstance(tool_call, dict):
118
117
  output_tool_calls.append(
@@ -120,13 +119,13 @@ def create_llm_call_record_from_response(
120
119
  name=tool_call.get("function", {}).get("name", ""),
121
120
  arguments_json=tool_call.get("function", {}).get("arguments", "{}"),
122
121
  call_id=tool_call.get("id", f"tool_{idx}"),
123
- index=idx
122
+ index=idx,
124
123
  )
125
124
  )
126
-
125
+
127
126
  # Extract usage information
128
127
  usage = None
129
- if hasattr(response, 'usage') and response.usage:
128
+ if hasattr(response, "usage") and response.usage:
130
129
  usage = LLMUsage(
131
130
  input_tokens=response.usage.get("input_tokens"),
132
131
  output_tokens=response.usage.get("output_tokens"),
@@ -139,23 +138,23 @@ def create_llm_call_record_from_response(
139
138
  cache_write_tokens=response.usage.get("cache_write_tokens"),
140
139
  cache_read_tokens=response.usage.get("cache_read_tokens"),
141
140
  )
142
-
141
+
143
142
  # Build request parameters
144
143
  params = LLMRequestParams(
145
144
  temperature=temperature,
146
145
  top_p=request_params.get("top_p") if request_params else None,
147
146
  max_tokens=request_params.get("max_tokens") if request_params else None,
148
147
  stop=request_params.get("stop") if request_params else None,
149
- raw_params=request_params or {}
148
+ raw_params=request_params or {},
150
149
  )
151
-
150
+
152
151
  # Handle response-specific fields
153
152
  finish_reason = None
154
- if hasattr(response, 'finish_reason'):
153
+ if hasattr(response, "finish_reason"):
155
154
  finish_reason = response.finish_reason
156
- elif hasattr(response, 'stop_reason'):
155
+ elif hasattr(response, "stop_reason"):
157
156
  finish_reason = response.stop_reason
158
-
157
+
159
158
  # Create the call record
160
159
  record = LLMCallRecord(
161
160
  call_id=call_id,
@@ -178,23 +177,23 @@ def create_llm_call_record_from_response(
178
177
  metadata={
179
178
  "has_tools": tools is not None,
180
179
  "num_tools": len(tools) if tools else 0,
181
- }
180
+ },
182
181
  )
183
-
182
+
184
183
  # Store response ID if available (for Responses API)
185
- if hasattr(response, 'response_id') and response.response_id:
184
+ if hasattr(response, "response_id") and response.response_id:
186
185
  record.metadata["response_id"] = response.response_id
187
186
  record.provider_request_id = response.response_id
188
-
187
+
189
188
  return record
190
189
 
191
190
 
192
- def compute_aggregates_from_call_records(call_records: List[LLMCallRecord]) -> Dict[str, Any]:
191
+ def compute_aggregates_from_call_records(call_records: list[LLMCallRecord]) -> dict[str, Any]:
193
192
  """Compute aggregate statistics from a list of LLMCallRecord instances.
194
-
193
+
195
194
  Args:
196
195
  call_records: List of LLMCallRecord instances
197
-
196
+
198
197
  Returns:
199
198
  Dictionary containing aggregated statistics
200
199
  """
@@ -210,9 +209,9 @@ def compute_aggregates_from_call_records(call_records: List[LLMCallRecord]) -> D
210
209
  "tool_calls_count": 0,
211
210
  "error_count": 0,
212
211
  "success_count": 0,
213
- "call_count": len(call_records)
212
+ "call_count": len(call_records),
214
213
  }
215
-
214
+
216
215
  for record in call_records:
217
216
  # Token aggregation
218
217
  if record.usage:
@@ -226,54 +225,54 @@ def compute_aggregates_from_call_records(call_records: List[LLMCallRecord]) -> D
226
225
  aggregates["reasoning_tokens"] += record.usage.reasoning_tokens
227
226
  if record.usage.cost_usd:
228
227
  aggregates["cost_usd"] += record.usage.cost_usd
229
-
228
+
230
229
  # Latency aggregation
231
230
  if record.latency_ms:
232
231
  aggregates["latency_ms"] += record.latency_ms
233
-
232
+
234
233
  # Model and provider tracking
235
234
  if record.model_name:
236
235
  aggregates["models_used"].add(record.model_name)
237
236
  if record.provider:
238
237
  aggregates["providers_used"].add(record.provider)
239
-
238
+
240
239
  # Tool calls
241
240
  aggregates["tool_calls_count"] += len(record.output_tool_calls)
242
-
241
+
243
242
  # Success/error tracking
244
243
  if record.outcome == "error":
245
244
  aggregates["error_count"] += 1
246
245
  elif record.outcome == "success":
247
246
  aggregates["success_count"] += 1
248
-
247
+
249
248
  # Convert sets to lists for JSON serialization
250
249
  aggregates["models_used"] = list(aggregates["models_used"])
251
250
  aggregates["providers_used"] = list(aggregates["providers_used"])
252
-
251
+
253
252
  # Compute averages
254
253
  if aggregates["call_count"] > 0:
255
254
  aggregates["avg_latency_ms"] = aggregates["latency_ms"] / aggregates["call_count"]
256
255
  aggregates["avg_input_tokens"] = aggregates["input_tokens"] / aggregates["call_count"]
257
256
  aggregates["avg_output_tokens"] = aggregates["output_tokens"] / aggregates["call_count"]
258
-
257
+
259
258
  return aggregates
260
259
 
261
260
 
262
261
  def create_llm_call_record_from_streaming(
263
- chunks: List[LLMChunk],
262
+ chunks: list[LLMChunk],
264
263
  model_name: str,
265
264
  provider: str,
266
- messages: List[Dict[str, Any]],
265
+ messages: list[dict[str, Any]],
267
266
  temperature: float = 0.8,
268
- request_params: Optional[Dict[str, Any]] = None,
269
- started_at: Optional[datetime] = None,
270
- completed_at: Optional[datetime] = None,
267
+ request_params: dict[str, Any] | None = None,
268
+ started_at: datetime | None = None,
269
+ completed_at: datetime | None = None,
271
270
  ) -> LLMCallRecord:
272
271
  """Create an LLMCallRecord from streaming chunks.
273
-
272
+
274
273
  This function reconstructs a complete LLMCallRecord from streaming
275
274
  response chunks, useful for Responses API or streaming Chat Completions.
276
-
275
+
277
276
  Args:
278
277
  chunks: List of LLMChunk instances from streaming
279
278
  model_name: Name of the model used
@@ -283,49 +282,40 @@ def create_llm_call_record_from_streaming(
283
282
  request_params: Additional request parameters
284
283
  started_at: When the request started
285
284
  completed_at: When the request completed
286
-
285
+
287
286
  Returns:
288
287
  A populated LLMCallRecord instance
289
288
  """
290
289
  # Reconstruct output text from chunks
291
- output_text = "".join(
292
- chunk.delta_text for chunk in chunks
293
- if chunk.delta_text
294
- )
295
-
290
+ output_text = "".join(chunk.delta_text for chunk in chunks if chunk.delta_text)
291
+
296
292
  # Calculate latency from chunk timestamps
297
293
  latency_ms = None
298
294
  if chunks and started_at:
299
295
  last_chunk_time = chunks[-1].received_at
300
296
  latency_ms = int((last_chunk_time - started_at).total_seconds() * 1000)
301
-
297
+
302
298
  # Convert input messages
303
299
  input_messages = []
304
300
  for msg in messages:
305
301
  role = msg.get("role", "user")
306
302
  content = msg.get("content", "")
307
-
303
+
308
304
  if isinstance(content, str):
309
305
  parts = [LLMContentPart(type="text", text=content)]
310
306
  else:
311
307
  parts = [LLMContentPart(type="text", text=str(content))]
312
-
308
+
313
309
  input_messages.append(LLMMessage(role=role, parts=parts))
314
-
310
+
315
311
  # Create output message
316
312
  output_messages = [
317
- LLMMessage(
318
- role="assistant",
319
- parts=[LLMContentPart(type="text", text=output_text)]
320
- )
313
+ LLMMessage(role="assistant", parts=[LLMContentPart(type="text", text=output_text)])
321
314
  ]
322
-
315
+
323
316
  # Build request parameters
324
- params = LLMRequestParams(
325
- temperature=temperature,
326
- raw_params=request_params or {}
327
- )
328
-
317
+ params = LLMRequestParams(temperature=temperature, raw_params=request_params or {})
318
+
329
319
  # Create the call record
330
320
  record = LLMCallRecord(
331
321
  call_id=str(uuid.uuid4()),
@@ -341,10 +331,7 @@ def create_llm_call_record_from_streaming(
341
331
  output_text=output_text,
342
332
  chunks=chunks,
343
333
  outcome="success",
344
- metadata={
345
- "chunk_count": len(chunks),
346
- "streaming": True
347
- }
334
+ metadata={"chunk_count": len(chunks), "streaming": True},
348
335
  )
349
-
350
- return record
336
+
337
+ return record
@@ -23,8 +23,8 @@ Design goals:
23
23
  from __future__ import annotations
24
24
 
25
25
  from dataclasses import dataclass, field
26
- from typing import Any, Optional, Literal
27
26
  from datetime import datetime
27
+ from typing import Any, Literal
28
28
 
29
29
 
30
30
  @dataclass
@@ -222,7 +222,7 @@ class LLMCallRecord:
222
222
  redactions: list[dict[str, Any]] | None = None
223
223
 
224
224
 
225
- def compute_latency_ms(record: LLMCallRecord) -> Optional[int]:
225
+ def compute_latency_ms(record: LLMCallRecord) -> int | None:
226
226
  """Compute and update latency_ms from timestamps if available."""
227
227
  if record.started_at and record.completed_at:
228
228
  delta = int((record.completed_at - record.started_at).total_seconds() * 1000)
@@ -253,5 +253,3 @@ def compute_latency_ms(record: LLMCallRecord) -> Optional[int]:
253
253
  #
254
254
  # Tool execution results should be attached as ToolCallResult entries when the
255
255
  # agent runtime executes the requested tool(s) and has ground-truth outputs.
256
-
257
-
@@ -5,11 +5,9 @@ Helper script to identify files that need migration from v2 to v3 tracing.
5
5
 
6
6
  import os
7
7
  import re
8
- from pathlib import Path
9
- from typing import List, Tuple
10
8
 
11
9
 
12
- def find_v2_imports(root_path: str = ".") -> List[Tuple[str, List[str]]]:
10
+ def find_v2_imports(root_path: str = ".") -> list[tuple[str, list[str]]]:
13
11
  """Find all Python files importing from tracing_v2."""
14
12
  v2_files = []
15
13
 
@@ -37,7 +35,7 @@ def find_v2_imports(root_path: str = ".") -> List[Tuple[str, List[str]]]:
37
35
  if file.endswith(".py"):
38
36
  file_path = os.path.join(root, file)
39
37
  try:
40
- with open(file_path, "r") as f:
38
+ with open(file_path) as f:
41
39
  content = f.read()
42
40
 
43
41
  matches = []
@@ -53,7 +51,7 @@ def find_v2_imports(root_path: str = ".") -> List[Tuple[str, List[str]]]:
53
51
  return v2_files
54
52
 
55
53
 
56
- def categorize_files(v2_files: List[Tuple[str, List[str]]]) -> dict:
54
+ def categorize_files(v2_files: list[tuple[str, list[str]]]) -> dict:
57
55
  """Categorize files by their type/location."""
58
56
  categories = {"tests": [], "core_library": [], "examples": [], "debug_scripts": [], "other": []}
59
57
 
@@ -25,10 +25,10 @@ application to continue without blocking on sync operations.
25
25
  """
26
26
 
27
27
  import asyncio
28
- import libsql
29
- import os
30
28
  import logging
31
- from typing import Optional
29
+
30
+ import libsql
31
+
32
32
  from .config import CONFIG
33
33
 
34
34
  logger = logging.getLogger(__name__)
@@ -36,13 +36,13 @@ logger = logging.getLogger(__name__)
36
36
 
37
37
  class ReplicaSync:
38
38
  """Manages synchronization of embedded SQLite replica with remote Turso database.
39
-
39
+
40
40
  This class handles the lifecycle of replica synchronization, including:
41
41
  - Establishing connections to both local and remote databases
42
42
  - Running periodic sync operations
43
43
  - Handling sync failures gracefully
44
44
  - Managing the background sync task
45
-
45
+
46
46
  The sync is designed to be resilient to network failures and will
47
47
  continue retrying with exponential backoff.
48
48
  """
@@ -50,9 +50,9 @@ class ReplicaSync:
50
50
  def __init__(
51
51
  self,
52
52
  db_path: str = "embedded.db",
53
- sync_url: Optional[str] = None,
54
- auth_token: Optional[str] = None,
55
- sync_interval: Optional[int] = None,
53
+ sync_url: str | None = None,
54
+ auth_token: str | None = None,
55
+ sync_interval: int | None = None,
56
56
  ):
57
57
  """Initialize replica sync manager.
58
58
 
@@ -66,16 +66,16 @@ class ReplicaSync:
66
66
  self.sync_url = sync_url or CONFIG.sync_url
67
67
  self.auth_token = auth_token or CONFIG.auth_token
68
68
  self.sync_interval = sync_interval or CONFIG.sync_interval
69
- self._sync_task: Optional[asyncio.Task] = None
70
- self._conn: Optional[libsql.Connection] = None
69
+ self._sync_task: asyncio.Task | None = None
70
+ self._conn: libsql.Connection | None = None
71
71
 
72
72
  def _ensure_connection(self):
73
73
  """Ensure libsql connection is established.
74
-
74
+
75
75
  Creates a connection to the local embedded database with sync
76
76
  capabilities. The libsql library handles the replication protocol
77
77
  with the remote Turso database.
78
-
78
+
79
79
  Raises:
80
80
  ValueError: If no sync_url is configured
81
81
  """
@@ -97,12 +97,12 @@ class ReplicaSync:
97
97
 
98
98
  async def sync_once(self) -> bool:
99
99
  """Perform a single sync operation.
100
-
100
+
101
101
  This method:
102
102
  1. Ensures a connection exists
103
103
  2. Runs the sync in a thread pool to avoid blocking
104
104
  3. Handles failures gracefully
105
-
105
+
106
106
  The actual sync protocol is handled by libsql and includes:
107
107
  - Sending local changes to remote
108
108
  - Receiving remote changes (if configured)
@@ -123,10 +123,10 @@ class ReplicaSync:
123
123
 
124
124
  async def keep_fresh(self):
125
125
  """Background task to continuously sync the replica.
126
-
126
+
127
127
  Runs in an infinite loop, performing sync operations at the configured
128
128
  interval. Handles cancellation gracefully for clean shutdown.
129
-
129
+
130
130
  The task will continue running even if individual syncs fail, ensuring
131
131
  eventual consistency when connectivity is restored.
132
132
  """
@@ -148,10 +148,10 @@ class ReplicaSync:
148
148
 
149
149
  def start_background_sync(self) -> asyncio.Task:
150
150
  """Start the background sync task.
151
-
151
+
152
152
  Creates an asyncio task that runs the sync loop. The task is stored
153
153
  internally for lifecycle management.
154
-
154
+
155
155
  This method is idempotent - calling it multiple times will not create
156
156
  multiple sync tasks.
157
157
 
@@ -168,23 +168,21 @@ class ReplicaSync:
168
168
 
169
169
  async def stop(self):
170
170
  """Stop the background sync task and close connection.
171
-
171
+
172
172
  Performs a clean shutdown:
173
173
  1. Cancels the background sync task
174
174
  2. Waits for task completion
175
175
  3. Closes the database connection
176
-
176
+
177
177
  This method is safe to call multiple times.
178
178
  """
179
179
  if self._sync_task and not self._sync_task.done():
180
180
  # Request cancellation
181
181
  self._sync_task.cancel()
182
- try:
182
+ import contextlib
183
+ with contextlib.suppress(asyncio.CancelledError):
183
184
  # Wait for the task to finish
184
185
  await self._sync_task
185
- except asyncio.CancelledError:
186
- # Expected when task is cancelled
187
- pass
188
186
 
189
187
  if self._conn:
190
188
  # Close the libsql connection
@@ -194,22 +192,22 @@ class ReplicaSync:
194
192
 
195
193
 
196
194
  # Global replica sync instance
197
- _replica_sync: Optional[ReplicaSync] = None
195
+ _replica_sync: ReplicaSync | None = None
198
196
 
199
197
 
200
- def get_replica_sync() -> Optional[ReplicaSync]:
198
+ def get_replica_sync() -> ReplicaSync | None:
201
199
  """Get the global replica sync instance."""
202
200
  return _replica_sync
203
201
 
204
202
 
205
203
  async def start_replica_sync(
206
204
  db_path: str = "embedded.db",
207
- sync_url: Optional[str] = None,
208
- auth_token: Optional[str] = None,
209
- sync_interval: Optional[int] = None,
205
+ sync_url: str | None = None,
206
+ auth_token: str | None = None,
207
+ sync_interval: int | None = None,
210
208
  ) -> ReplicaSync:
211
209
  """Start global replica sync.
212
-
210
+
213
211
  Convenience function to create and start a replica sync instance.
214
212
  Performs an initial sync before starting the background task to ensure
215
213
  the local database is up-to-date.
@@ -222,7 +220,7 @@ async def start_replica_sync(
222
220
 
223
221
  Returns:
224
222
  The ReplicaSync instance
225
-
223
+
226
224
  Raises:
227
225
  ValueError: If sync_url is not provided and not in environment
228
226
  """
@@ -247,7 +245,7 @@ async def start_replica_sync(
247
245
 
248
246
  async def stop_replica_sync():
249
247
  """Stop the global replica sync.
250
-
248
+
251
249
  Stops the global replica sync instance if one is running.
252
250
  This should be called during application shutdown to ensure
253
251
  clean termination of the sync task.