synth-ai 0.2.16__py3-none-any.whl → 0.2.19__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.
- examples/analyze_semantic_words.sh +2 -2
- examples/baseline/banking77_baseline.py +204 -0
- examples/baseline/crafter_baseline.py +407 -0
- examples/baseline/pokemon_red_baseline.py +326 -0
- examples/baseline/simple_baseline.py +56 -0
- examples/baseline/warming_up_to_rl_baseline.py +239 -0
- examples/blog_posts/gepa/README.md +355 -0
- examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
- examples/blog_posts/gepa/configs/banking77_gepa_test.toml +82 -0
- examples/blog_posts/gepa/configs/banking77_mipro_local.toml +52 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/hover_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/hover_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +59 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +36 -0
- examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +53 -0
- examples/blog_posts/gepa/configs/pupa_gepa_local.toml +60 -0
- examples/blog_posts/gepa/configs/pupa_mipro_local.toml +54 -0
- examples/blog_posts/gepa/deploy_banking77_task_app.sh +41 -0
- examples/blog_posts/gepa/gepa_baseline.py +204 -0
- examples/blog_posts/gepa/query_prompts_example.py +97 -0
- examples/blog_posts/gepa/run_gepa_banking77.sh +87 -0
- examples/blog_posts/gepa/task_apps.py +105 -0
- examples/blog_posts/gepa/test_gepa_local.sh +67 -0
- examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
- examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
- examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
- examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/pokemon_vl/extract_images.py +239 -0
- examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
- examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
- examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
- examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
- examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
- examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
- examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
- examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
- examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
- examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
- examples/multi_step/configs/VERILOG_REWARDS.md +4 -0
- examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +4 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +2 -1
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +2 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +2 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
- examples/multi_step/configs/verilog_rl_lora.toml +80 -123
- examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
- examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
- examples/qwen_coder/configs/coder_lora_small.toml +1 -3
- examples/qwen_vl/README.md +10 -12
- examples/qwen_vl/SETUP_COMPLETE.md +7 -8
- examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
- examples/qwen_vl/collect_data_via_cli.md +76 -84
- examples/qwen_vl/collect_vision_traces.py +4 -4
- examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
- examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
- examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
- examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
- examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
- examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
- examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
- examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
- examples/qwen_vl/run_vision_comparison.sh +6 -7
- examples/rl/README.md +5 -5
- examples/rl/configs/rl_from_base_qwen.toml +26 -1
- examples/rl/configs/rl_from_base_qwen17.toml +6 -2
- examples/rl/task_app/README.md +1 -2
- examples/rl/task_app/math_single_step.py +2 -2
- examples/run_crafter_demo.sh +2 -2
- examples/sft/README.md +1 -1
- examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
- examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
- examples/swe/task_app/README.md +32 -2
- examples/swe/task_app/grpo_swe_mini.py +4 -0
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
- examples/swe/task_app/hosted/inference/openai_client.py +4 -38
- examples/swe/task_app/hosted/policy_routes.py +17 -0
- examples/swe/task_app/hosted/rollout.py +4 -2
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/banking77/__init__.py +6 -0
- examples/task_apps/banking77/banking77_task_app.py +841 -0
- examples/task_apps/banking77/deploy_wrapper.py +46 -0
- examples/task_apps/crafter/CREATE_SFT_DATASET.md +4 -0
- examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +4 -0
- examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +4 -0
- examples/task_apps/crafter/task_app/README.md +1 -1
- examples/task_apps/crafter/task_app/grpo_crafter.py +90 -5
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
- examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +49 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +372 -107
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +81 -12
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +82 -11
- examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -1
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
- examples/task_apps/gepa_benchmarks/__init__.py +7 -0
- examples/task_apps/gepa_benchmarks/common.py +260 -0
- examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
- examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
- examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
- examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
- examples/task_apps/math/README.md +1 -2
- examples/task_apps/pokemon_red/README.md +3 -4
- examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
- examples/task_apps/pokemon_red/task_app.py +288 -39
- examples/task_apps/sokoban/README.md +2 -3
- examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
- examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
- examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
- examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +3 -2
- examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
- examples/warming_up_to_rl/task_app/README.md +1 -1
- examples/warming_up_to_rl/task_app/grpo_crafter.py +185 -5
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +49 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +156 -45
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +37 -4
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +33 -3
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +67 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +6 -0
- synth_ai/api/train/builders.py +99 -4
- synth_ai/api/train/cli.py +516 -26
- synth_ai/api/train/config_finder.py +13 -2
- synth_ai/api/train/configs/__init__.py +23 -2
- synth_ai/api/train/configs/prompt_learning.py +442 -0
- synth_ai/api/train/configs/rl.py +61 -7
- synth_ai/api/train/configs/sft.py +6 -2
- synth_ai/api/train/configs/shared.py +59 -2
- synth_ai/api/train/task_app.py +1 -1
- synth_ai/api/train/validators.py +277 -0
- synth_ai/auth/credentials.py +119 -0
- synth_ai/baseline/__init__.py +25 -0
- synth_ai/baseline/config.py +209 -0
- synth_ai/baseline/discovery.py +214 -0
- synth_ai/baseline/execution.py +146 -0
- synth_ai/cli/__init__.py +94 -18
- synth_ai/cli/__main__.py +0 -0
- synth_ai/cli/claude.py +70 -0
- synth_ai/cli/codex.py +84 -0
- synth_ai/cli/commands/__init__.py +18 -0
- synth_ai/cli/commands/baseline/__init__.py +12 -0
- synth_ai/cli/commands/baseline/core.py +637 -0
- synth_ai/cli/commands/baseline/list.py +93 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1112 -0
- synth_ai/cli/commands/eval/errors.py +81 -0
- synth_ai/cli/commands/eval/validation.py +133 -0
- synth_ai/cli/commands/filter/__init__.py +12 -0
- synth_ai/cli/commands/filter/core.py +424 -0
- synth_ai/cli/commands/filter/errors.py +55 -0
- synth_ai/cli/commands/filter/validation.py +77 -0
- synth_ai/cli/commands/help/__init__.py +177 -0
- synth_ai/cli/commands/help/core.py +72 -0
- synth_ai/cli/commands/smoke/__init__.py +7 -0
- synth_ai/cli/commands/smoke/core.py +1436 -0
- synth_ai/cli/commands/status/__init__.py +64 -0
- synth_ai/cli/commands/status/client.py +192 -0
- synth_ai/cli/commands/status/config.py +92 -0
- synth_ai/cli/commands/status/errors.py +20 -0
- synth_ai/cli/commands/status/formatters.py +164 -0
- synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
- synth_ai/cli/commands/status/subcommands/files.py +79 -0
- synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
- synth_ai/cli/commands/status/subcommands/models.py +79 -0
- synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
- synth_ai/cli/commands/status/subcommands/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -0
- synth_ai/cli/commands/status/subcommands/usage.py +203 -0
- synth_ai/cli/commands/status/utils.py +114 -0
- synth_ai/cli/commands/train/__init__.py +53 -0
- synth_ai/cli/commands/train/core.py +21 -0
- synth_ai/cli/commands/train/errors.py +117 -0
- synth_ai/cli/commands/train/judge_schemas.py +200 -0
- synth_ai/cli/commands/train/judge_validation.py +305 -0
- synth_ai/cli/commands/train/validation.py +386 -0
- synth_ai/cli/demo.py +30 -158
- synth_ai/cli/deploy/__init__.py +43 -0
- synth_ai/cli/deploy.py +162 -0
- synth_ai/cli/eval/__init__.py +36 -0
- synth_ai/cli/eval/core.py +5 -0
- synth_ai/cli/eval/errors.py +31 -0
- synth_ai/cli/eval/validation.py +5 -0
- synth_ai/cli/filter/__init__.py +28 -0
- synth_ai/cli/filter/core.py +5 -0
- synth_ai/cli/filter/errors.py +23 -0
- synth_ai/cli/filter/validation.py +5 -0
- synth_ai/cli/legacy_root_backup.py +14 -8
- synth_ai/cli/modal_serve/__init__.py +12 -0
- synth_ai/cli/modal_serve/core.py +14 -0
- synth_ai/cli/modal_serve/errors.py +8 -0
- synth_ai/cli/modal_serve/validation.py +11 -0
- synth_ai/cli/opencode.py +107 -0
- synth_ai/cli/root.py +9 -5
- synth_ai/cli/serve/__init__.py +12 -0
- synth_ai/cli/serve/core.py +14 -0
- synth_ai/cli/serve/errors.py +8 -0
- synth_ai/cli/serve/validation.py +11 -0
- synth_ai/cli/setup.py +20 -265
- synth_ai/cli/status.py +7 -126
- synth_ai/cli/task_app_deploy.py +1 -10
- synth_ai/cli/task_app_modal_serve.py +4 -9
- synth_ai/cli/task_app_serve.py +4 -11
- synth_ai/cli/task_apps.py +51 -1480
- synth_ai/cli/train/__init__.py +12 -0
- synth_ai/cli/train/core.py +21 -0
- synth_ai/cli/train/errors.py +8 -0
- synth_ai/cli/train/validation.py +24 -0
- synth_ai/cli/train.py +1 -14
- synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
- synth_ai/environments/examples/red/engine.py +33 -12
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
- synth_ai/environments/examples/red/environment.py +26 -0
- synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
- synth_ai/http.py +12 -0
- synth_ai/judge_schemas.py +10 -10
- synth_ai/learning/__init__.py +10 -0
- synth_ai/learning/prompt_learning_client.py +276 -0
- synth_ai/learning/prompt_learning_types.py +184 -0
- synth_ai/learning/rl/client.py +3 -1
- synth_ai/pricing/__init__.py +2 -0
- synth_ai/pricing/model_pricing.py +57 -0
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +518 -0
- synth_ai/streaming/streamer.py +320 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/apps/__init__.py +1 -0
- synth_ai/task/config.py +2 -0
- synth_ai/task/tracing_utils.py +25 -25
- synth_ai/task/validators.py +45 -9
- synth_ai/task_app_cfgs.py +21 -0
- synth_ai/tracing_v3/config.py +162 -19
- synth_ai/tracing_v3/constants.py +1 -1
- synth_ai/tracing_v3/db_config.py +24 -38
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/tracing_v3/storage/config.py +47 -13
- synth_ai/tracing_v3/storage/factory.py +3 -3
- synth_ai/tracing_v3/turso/daemon.py +113 -11
- synth_ai/tracing_v3/turso/native_manager.py +92 -16
- synth_ai/types.py +8 -0
- synth_ai/urls.py +11 -0
- synth_ai/utils/__init__.py +30 -1
- synth_ai/utils/agents.py +74 -0
- synth_ai/utils/bin.py +39 -0
- synth_ai/utils/cli.py +149 -5
- synth_ai/utils/env.py +40 -33
- synth_ai/utils/http.py +4 -1
- synth_ai/utils/json.py +72 -0
- synth_ai/utils/modal.py +285 -3
- synth_ai/utils/paths.py +48 -0
- synth_ai/utils/uvicorn.py +113 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/METADATA +109 -6
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/RECORD +291 -142
- examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
- synth_ai/cli/tui.py +0 -62
- synth_ai/tui/__init__.py +0 -5
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -164
- synth_ai/tui/cli/query_experiments_v3.py +0 -164
- synth_ai/tui/dashboard.py +0 -911
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/top_level.txt +0 -0
synth_ai/tracing_v3/config.py
CHANGED
|
@@ -1,19 +1,162 @@
|
|
|
1
|
-
"""Configuration for tracing v3
|
|
1
|
+
"""Configuration helpers for tracing v3.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
This module centralises the logic for discovering which datastore the tracer
|
|
4
|
+
should use. Historically the project defaulted to a local SQLite file which
|
|
5
|
+
breaks under parallel load. The new resolver inspects environment variables
|
|
6
|
+
and defaults to Turso/libSQL whenever credentials are supplied, while keeping a
|
|
7
|
+
SQLite fallback for contributors without remote access.
|
|
8
|
+
"""
|
|
5
9
|
|
|
6
|
-
from
|
|
10
|
+
from __future__ import annotations
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
|
9
17
|
|
|
18
|
+
from synth_ai.tracing_v3.constants import canonical_trace_db_path
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
# STARTUP DIAGNOSTIC - Commented out to reduce noise
|
|
21
|
+
# print(f"[TRACING_V3_CONFIG_LOADED] Python={sys.version_info.major}.{sys.version_info.minor} MODAL_IS_REMOTE={os.getenv('MODAL_IS_REMOTE')}", flush=True)
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# DSN resolution helpers
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
_CANONICAL_DB_PATH = canonical_trace_db_path()
|
|
28
|
+
_DEFAULT_TRACE_DIR = Path(os.getenv("SYNTH_TRACES_DIR", _CANONICAL_DB_PATH.parent))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalise_path(path: Path) -> Path:
|
|
32
|
+
"""Resolve relative paths and expand user/home markers."""
|
|
33
|
+
path = path.expanduser()
|
|
34
|
+
if not path.is_absolute():
|
|
35
|
+
path = (Path.cwd() / path).resolve()
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_modal_environment() -> bool:
|
|
40
|
+
"""Detect if running in Modal container.
|
|
41
|
+
|
|
42
|
+
Modal automatically sets MODAL_IS_REMOTE=1 in all deployed containers.
|
|
43
|
+
We check this first, then fall back to other Modal env vars.
|
|
44
|
+
"""
|
|
45
|
+
# Modal sets this in all deployed containers
|
|
46
|
+
if os.getenv("MODAL_IS_REMOTE") == "1":
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
# Additional Modal env vars as fallback
|
|
50
|
+
return bool(
|
|
51
|
+
os.getenv("MODAL_TASK_ID")
|
|
52
|
+
or os.getenv("MODAL_ENVIRONMENT")
|
|
53
|
+
or os.getenv("SERVICE", "").upper() == "MODAL"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _split_auth_from_url(url: str) -> tuple[str, str | None]:
|
|
58
|
+
"""Strip any auth_token query parameter from a DSN."""
|
|
59
|
+
parsed = urlparse(url)
|
|
60
|
+
if not parsed.query:
|
|
61
|
+
return url, None
|
|
62
|
+
|
|
63
|
+
params = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
|
64
|
+
token = params.pop("auth_token", None)
|
|
65
|
+
query = urlencode(params, doseq=True)
|
|
66
|
+
# urlunparse will omit the '?' automatically when query is empty
|
|
67
|
+
sanitised = urlunparse(parsed._replace(query=query))
|
|
68
|
+
return sanitised, token
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _default_sqlite_url(*, ensure_dir: bool = True) -> tuple[str, str | None]:
|
|
72
|
+
"""Generate a SQLite URL from SYNTH_TRACES_DIR if set, otherwise raise."""
|
|
73
|
+
traces_dir = os.getenv("SYNTH_TRACES_DIR")
|
|
74
|
+
if traces_dir:
|
|
75
|
+
dir_path = _normalise_path(Path(traces_dir))
|
|
76
|
+
if ensure_dir:
|
|
77
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
db_path = dir_path / "synth_traces.db"
|
|
79
|
+
sqlite_url = f"sqlite+aiosqlite:///{db_path}"
|
|
80
|
+
return sqlite_url, None
|
|
81
|
+
raise RuntimeError("SQLite fallback is disabled; configure LIBSQL_URL or run sqld locally.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_trace_db_settings(*, ensure_dir: bool = True) -> tuple[str, str | None]:
|
|
85
|
+
"""Resolve the tracing database URL and optional auth token.
|
|
86
|
+
|
|
87
|
+
Resolution order:
|
|
88
|
+
1. `SYNTH_TRACES_DB` (explicit DSN override)
|
|
89
|
+
2. `LIBSQL_URL` / `TURSO_DATABASE_URL` (remote libSQL endpoints)
|
|
90
|
+
3. `TURSO_LOCAL_DB_URL` (legacy env for local sqld)
|
|
91
|
+
4. Modal environment: plain SQLite file (no sqld, no auth)
|
|
92
|
+
5. Local dev: sqld default
|
|
93
|
+
"""
|
|
94
|
+
import logging
|
|
95
|
+
logger = logging.getLogger(__name__)
|
|
96
|
+
|
|
97
|
+
explicit = os.getenv("SYNTH_TRACES_DB")
|
|
98
|
+
if explicit:
|
|
99
|
+
logger.info(f"[TRACE_CONFIG] Using explicit SYNTH_TRACES_DB: {explicit}")
|
|
100
|
+
return _split_auth_from_url(explicit)
|
|
101
|
+
|
|
102
|
+
remote = os.getenv("LIBSQL_URL") or os.getenv("TURSO_DATABASE_URL")
|
|
103
|
+
if remote:
|
|
104
|
+
logger.info(f"[TRACE_CONFIG] Using remote Turso: {remote}")
|
|
105
|
+
url, token = _split_auth_from_url(remote)
|
|
106
|
+
if token:
|
|
107
|
+
return url, token
|
|
108
|
+
env_token = os.getenv("LIBSQL_AUTH_TOKEN") or os.getenv("TURSO_AUTH_TOKEN")
|
|
109
|
+
return url, env_token
|
|
110
|
+
|
|
111
|
+
local_override = os.getenv("TURSO_LOCAL_DB_URL")
|
|
112
|
+
if local_override:
|
|
113
|
+
logger.info(f"[TRACE_CONFIG] Using TURSO_LOCAL_DB_URL: {local_override}")
|
|
114
|
+
url, token = _split_auth_from_url(local_override)
|
|
115
|
+
if token:
|
|
116
|
+
return url, token
|
|
117
|
+
env_token = os.getenv("LIBSQL_AUTH_TOKEN") or os.getenv("TURSO_AUTH_TOKEN")
|
|
118
|
+
return url, env_token
|
|
119
|
+
|
|
120
|
+
# Check for SYNTH_TRACES_DIR to generate SQLite URL
|
|
121
|
+
traces_dir = os.getenv("SYNTH_TRACES_DIR")
|
|
122
|
+
if traces_dir:
|
|
123
|
+
try:
|
|
124
|
+
sqlite_url, _ = _default_sqlite_url(ensure_dir=ensure_dir)
|
|
125
|
+
logger.info(f"[TRACE_CONFIG] Using SQLite from SYNTH_TRACES_DIR: {sqlite_url}")
|
|
126
|
+
return sqlite_url, None
|
|
127
|
+
except RuntimeError:
|
|
128
|
+
pass # Fall through to other options
|
|
129
|
+
|
|
130
|
+
# Modal environment: use plain SQLite file (no sqld daemon, no auth required)
|
|
131
|
+
is_modal = _is_modal_environment()
|
|
132
|
+
logger.info(f"[TRACE_CONFIG] Modal detection: {is_modal} (MODAL_IS_REMOTE={os.getenv('MODAL_IS_REMOTE')})")
|
|
133
|
+
if is_modal:
|
|
134
|
+
logger.info("[TRACE_CONFIG] Using Modal SQLite: file:/tmp/synth_traces.db")
|
|
135
|
+
return "file:/tmp/synth_traces.db", None
|
|
136
|
+
|
|
137
|
+
# Local dev: default to sqld HTTP API
|
|
138
|
+
default_url = os.getenv("LIBSQL_DEFAULT_URL", "http://127.0.0.1:8081")
|
|
139
|
+
logger.info(f"[TRACE_CONFIG] Using local sqld: {default_url}")
|
|
140
|
+
return default_url, None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def resolve_trace_db_url(*, ensure_dir: bool = True) -> str:
|
|
144
|
+
"""Return just the DSN, discarding any auth token."""
|
|
145
|
+
url, _ = resolve_trace_db_settings(ensure_dir=ensure_dir)
|
|
146
|
+
return url
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def resolve_trace_db_auth_token() -> str | None:
|
|
150
|
+
"""Return the resolved auth token for the tracing datastore."""
|
|
151
|
+
_, token = resolve_trace_db_settings()
|
|
152
|
+
return token
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Config dataclasses
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
DEFAULT_DB_FILE = str(_normalise_path(_DEFAULT_TRACE_DIR) / _CANONICAL_DB_PATH.name)
|
|
17
160
|
|
|
18
161
|
|
|
19
162
|
@dataclass
|
|
@@ -24,12 +167,12 @@ class TursoConfig:
|
|
|
24
167
|
DEFAULT_DB_FILE = DEFAULT_DB_FILE
|
|
25
168
|
DEFAULT_HTTP_PORT = 8080
|
|
26
169
|
|
|
27
|
-
#
|
|
28
|
-
db_url: str =
|
|
170
|
+
# Resolve DB URL and auth token from environment (libSQL preferred)
|
|
171
|
+
db_url: str = field(default_factory=resolve_trace_db_url)
|
|
29
172
|
|
|
30
173
|
# Remote database sync configuration
|
|
31
|
-
sync_url: str = os.getenv("
|
|
32
|
-
auth_token: str =
|
|
174
|
+
sync_url: str = os.getenv("LIBSQL_SYNC_URL") or os.getenv("TURSO_SYNC_URL", "")
|
|
175
|
+
auth_token: str = resolve_trace_db_auth_token() or ""
|
|
33
176
|
sync_interval: int = int(
|
|
34
177
|
os.getenv("TURSO_SYNC_SECONDS", "2")
|
|
35
178
|
) # 2 seconds for responsive local development
|
|
@@ -54,16 +197,16 @@ class TursoConfig:
|
|
|
54
197
|
sqld_http_port: int = int(os.getenv("SQLD_HTTP_PORT", "8080"))
|
|
55
198
|
sqld_idle_shutdown: int = int(os.getenv("SQLD_IDLE_SHUTDOWN", "0")) # 0 = no idle shutdown
|
|
56
199
|
|
|
57
|
-
def get_connect_args(self) -> dict:
|
|
200
|
+
def get_connect_args(self) -> dict[str, str]:
|
|
58
201
|
"""Get SQLAlchemy connection arguments."""
|
|
59
|
-
args = {}
|
|
202
|
+
args: dict[str, str] = {}
|
|
60
203
|
if self.auth_token:
|
|
61
204
|
args["auth_token"] = self.auth_token
|
|
62
205
|
return args
|
|
63
206
|
|
|
64
|
-
def get_engine_kwargs(self) -> dict:
|
|
207
|
+
def get_engine_kwargs(self) -> dict[str, Any]:
|
|
65
208
|
"""Get SQLAlchemy engine creation kwargs."""
|
|
66
|
-
kwargs = {
|
|
209
|
+
kwargs: dict[str, Any] = {
|
|
67
210
|
"echo": self.echo_sql,
|
|
68
211
|
"future": True,
|
|
69
212
|
}
|
synth_ai/tracing_v3/constants.py
CHANGED
synth_ai/tracing_v3/db_config.py
CHANGED
|
@@ -30,11 +30,12 @@ class DatabaseConfig:
|
|
|
30
30
|
|
|
31
31
|
Args:
|
|
32
32
|
db_path: Path to database file. If None, uses DEFAULT_DB_FILE from serve.sh.
|
|
33
|
-
http_port:
|
|
33
|
+
http_port: Hrana WebSocket port for sqld daemon (env: SQLD_HTTP_PORT). If None, uses DEFAULT_HTTP_PORT.
|
|
34
34
|
use_sqld: Whether to use sqld daemon or direct SQLite.
|
|
35
35
|
"""
|
|
36
36
|
self.use_sqld = use_sqld and self._sqld_binary_available()
|
|
37
|
-
|
|
37
|
+
# Note: SQLD_HTTP_PORT is actually the hrana port (8080), not the HTTP API port
|
|
38
|
+
self.hrana_port = http_port or int(os.getenv("SQLD_HTTP_PORT", self.DEFAULT_HTTP_PORT))
|
|
38
39
|
self._daemon: SqldDaemon | None = None
|
|
39
40
|
|
|
40
41
|
# Set up database path to match serve.sh configuration
|
|
@@ -57,21 +58,16 @@ class DatabaseConfig:
|
|
|
57
58
|
abs_path = os.path.abspath(self.db_file)
|
|
58
59
|
sqld_data_path = os.path.join(abs_path, "dbs", "default", "data")
|
|
59
60
|
|
|
60
|
-
if os.path.exists(sqld_data_path):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
else:
|
|
65
|
-
# Direct SQLite file
|
|
66
|
-
if not os.path.exists(abs_path):
|
|
67
|
-
logger.debug(f"⚠️ Database file not found at: {abs_path}")
|
|
68
|
-
logger.debug("🔧 Make sure to run './serve.sh' to start the turso/sqld service")
|
|
69
|
-
else:
|
|
70
|
-
logger.debug(f"📁 Using direct SQLite file at: {abs_path}")
|
|
71
|
-
actual_db_path = abs_path
|
|
61
|
+
if not os.path.exists(sqld_data_path) and not os.path.exists(abs_path):
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
"sqld data directory not found. Run `sqld --db-path <path>` before using the tracing database."
|
|
64
|
+
)
|
|
72
65
|
|
|
73
|
-
#
|
|
74
|
-
|
|
66
|
+
# Use http:// for local sqld HTTP API port
|
|
67
|
+
# sqld has two ports: hrana_port (Hrana WebSocket) and hrana_port+1 (HTTP API)
|
|
68
|
+
# Python libsql client uses HTTP API with http:// URLs
|
|
69
|
+
http_api_port = self.hrana_port + 1
|
|
70
|
+
return f"http://127.0.0.1:{http_api_port}"
|
|
75
71
|
|
|
76
72
|
def _sqld_binary_available(self) -> bool:
|
|
77
73
|
"""Check if the sqld (Turso) binary is available on PATH."""
|
|
@@ -84,18 +80,12 @@ class DatabaseConfig:
|
|
|
84
80
|
return True
|
|
85
81
|
|
|
86
82
|
if binary_override:
|
|
87
|
-
|
|
88
|
-
"Configured SQLD_BINARY='
|
|
89
|
-
"Falling back to direct SQLite.",
|
|
90
|
-
binary_override,
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
f"Configured SQLD_BINARY='{binary_override}' but the executable was not found on PATH."
|
|
91
85
|
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"Install Turso's sqld or set SQLD_BINARY to enable the Turso daemon."
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
return False
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
"sqld binary not detected; install Turso's sqld or set SQLD_BINARY so that libSQL can be used."
|
|
88
|
+
)
|
|
99
89
|
|
|
100
90
|
def start_daemon(self, wait_time: float = 2.0):
|
|
101
91
|
"""
|
|
@@ -114,7 +104,7 @@ class DatabaseConfig:
|
|
|
114
104
|
# Import here to avoid circular dependency
|
|
115
105
|
from .turso.daemon import SqldDaemon
|
|
116
106
|
|
|
117
|
-
self._daemon = SqldDaemon(db_path=self.db_base_path,
|
|
107
|
+
self._daemon = SqldDaemon(db_path=self.db_base_path, hrana_port=self.hrana_port)
|
|
118
108
|
|
|
119
109
|
self._daemon.start()
|
|
120
110
|
|
|
@@ -160,11 +150,13 @@ def get_default_db_config() -> DatabaseConfig:
|
|
|
160
150
|
# Check if sqld is already running (started by serve.sh)
|
|
161
151
|
import subprocess
|
|
162
152
|
|
|
163
|
-
|
|
153
|
+
sqld_hrana_port = int(os.getenv("SQLD_HTTP_PORT", DatabaseConfig.DEFAULT_HTTP_PORT))
|
|
154
|
+
sqld_http_port = sqld_hrana_port + 1
|
|
164
155
|
sqld_running = False
|
|
165
156
|
try:
|
|
157
|
+
# Check for either hrana or http port in the process command line
|
|
166
158
|
result = subprocess.run(
|
|
167
|
-
["pgrep", "-f", f"sqld
|
|
159
|
+
["pgrep", "-f", f"sqld.*(--hrana-listen-addr.*:{sqld_hrana_port}|--http-listen-addr.*:{sqld_http_port})"],
|
|
168
160
|
capture_output=True,
|
|
169
161
|
text=True,
|
|
170
162
|
)
|
|
@@ -172,18 +164,12 @@ def get_default_db_config() -> DatabaseConfig:
|
|
|
172
164
|
# sqld is already running, don't start a new one
|
|
173
165
|
sqld_running = True
|
|
174
166
|
use_sqld = False
|
|
175
|
-
logger.debug(f"✅ Detected sqld already running on
|
|
167
|
+
logger.debug(f"✅ Detected sqld already running on ports {sqld_hrana_port} (hrana) and {sqld_http_port} (http)")
|
|
176
168
|
except Exception as e:
|
|
177
169
|
logger.debug(f"Could not check for sqld process: {e}")
|
|
178
170
|
|
|
179
171
|
if not sqld_running and use_sqld:
|
|
180
|
-
logger.warning("
|
|
181
|
-
logger.warning("🔧 Please start the turso/sqld service by running:")
|
|
182
|
-
logger.warning(" ./serve.sh")
|
|
183
|
-
logger.warning("")
|
|
184
|
-
logger.warning("This will start:")
|
|
185
|
-
logger.warning(" - sqld daemon (SQLite server) on port 8080")
|
|
186
|
-
logger.warning(" - Environment service on port 8901")
|
|
172
|
+
logger.warning("sqld service not detected. Start the Turso daemon (./serve.sh) before running tracing workloads.")
|
|
187
173
|
|
|
188
174
|
_default_config = DatabaseConfig(db_path=db_path, use_sqld=use_sqld)
|
|
189
175
|
|
|
@@ -68,7 +68,7 @@ def categorize_files(v2_files: list[tuple[str, list[str]]]) -> dict:
|
|
|
68
68
|
categories["examples"].append((file_path, imports))
|
|
69
69
|
elif any(
|
|
70
70
|
core in file_path
|
|
71
|
-
for core in ["synth_ai/lm/", "synth_ai/
|
|
71
|
+
for core in ["synth_ai/lm/", "synth_ai/environments/"]
|
|
72
72
|
):
|
|
73
73
|
categories["core_library"].append((file_path, imports))
|
|
74
74
|
else:
|
|
@@ -104,7 +104,6 @@ def print_migration_report():
|
|
|
104
104
|
print("2. Debug scripts: Can be deleted or archived")
|
|
105
105
|
print("3. Core library files: Need careful migration to v3")
|
|
106
106
|
print(" - synth_ai/lm/core/main_v2.py")
|
|
107
|
-
print(" - synth_ai/tui/cli/query_experiments.py")
|
|
108
107
|
print(" - synth_ai/environments/service/core_routes.py")
|
|
109
108
|
print("4. Examples: Should be updated to demonstrate v3 usage")
|
|
110
109
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Storage configuration for tracing v3."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from ..config import resolve_trace_db_auth_token, resolve_trace_db_settings
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class StorageBackend(str, Enum):
|
|
10
12
|
"""Supported storage backends."""
|
|
@@ -24,12 +26,9 @@ def _is_enabled(value: str | None) -> bool:
|
|
|
24
26
|
class StorageConfig:
|
|
25
27
|
"""Configuration for storage backend."""
|
|
26
28
|
|
|
27
|
-
backend: StorageBackend = StorageBackend.TURSO_NATIVE
|
|
28
29
|
connection_string: str | None = None
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
turso_url: str = os.getenv("TURSO_DATABASE_URL", "sqlite+libsql://http://127.0.0.1:8080")
|
|
32
|
-
turso_auth_token: str = os.getenv("TURSO_AUTH_TOKEN", "")
|
|
30
|
+
backend: StorageBackend | None = None
|
|
31
|
+
turso_auth_token: str | None = field(default=None)
|
|
33
32
|
|
|
34
33
|
# Common settings
|
|
35
34
|
pool_size: int = int(os.getenv("STORAGE_POOL_SIZE", "8"))
|
|
@@ -44,9 +43,48 @@ class StorageConfig:
|
|
|
44
43
|
# Allow legacy override while keeping compatibility with existing TURSO_NATIVE env flag
|
|
45
44
|
native_env = os.getenv("TURSO_NATIVE")
|
|
46
45
|
native_flag = _is_enabled(native_env) if native_env is not None else None
|
|
46
|
+
resolved_url: str | None = self.connection_string
|
|
47
|
+
resolved_token: str | None = self.turso_auth_token
|
|
48
|
+
|
|
49
|
+
if resolved_url is None:
|
|
50
|
+
resolved_url, inferred_token = resolve_trace_db_settings()
|
|
51
|
+
self.connection_string = resolved_url
|
|
52
|
+
resolved_token = inferred_token
|
|
53
|
+
|
|
54
|
+
if resolved_token is None:
|
|
55
|
+
resolved_token = resolve_trace_db_auth_token()
|
|
56
|
+
|
|
57
|
+
self.turso_auth_token = resolved_token or ""
|
|
58
|
+
|
|
59
|
+
if self.backend is None:
|
|
60
|
+
self.backend = self._infer_backend(self.connection_string or "")
|
|
47
61
|
|
|
48
62
|
if native_flag is False:
|
|
49
|
-
|
|
63
|
+
raise RuntimeError("TURSO_NATIVE=false is no longer supported; only Turso/libSQL backend is available.")
|
|
64
|
+
|
|
65
|
+
# Allow both TURSO_NATIVE and SQLITE backends (both use libsql.connect)
|
|
66
|
+
if self.backend not in (StorageBackend.TURSO_NATIVE, StorageBackend.SQLITE):
|
|
67
|
+
raise RuntimeError(f"Unsupported backend: {self.backend}. Only Turso/libSQL and SQLite are supported.")
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _infer_backend(connection_string: str) -> StorageBackend:
|
|
71
|
+
"""Infer backend type from the connection string."""
|
|
72
|
+
scheme = connection_string.split(":", 1)[0].lower()
|
|
73
|
+
|
|
74
|
+
# Plain SQLite files: file://, /absolute/path, or no scheme
|
|
75
|
+
if (
|
|
76
|
+
scheme == "file"
|
|
77
|
+
or scheme.startswith("sqlite")
|
|
78
|
+
or connection_string.startswith("/")
|
|
79
|
+
or "://" not in connection_string
|
|
80
|
+
):
|
|
81
|
+
return StorageBackend.SQLITE
|
|
82
|
+
|
|
83
|
+
# Turso/sqld: libsql://, http://, https://
|
|
84
|
+
if scheme.startswith("libsql") or "libsql" in scheme or scheme in ("http", "https"):
|
|
85
|
+
return StorageBackend.TURSO_NATIVE
|
|
86
|
+
|
|
87
|
+
raise RuntimeError(f"Unsupported tracing backend scheme: {scheme}")
|
|
50
88
|
|
|
51
89
|
def get_connection_string(self) -> str:
|
|
52
90
|
"""Get the appropriate connection string for the backend."""
|
|
@@ -54,12 +92,8 @@ class StorageConfig:
|
|
|
54
92
|
return self.connection_string
|
|
55
93
|
|
|
56
94
|
if self.backend == StorageBackend.TURSO_NATIVE:
|
|
57
|
-
return self.
|
|
58
|
-
|
|
59
|
-
return "sqlite+aiosqlite:///traces.db"
|
|
60
|
-
if self.backend == StorageBackend.POSTGRES:
|
|
61
|
-
return os.getenv("POSTGRES_URL", "postgresql+asyncpg://localhost/traces")
|
|
62
|
-
raise ValueError(f"Unknown backend: {self.backend}")
|
|
95
|
+
return self.connection_string or ""
|
|
96
|
+
raise ValueError(f"Unsupported backend: {self.backend}")
|
|
63
97
|
|
|
64
98
|
def get_backend_config(self) -> dict[str, Any]:
|
|
65
99
|
"""Get backend-specific configuration."""
|
|
@@ -24,14 +24,14 @@ def create_storage(config: StorageConfig | None = None) -> TraceStorage:
|
|
|
24
24
|
|
|
25
25
|
connection_string = config.get_connection_string()
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
# Both TURSO_NATIVE and SQLITE use NativeLibsqlTraceManager
|
|
28
|
+
# because libsql.connect() handles both remote and local file databases
|
|
29
|
+
if config.backend in (StorageBackend.TURSO_NATIVE, StorageBackend.SQLITE):
|
|
28
30
|
backend_config = config.get_backend_config()
|
|
29
31
|
return NativeLibsqlTraceManager(
|
|
30
32
|
db_url=connection_string,
|
|
31
33
|
auth_token=backend_config.get("auth_token"),
|
|
32
34
|
)
|
|
33
|
-
elif config.backend == StorageBackend.SQLITE:
|
|
34
|
-
return NativeLibsqlTraceManager(db_url=connection_string)
|
|
35
35
|
elif config.backend == StorageBackend.POSTGRES:
|
|
36
36
|
# Future: PostgreSQL implementation
|
|
37
37
|
raise NotImplementedError("PostgreSQL backend not yet implemented")
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""sqld daemon management utilities."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
3
5
|
import pathlib
|
|
4
6
|
import shutil
|
|
5
7
|
import subprocess
|
|
8
|
+
import sys
|
|
6
9
|
import time
|
|
7
10
|
|
|
8
11
|
import requests
|
|
@@ -10,6 +13,8 @@ from requests import RequestException
|
|
|
10
13
|
|
|
11
14
|
from ..config import CONFIG
|
|
12
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
class SqldDaemon:
|
|
15
20
|
"""Manages local sqld daemon lifecycle."""
|
|
@@ -18,28 +23,101 @@ class SqldDaemon:
|
|
|
18
23
|
self,
|
|
19
24
|
db_path: str | None = None,
|
|
20
25
|
http_port: int | None = None,
|
|
26
|
+
hrana_port: int | None = None,
|
|
21
27
|
binary_path: str | None = None,
|
|
22
28
|
):
|
|
23
29
|
"""Initialize sqld daemon manager.
|
|
24
30
|
|
|
25
31
|
Args:
|
|
26
32
|
db_path: Path to database file (uses config default if not provided)
|
|
27
|
-
http_port: HTTP port for
|
|
33
|
+
http_port: HTTP port for health/API (uses config default + 1 if not provided)
|
|
34
|
+
hrana_port: Hrana WebSocket port for libsql connections (uses config default if not provided)
|
|
28
35
|
binary_path: Path to sqld binary (auto-detected if not provided)
|
|
29
36
|
"""
|
|
30
37
|
self.db_path = db_path or CONFIG.sqld_db_path
|
|
31
|
-
self.
|
|
38
|
+
self.hrana_port = hrana_port or CONFIG.sqld_http_port # Main port for libsql://
|
|
39
|
+
self.http_port = http_port or (self.hrana_port + 1) # HTTP API on next port
|
|
32
40
|
self.binary_path = binary_path or self._find_binary()
|
|
33
41
|
self.process: subprocess.Popen[str] | None = None
|
|
34
42
|
|
|
35
43
|
def _find_binary(self) -> str:
|
|
36
|
-
"""Find sqld binary in PATH.
|
|
44
|
+
"""Find sqld binary in PATH, auto-installing if needed.
|
|
45
|
+
|
|
46
|
+
Search order:
|
|
47
|
+
1. CONFIG.sqld_binary in PATH
|
|
48
|
+
2. libsql-server in PATH
|
|
49
|
+
3. Common install locations (~/.turso/bin, /usr/local/bin, etc.)
|
|
50
|
+
4. Auto-install via synth_ai.utils.sqld (if interactive terminal)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Path to sqld binary
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
RuntimeError: If binary not found and auto-install fails/disabled
|
|
57
|
+
"""
|
|
58
|
+
# Check PATH first
|
|
37
59
|
binary = shutil.which(CONFIG.sqld_binary) or shutil.which("libsql-server")
|
|
38
|
-
if
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
if binary:
|
|
61
|
+
logger.debug(f"Found sqld binary in PATH: {binary}")
|
|
62
|
+
return binary
|
|
63
|
+
|
|
64
|
+
# Check common install locations
|
|
65
|
+
try:
|
|
66
|
+
from synth_ai.utils.sqld import find_sqld_binary
|
|
67
|
+
binary = find_sqld_binary()
|
|
68
|
+
if binary:
|
|
69
|
+
logger.debug(f"Found sqld binary in common location: {binary}")
|
|
70
|
+
return binary
|
|
71
|
+
except ImportError:
|
|
72
|
+
logger.debug("synth_ai.utils.sqld not available, skipping common location check")
|
|
73
|
+
|
|
74
|
+
# Try auto-install if enabled and interactive
|
|
75
|
+
auto_install_enabled = os.getenv("SYNTH_AI_AUTO_INSTALL_SQLD", "true").lower() == "true"
|
|
76
|
+
|
|
77
|
+
if auto_install_enabled and sys.stdin.isatty():
|
|
78
|
+
try:
|
|
79
|
+
from synth_ai.utils.sqld import install_sqld
|
|
80
|
+
logger.info("sqld binary not found. Attempting automatic installation...")
|
|
81
|
+
|
|
82
|
+
# Use click if available for better UX, otherwise proceed automatically
|
|
83
|
+
try:
|
|
84
|
+
import click
|
|
85
|
+
if not click.confirm(
|
|
86
|
+
"sqld not found. Install automatically via Homebrew?",
|
|
87
|
+
default=True
|
|
88
|
+
):
|
|
89
|
+
raise RuntimeError("User declined automatic installation")
|
|
90
|
+
except ImportError:
|
|
91
|
+
# click not available, auto-install without prompt
|
|
92
|
+
logger.info("Installing sqld automatically (non-interactive mode)")
|
|
93
|
+
|
|
94
|
+
binary = install_sqld()
|
|
95
|
+
logger.info(f"Successfully installed sqld to: {binary}")
|
|
96
|
+
return binary
|
|
97
|
+
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
logger.warning(f"Auto-install failed: {exc}")
|
|
100
|
+
# Fall through to error message below
|
|
101
|
+
elif not auto_install_enabled:
|
|
102
|
+
logger.debug("Auto-install disabled via SYNTH_AI_AUTO_INSTALL_SQLD=false")
|
|
103
|
+
elif not sys.stdin.isatty():
|
|
104
|
+
logger.debug("Non-interactive terminal, skipping auto-install prompt")
|
|
105
|
+
|
|
106
|
+
# If we get here, all methods failed
|
|
107
|
+
raise RuntimeError(
|
|
108
|
+
"sqld binary not found. Install using one of these methods:\n"
|
|
109
|
+
"\n"
|
|
110
|
+
"Quick install (recommended):\n"
|
|
111
|
+
" synth-ai turso\n"
|
|
112
|
+
"\n"
|
|
113
|
+
"Manual install:\n"
|
|
114
|
+
" brew install turso-tech/tools/sqld\n"
|
|
115
|
+
" # or\n"
|
|
116
|
+
" curl -sSfL https://get.tur.so/install.sh | bash && turso dev\n"
|
|
117
|
+
"\n"
|
|
118
|
+
"For CI/CD environments:\n"
|
|
119
|
+
" Set SYNTH_AI_AUTO_INSTALL_SQLD=false and pre-install sqld"
|
|
120
|
+
)
|
|
43
121
|
|
|
44
122
|
def start(self, wait_for_ready: bool = True) -> subprocess.Popen:
|
|
45
123
|
"""Start the sqld daemon."""
|
|
@@ -53,6 +131,8 @@ class SqldDaemon:
|
|
|
53
131
|
self.binary_path,
|
|
54
132
|
"--db-path",
|
|
55
133
|
str(db_file),
|
|
134
|
+
"--hrana-listen-addr",
|
|
135
|
+
f"127.0.0.1:{self.hrana_port}",
|
|
56
136
|
"--http-listen-addr",
|
|
57
137
|
f"127.0.0.1:{self.http_port}",
|
|
58
138
|
]
|
|
@@ -112,6 +192,14 @@ class SqldDaemon:
|
|
|
112
192
|
"""Check if daemon is running."""
|
|
113
193
|
return self.process is not None and self.process.poll() is None
|
|
114
194
|
|
|
195
|
+
def get_hrana_port(self) -> int:
|
|
196
|
+
"""Get the Hrana WebSocket port for libsql:// connections."""
|
|
197
|
+
return self.hrana_port
|
|
198
|
+
|
|
199
|
+
def get_http_port(self) -> int:
|
|
200
|
+
"""Get the HTTP API port for health checks."""
|
|
201
|
+
return self.http_port
|
|
202
|
+
|
|
115
203
|
def __enter__(self):
|
|
116
204
|
"""Context manager entry."""
|
|
117
205
|
self.start()
|
|
@@ -126,13 +214,27 @@ class SqldDaemon:
|
|
|
126
214
|
_daemon: SqldDaemon | None = None
|
|
127
215
|
|
|
128
216
|
|
|
129
|
-
def start_sqld(
|
|
130
|
-
|
|
217
|
+
def start_sqld(
|
|
218
|
+
db_path: str | None = None,
|
|
219
|
+
port: int | None = None,
|
|
220
|
+
hrana_port: int | None = None,
|
|
221
|
+
http_port: int | None = None,
|
|
222
|
+
) -> SqldDaemon:
|
|
223
|
+
"""Start a global sqld daemon instance.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
db_path: Path to database file
|
|
227
|
+
port: Legacy parameter - used as hrana_port if hrana_port not specified
|
|
228
|
+
hrana_port: Hrana WebSocket port for libsql:// connections
|
|
229
|
+
http_port: HTTP API port for health checks
|
|
230
|
+
"""
|
|
131
231
|
global _daemon
|
|
132
232
|
if _daemon and _daemon.is_running():
|
|
133
233
|
return _daemon
|
|
134
234
|
|
|
135
|
-
|
|
235
|
+
# Support legacy 'port' parameter by using it as hrana_port
|
|
236
|
+
final_hrana_port = hrana_port or port
|
|
237
|
+
_daemon = SqldDaemon(db_path=db_path, hrana_port=final_hrana_port, http_port=http_port)
|
|
136
238
|
_daemon.start()
|
|
137
239
|
return _daemon
|
|
138
240
|
|