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.
- synth_ai/__init__.py +18 -9
- synth_ai/cli/__init__.py +10 -5
- synth_ai/cli/balance.py +25 -32
- synth_ai/cli/calc.py +2 -3
- synth_ai/cli/demo.py +3 -5
- synth_ai/cli/legacy_root_backup.py +58 -32
- synth_ai/cli/man.py +22 -19
- synth_ai/cli/recent.py +9 -8
- synth_ai/cli/root.py +58 -13
- synth_ai/cli/status.py +13 -6
- synth_ai/cli/traces.py +45 -21
- synth_ai/cli/watch.py +40 -37
- synth_ai/config/base_url.py +47 -2
- synth_ai/core/experiment.py +1 -2
- synth_ai/environments/__init__.py +2 -6
- synth_ai/environments/environment/artifacts/base.py +3 -1
- synth_ai/environments/environment/db/sqlite.py +1 -1
- synth_ai/environments/environment/registry.py +19 -20
- synth_ai/environments/environment/resources/sqlite.py +2 -3
- synth_ai/environments/environment/rewards/core.py +3 -2
- synth_ai/environments/environment/tools/__init__.py +6 -4
- synth_ai/environments/examples/crafter_classic/__init__.py +1 -1
- synth_ai/environments/examples/crafter_classic/engine.py +13 -13
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +1 -0
- synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +2 -1
- synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +2 -1
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +3 -2
- synth_ai/environments/examples/crafter_classic/environment.py +16 -15
- synth_ai/environments/examples/crafter_classic/taskset.py +2 -2
- synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +2 -3
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +2 -1
- synth_ai/environments/examples/crafter_custom/crafter/__init__.py +2 -2
- synth_ai/environments/examples/crafter_custom/crafter/config.py +2 -2
- synth_ai/environments/examples/crafter_custom/crafter/env.py +1 -5
- synth_ai/environments/examples/crafter_custom/crafter/objects.py +1 -2
- synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +1 -2
- synth_ai/environments/examples/crafter_custom/dataset_builder.py +5 -5
- synth_ai/environments/examples/crafter_custom/environment.py +13 -13
- synth_ai/environments/examples/crafter_custom/run_dataset.py +5 -5
- synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +2 -2
- synth_ai/environments/examples/enron/art_helpers/local_email_db.py +5 -4
- synth_ai/environments/examples/enron/art_helpers/types_enron.py +2 -1
- synth_ai/environments/examples/enron/engine.py +18 -14
- synth_ai/environments/examples/enron/environment.py +12 -11
- synth_ai/environments/examples/enron/taskset.py +7 -7
- synth_ai/environments/examples/minigrid/__init__.py +6 -6
- synth_ai/environments/examples/minigrid/engine.py +6 -6
- synth_ai/environments/examples/minigrid/environment.py +6 -6
- synth_ai/environments/examples/minigrid/puzzle_loader.py +3 -2
- synth_ai/environments/examples/minigrid/taskset.py +13 -13
- synth_ai/environments/examples/nethack/achievements.py +1 -1
- synth_ai/environments/examples/nethack/engine.py +8 -7
- synth_ai/environments/examples/nethack/environment.py +10 -9
- synth_ai/environments/examples/nethack/helpers/__init__.py +8 -9
- synth_ai/environments/examples/nethack/helpers/action_mapping.py +1 -1
- synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +2 -1
- synth_ai/environments/examples/nethack/helpers/observation_utils.py +1 -1
- synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +3 -4
- synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +6 -5
- synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +5 -5
- synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +7 -6
- synth_ai/environments/examples/nethack/taskset.py +5 -5
- synth_ai/environments/examples/red/engine.py +9 -8
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +7 -7
- synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +3 -2
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +2 -1
- synth_ai/environments/examples/red/environment.py +18 -15
- synth_ai/environments/examples/red/taskset.py +5 -3
- synth_ai/environments/examples/sokoban/engine.py +16 -13
- synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +3 -2
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +2 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +1 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +7 -5
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +1 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +2 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +5 -4
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +3 -2
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +2 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +5 -4
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +1 -1
- synth_ai/environments/examples/sokoban/environment.py +15 -14
- synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +5 -3
- synth_ai/environments/examples/sokoban/puzzle_loader.py +3 -2
- synth_ai/environments/examples/sokoban/taskset.py +13 -10
- synth_ai/environments/examples/tictactoe/engine.py +6 -6
- synth_ai/environments/examples/tictactoe/environment.py +8 -7
- synth_ai/environments/examples/tictactoe/taskset.py +6 -5
- synth_ai/environments/examples/verilog/engine.py +4 -3
- synth_ai/environments/examples/verilog/environment.py +11 -10
- synth_ai/environments/examples/verilog/taskset.py +14 -12
- synth_ai/environments/examples/wordle/__init__.py +5 -5
- synth_ai/environments/examples/wordle/engine.py +32 -25
- synth_ai/environments/examples/wordle/environment.py +21 -16
- synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +6 -6
- synth_ai/environments/examples/wordle/taskset.py +20 -12
- synth_ai/environments/reproducibility/core.py +1 -1
- synth_ai/environments/reproducibility/tree.py +21 -21
- synth_ai/environments/service/app.py +3 -2
- synth_ai/environments/service/core_routes.py +104 -110
- synth_ai/environments/service/external_registry.py +1 -2
- synth_ai/environments/service/registry.py +1 -1
- synth_ai/environments/stateful/core.py +1 -2
- synth_ai/environments/stateful/engine.py +1 -1
- synth_ai/environments/tasks/api.py +4 -4
- synth_ai/environments/tasks/core.py +14 -12
- synth_ai/environments/tasks/filters.py +6 -4
- synth_ai/environments/tasks/utils.py +13 -11
- synth_ai/evals/base.py +2 -3
- synth_ai/experimental/synth_oss.py +4 -4
- synth_ai/http.py +102 -0
- synth_ai/inference/__init__.py +7 -0
- synth_ai/inference/client.py +20 -0
- synth_ai/jobs/client.py +246 -0
- synth_ai/learning/__init__.py +24 -0
- synth_ai/learning/client.py +149 -0
- synth_ai/learning/config.py +43 -0
- synth_ai/learning/constants.py +29 -0
- synth_ai/learning/ft_client.py +59 -0
- synth_ai/learning/gateway.py +1 -3
- synth_ai/learning/health.py +43 -0
- synth_ai/learning/jobs.py +205 -0
- synth_ai/learning/prompts/banking77_injection_eval.py +15 -10
- synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +26 -14
- synth_ai/learning/prompts/mipro.py +61 -52
- synth_ai/learning/prompts/random_search.py +42 -43
- synth_ai/learning/prompts/run_mipro_banking77.py +32 -20
- synth_ai/learning/prompts/run_random_search_banking77.py +71 -52
- synth_ai/learning/rl_client.py +256 -0
- synth_ai/learning/sse.py +58 -0
- synth_ai/learning/validators.py +48 -0
- synth_ai/lm/__init__.py +5 -5
- synth_ai/lm/caching/ephemeral.py +9 -9
- synth_ai/lm/caching/handler.py +20 -20
- synth_ai/lm/caching/persistent.py +10 -10
- synth_ai/lm/config.py +3 -3
- synth_ai/lm/constants.py +7 -7
- synth_ai/lm/core/all.py +17 -3
- synth_ai/lm/core/exceptions.py +0 -2
- synth_ai/lm/core/main.py +26 -41
- synth_ai/lm/core/main_v3.py +33 -10
- synth_ai/lm/core/synth_models.py +48 -0
- synth_ai/lm/core/vendor_clients.py +26 -22
- synth_ai/lm/injection.py +7 -8
- synth_ai/lm/overrides.py +21 -19
- synth_ai/lm/provider_support/__init__.py +1 -1
- synth_ai/lm/provider_support/anthropic.py +15 -15
- synth_ai/lm/provider_support/openai.py +23 -21
- synth_ai/lm/structured_outputs/handler.py +34 -32
- synth_ai/lm/structured_outputs/inject.py +24 -27
- synth_ai/lm/structured_outputs/rehabilitate.py +19 -15
- synth_ai/lm/tools/base.py +17 -16
- synth_ai/lm/unified_interface.py +17 -18
- synth_ai/lm/vendors/base.py +20 -18
- synth_ai/lm/vendors/core/anthropic_api.py +36 -27
- synth_ai/lm/vendors/core/gemini_api.py +31 -36
- synth_ai/lm/vendors/core/mistral_api.py +19 -19
- synth_ai/lm/vendors/core/openai_api.py +42 -13
- synth_ai/lm/vendors/openai_standard.py +158 -101
- synth_ai/lm/vendors/openai_standard_responses.py +74 -61
- synth_ai/lm/vendors/retries.py +9 -1
- synth_ai/lm/vendors/supported/custom_endpoint.py +38 -28
- synth_ai/lm/vendors/supported/deepseek.py +10 -10
- synth_ai/lm/vendors/supported/grok.py +8 -8
- synth_ai/lm/vendors/supported/ollama.py +2 -1
- synth_ai/lm/vendors/supported/openrouter.py +11 -9
- synth_ai/lm/vendors/synth_client.py +425 -75
- synth_ai/lm/warmup.py +8 -7
- synth_ai/rl/__init__.py +30 -0
- synth_ai/rl/contracts.py +32 -0
- synth_ai/rl/env_keys.py +137 -0
- synth_ai/rl/secrets.py +19 -0
- synth_ai/scripts/verify_rewards.py +100 -0
- synth_ai/task/__init__.py +10 -0
- synth_ai/task/contracts.py +120 -0
- synth_ai/task/health.py +28 -0
- synth_ai/task/validators.py +12 -0
- synth_ai/tracing/__init__.py +22 -10
- synth_ai/tracing_v1/__init__.py +22 -20
- synth_ai/tracing_v3/__init__.py +7 -7
- synth_ai/tracing_v3/abstractions.py +56 -52
- synth_ai/tracing_v3/config.py +4 -2
- synth_ai/tracing_v3/db_config.py +6 -8
- synth_ai/tracing_v3/decorators.py +29 -30
- synth_ai/tracing_v3/examples/basic_usage.py +12 -12
- synth_ai/tracing_v3/hooks.py +24 -22
- synth_ai/tracing_v3/llm_call_record_helpers.py +85 -98
- synth_ai/tracing_v3/lm_call_record_abstractions.py +2 -4
- synth_ai/tracing_v3/migration_helper.py +3 -5
- synth_ai/tracing_v3/replica_sync.py +30 -32
- synth_ai/tracing_v3/session_tracer.py +158 -31
- synth_ai/tracing_v3/storage/__init__.py +1 -1
- synth_ai/tracing_v3/storage/base.py +8 -7
- synth_ai/tracing_v3/storage/config.py +4 -4
- synth_ai/tracing_v3/storage/factory.py +4 -4
- synth_ai/tracing_v3/storage/utils.py +9 -9
- synth_ai/tracing_v3/turso/__init__.py +3 -3
- synth_ai/tracing_v3/turso/daemon.py +9 -9
- synth_ai/tracing_v3/turso/manager.py +278 -48
- synth_ai/tracing_v3/turso/models.py +77 -19
- synth_ai/tracing_v3/utils.py +5 -5
- synth_ai/v0/tracing/abstractions.py +28 -28
- synth_ai/v0/tracing/base_client.py +9 -9
- synth_ai/v0/tracing/client_manager.py +7 -7
- synth_ai/v0/tracing/config.py +7 -7
- synth_ai/v0/tracing/context.py +6 -6
- synth_ai/v0/tracing/decorators.py +6 -5
- synth_ai/v0/tracing/events/manage.py +1 -1
- synth_ai/v0/tracing/events/store.py +5 -4
- synth_ai/v0/tracing/immediate_client.py +4 -5
- synth_ai/v0/tracing/local.py +3 -3
- synth_ai/v0/tracing/log_client_base.py +4 -5
- synth_ai/v0/tracing/retry_queue.py +5 -6
- synth_ai/v0/tracing/trackers.py +25 -25
- synth_ai/v0/tracing/upload.py +6 -0
- synth_ai/v0/tracing_v1/__init__.py +1 -1
- synth_ai/v0/tracing_v1/abstractions.py +28 -28
- synth_ai/v0/tracing_v1/base_client.py +9 -9
- synth_ai/v0/tracing_v1/client_manager.py +7 -7
- synth_ai/v0/tracing_v1/config.py +7 -7
- synth_ai/v0/tracing_v1/context.py +6 -6
- synth_ai/v0/tracing_v1/decorators.py +7 -6
- synth_ai/v0/tracing_v1/events/manage.py +1 -1
- synth_ai/v0/tracing_v1/events/store.py +5 -4
- synth_ai/v0/tracing_v1/immediate_client.py +4 -5
- synth_ai/v0/tracing_v1/local.py +3 -3
- synth_ai/v0/tracing_v1/log_client_base.py +4 -5
- synth_ai/v0/tracing_v1/retry_queue.py +5 -6
- synth_ai/v0/tracing_v1/trackers.py +25 -25
- synth_ai/v0/tracing_v1/upload.py +25 -24
- synth_ai/zyk/__init__.py +1 -0
- synth_ai-0.2.4.dev8.dist-info/METADATA +635 -0
- synth_ai-0.2.4.dev8.dist-info/RECORD +317 -0
- synth_ai/tui/__init__.py +0 -1
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -165
- synth_ai/tui/cli/query_experiments_v3.py +0 -165
- synth_ai/tui/dashboard.py +0 -329
- synth_ai-0.2.4.dev6.dist-info/METADATA +0 -203
- synth_ai-0.2.4.dev6.dist-info/RECORD +0 -299
- {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
-
|
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:
|
27
|
+
messages: list[dict[str, Any]],
|
30
28
|
temperature: float = 0.8,
|
31
|
-
request_params:
|
32
|
-
tools:
|
33
|
-
started_at:
|
34
|
-
completed_at:
|
35
|
-
latency_ms:
|
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,
|
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(
|
82
|
-
|
83
|
-
|
84
|
-
|
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(
|
88
|
-
|
89
|
-
|
90
|
-
|
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,
|
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,
|
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,
|
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,
|
153
|
+
if hasattr(response, "finish_reason"):
|
155
154
|
finish_reason = response.finish_reason
|
156
|
-
elif hasattr(response,
|
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,
|
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:
|
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:
|
262
|
+
chunks: list[LLMChunk],
|
264
263
|
model_name: str,
|
265
264
|
provider: str,
|
266
|
-
messages:
|
265
|
+
messages: list[dict[str, Any]],
|
267
266
|
temperature: float = 0.8,
|
268
|
-
request_params:
|
269
|
-
started_at:
|
270
|
-
completed_at:
|
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
|
-
|
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
|
-
|
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) ->
|
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 = ".") ->
|
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
|
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:
|
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
|
-
|
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:
|
54
|
-
auth_token:
|
55
|
-
sync_interval:
|
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:
|
70
|
-
self._conn:
|
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
|
-
|
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:
|
195
|
+
_replica_sync: ReplicaSync | None = None
|
198
196
|
|
199
197
|
|
200
|
-
def get_replica_sync() ->
|
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:
|
208
|
-
auth_token:
|
209
|
-
sync_interval:
|
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.
|