synth-ai 0.2.17__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/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/configs/eval_gpt5nano.toml +26 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +12 -10
- examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +1 -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/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_10x20.toml +26 -0
- examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +1 -1
- 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 +60 -10
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +1 -1
- examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -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 +1 -0
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -0
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -0
- examples/rl/configs/rl_from_base_qwen17.toml +1 -0
- examples/swe/task_app/hosted/inference/openai_client.py +0 -34
- examples/swe/task_app/hosted/policy_routes.py +17 -0
- examples/swe/task_app/hosted/rollout.py +4 -2
- 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/grpo_crafter.py +24 -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 +355 -58
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +68 -7
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +78 -21
- examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -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/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
- examples/task_apps/pokemon_red/task_app.py +254 -36
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +1 -0
- examples/warming_up_to_rl/task_app/grpo_crafter.py +53 -4
- 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 +152 -41
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +31 -1
- 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_qwen17.toml +1 -0
- synth_ai/api/train/builders.py +90 -1
- synth_ai/api/train/cli.py +396 -21
- synth_ai/api/train/config_finder.py +13 -2
- synth_ai/api/train/configs/__init__.py +15 -1
- synth_ai/api/train/configs/prompt_learning.py +442 -0
- synth_ai/api/train/configs/rl.py +29 -0
- synth_ai/api/train/task_app.py +1 -1
- synth_ai/api/train/validators.py +277 -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 +85 -17
- 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 +1 -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/eval/core.py +13 -10
- synth_ai/cli/commands/filter/core.py +53 -17
- synth_ai/cli/commands/help/core.py +0 -1
- synth_ai/cli/commands/smoke/__init__.py +7 -0
- synth_ai/cli/commands/smoke/core.py +1436 -0
- synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
- synth_ai/cli/commands/status/subcommands/usage.py +203 -0
- synth_ai/cli/commands/train/judge_schemas.py +1 -0
- synth_ai/cli/commands/train/judge_validation.py +1 -0
- synth_ai/cli/commands/train/validation.py +0 -57
- synth_ai/cli/demo.py +35 -3
- synth_ai/cli/deploy/__init__.py +40 -25
- synth_ai/cli/deploy.py +162 -0
- synth_ai/cli/legacy_root_backup.py +14 -8
- synth_ai/cli/opencode.py +107 -0
- synth_ai/cli/root.py +9 -5
- synth_ai/cli/task_app_deploy.py +1 -1
- synth_ai/cli/task_apps.py +53 -53
- 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/judge_schemas.py +1 -0
- 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/pricing/__init__.py +2 -0
- synth_ai/pricing/model_pricing.py +57 -0
- synth_ai/streaming/handlers.py +53 -4
- synth_ai/streaming/streamer.py +19 -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 +44 -8
- 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/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 +17 -17
- synth_ai/utils/json.py +72 -0
- synth_ai/utils/modal.py +283 -1
- synth_ai/utils/paths.py +48 -0
- synth_ai/utils/uvicorn.py +113 -0
- {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/METADATA +102 -4
- {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/RECORD +162 -88
- synth_ai/cli/commands/deploy/__init__.py +0 -23
- synth_ai/cli/commands/deploy/core.py +0 -614
- synth_ai/cli/commands/deploy/errors.py +0 -72
- synth_ai/cli/commands/deploy/validation.py +0 -11
- synth_ai/cli/deploy/core.py +0 -5
- synth_ai/cli/deploy/errors.py +0 -23
- synth_ai/cli/deploy/validation.py +0 -5
- {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
|
@@ -14,8 +14,11 @@ import re
|
|
|
14
14
|
from collections.abc import Callable
|
|
15
15
|
from dataclasses import asdict, dataclass
|
|
16
16
|
from datetime import UTC, datetime
|
|
17
|
+
from pathlib import Path
|
|
17
18
|
from typing import TYPE_CHECKING, Any, cast
|
|
19
|
+
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
|
18
20
|
|
|
21
|
+
import httpx
|
|
19
22
|
import libsql
|
|
20
23
|
from sqlalchemy.engine import make_url
|
|
21
24
|
|
|
@@ -60,36 +63,70 @@ class _ConnectionTarget:
|
|
|
60
63
|
auth_token: str | None = None
|
|
61
64
|
|
|
62
65
|
|
|
66
|
+
def _strip_auth_component(url: str) -> tuple[str, str | None]:
|
|
67
|
+
"""Remove auth_token query parameter from URL, returning the token separately."""
|
|
68
|
+
parsed = urlparse(url)
|
|
69
|
+
if not parsed.query:
|
|
70
|
+
return url, None
|
|
71
|
+
|
|
72
|
+
params = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
|
73
|
+
token = params.pop("auth_token", None)
|
|
74
|
+
query = urlencode(params, doseq=True)
|
|
75
|
+
sanitised = urlunparse(parsed._replace(query=query))
|
|
76
|
+
return sanitised, token
|
|
77
|
+
|
|
78
|
+
|
|
63
79
|
def _resolve_connection_target(db_url: str | None, auth_token: str | None) -> _ConnectionTarget:
|
|
64
80
|
"""Normalise the configured database URL."""
|
|
65
81
|
url = db_url or CONFIG.db_url
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if url.startswith("sqlite+aiosqlite:///"):
|
|
69
|
-
return _ConnectionTarget(database=url.replace("sqlite+aiosqlite:///", ""), auth_token=auth_token)
|
|
82
|
+
sanitised, token_from_url = _strip_auth_component(url)
|
|
83
|
+
effective_token = auth_token or token_from_url or CONFIG.auth_token
|
|
70
84
|
|
|
71
85
|
# SQLAlchemy-compatible libsql scheme (`sqlite+libsql://<endpoint or path>`)
|
|
72
|
-
if
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
if sanitised.startswith("sqlite+libsql://"):
|
|
87
|
+
raise RuntimeError("sqlite+libsql scheme is no longer supported; use libsql://")
|
|
88
|
+
|
|
89
|
+
# Plain SQLite files: file://, /absolute/path, or relative path
|
|
90
|
+
# libsql.connect() handles these without sync_url or auth_token
|
|
91
|
+
if sanitised.startswith("file://") or sanitised.startswith("/") or "://" not in sanitised:
|
|
92
|
+
# Strip file:// prefix if present, libsql.connect handles both formats
|
|
93
|
+
db_path = sanitised.replace("file://", "") if sanitised.startswith("file://") else sanitised
|
|
94
|
+
return _ConnectionTarget(database=db_path, sync_url=None, auth_token=None)
|
|
75
95
|
|
|
76
96
|
# Native libsql URLs (`libsql://...`).
|
|
77
|
-
if
|
|
78
|
-
return _ConnectionTarget(database=
|
|
97
|
+
if sanitised.startswith("libsql://"):
|
|
98
|
+
return _ConnectionTarget(database=sanitised, sync_url=sanitised, auth_token=effective_token)
|
|
79
99
|
|
|
80
100
|
# Fallback to SQLAlchemy URL parsing for anything else we missed.
|
|
81
101
|
try:
|
|
82
|
-
parsed = make_url(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
parsed = make_url(sanitised)
|
|
103
|
+
driver = parsed.drivername.lower()
|
|
104
|
+
if driver.startswith("sqlite"):
|
|
105
|
+
database = parsed.database or ""
|
|
106
|
+
if database and database not in {":memory:", ":memory"}:
|
|
107
|
+
# Absolute paths are passed through; relative paths are resolved to cwd
|
|
108
|
+
if database.startswith("/"):
|
|
109
|
+
db_path = database
|
|
110
|
+
else:
|
|
111
|
+
db_path = str(Path(database).expanduser().resolve())
|
|
112
|
+
elif database in {":memory:", ":memory"}:
|
|
113
|
+
db_path = ":memory:"
|
|
114
|
+
else:
|
|
115
|
+
raise RuntimeError("SQLite URL missing database path.")
|
|
116
|
+
return _ConnectionTarget(database=db_path, sync_url=None, auth_token=None)
|
|
117
|
+
if driver.startswith("libsql"):
|
|
86
118
|
database = parsed.render_as_string(hide_password=False)
|
|
87
|
-
return _ConnectionTarget(database=database, sync_url=database, auth_token=
|
|
119
|
+
return _ConnectionTarget(database=database, sync_url=database, auth_token=effective_token)
|
|
88
120
|
except Exception: # pragma: no cover - defensive guardrail
|
|
89
121
|
logger.debug("Unable to parse db_url via SQLAlchemy", exc_info=True)
|
|
90
122
|
|
|
91
|
-
#
|
|
92
|
-
|
|
123
|
+
# Python libsql client uses HTTP API for http:// URLs, not Hrana WebSocket
|
|
124
|
+
# For local sqld with http:// URL, we need to ensure it points to the HTTP API port
|
|
125
|
+
# sqld uses two ports: Hrana WebSocket (e.g. 8080) and HTTP API (e.g. 8081)
|
|
126
|
+
# libsql.connect() with http:// uses HTTP API, so URL should point to HTTP API port
|
|
127
|
+
if sanitised.startswith(("http://", "https://", "libsql://")):
|
|
128
|
+
return _ConnectionTarget(database=sanitised, sync_url=sanitised, auth_token=effective_token)
|
|
129
|
+
raise RuntimeError(f"Unsupported tracing database URL: {sanitised}")
|
|
93
130
|
|
|
94
131
|
|
|
95
132
|
def _json_dumps(value: Any) -> str | None:
|
|
@@ -350,6 +387,45 @@ class NativeLibsqlTraceManager(TraceStorage):
|
|
|
350
387
|
if self._initialized:
|
|
351
388
|
return
|
|
352
389
|
|
|
390
|
+
# Fast-fail preflight: if using remote endpoint or local sqld, check health
|
|
391
|
+
# Skip health check for plain SQLite files (sync_url is None)
|
|
392
|
+
if self._target.sync_url:
|
|
393
|
+
try:
|
|
394
|
+
parsed = urlparse(self._target.database or "")
|
|
395
|
+
# Check for local sqld: http://, https://, or libsql://
|
|
396
|
+
if parsed.scheme in ("http", "https", "libsql"):
|
|
397
|
+
host_port = parsed.netloc or ""
|
|
398
|
+
host = (host_port.split(":", 1)[0] or "").strip().lower()
|
|
399
|
+
if host in {"127.0.0.1", "localhost"} and host_port:
|
|
400
|
+
# For http:// URLs, the port should already be the HTTP API port
|
|
401
|
+
# For libsql:// URLs, we need to calculate health check port
|
|
402
|
+
if ":" in host_port:
|
|
403
|
+
port = int(host_port.split(":", 1)[1])
|
|
404
|
+
if parsed.scheme == "libsql":
|
|
405
|
+
# libsql:// uses Hrana port, health check is on HTTP API port (Hrana + 1)
|
|
406
|
+
health_url = f"http://{host}:{port + 1}/health"
|
|
407
|
+
else:
|
|
408
|
+
# http:// already points to HTTP API port
|
|
409
|
+
health_url = f"http://{host}:{port}/health"
|
|
410
|
+
else:
|
|
411
|
+
health_url = f"http://{host_port}/health"
|
|
412
|
+
try:
|
|
413
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(1.0)) as client:
|
|
414
|
+
resp = await client.get(health_url)
|
|
415
|
+
if resp.status_code != 200:
|
|
416
|
+
raise RuntimeError(
|
|
417
|
+
f"Tracing backend unhealthy at {health_url} (status={resp.status_code})"
|
|
418
|
+
)
|
|
419
|
+
except Exception as exc: # pragma: no cover - network env dependent
|
|
420
|
+
raise RuntimeError(
|
|
421
|
+
f"Tracing backend not reachable at {health_url}. "
|
|
422
|
+
f"Start sqld with both ports: sqld --db-path <path> --hrana-listen-addr {host}:HRANA_PORT --http-listen-addr {host}:HTTP_PORT "
|
|
423
|
+
f"or disable tracing (TASKAPP_TRACING_ENABLED=0)."
|
|
424
|
+
) from exc
|
|
425
|
+
except Exception:
|
|
426
|
+
# Propagate any preflight failure to abort early
|
|
427
|
+
raise
|
|
428
|
+
|
|
353
429
|
# Establish a libsql connection for future native operations.
|
|
354
430
|
self._conn = self._open_connection()
|
|
355
431
|
self._ensure_schema()
|
synth_ai/types.py
ADDED
synth_ai/urls.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Base URL for all backends
|
|
2
|
+
BACKEND_URL_BASE = "https://agent-learning.onrender.com"
|
|
3
|
+
|
|
4
|
+
# Synth Research API base (supports OpenAI, Anthropic, and custom formats)
|
|
5
|
+
# Real routes: /api/synth-research/chat/completions, /api/synth-research/messages
|
|
6
|
+
# V1 routes: /api/synth-research/v1/chat/completions, /api/synth-research/v1/messages
|
|
7
|
+
BACKEND_URL_SYNTH_RESEARCH_BASE = BACKEND_URL_BASE + "/api/synth-research"
|
|
8
|
+
|
|
9
|
+
# Provider-specific URLs (for SDKs that expect standard paths)
|
|
10
|
+
BACKEND_URL_SYNTH_RESEARCH_OPENAI = BACKEND_URL_SYNTH_RESEARCH_BASE + "/v1" # For OpenAI SDKs (appends /chat/completions)
|
|
11
|
+
BACKEND_URL_SYNTH_RESEARCH_ANTHROPIC = BACKEND_URL_SYNTH_RESEARCH_BASE # For Anthropic SDKs (appends /v1/messages)
|
synth_ai/utils/__init__.py
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
from . import task_app_state
|
|
2
|
+
from .agents import write_agents_md
|
|
2
3
|
from .base_url import PROD_BASE_URL_DEFAULT, get_backend_from_env, get_learning_v2_base_url
|
|
3
|
-
from .
|
|
4
|
+
from .bin import install_bin, verify_bin
|
|
5
|
+
from .cli import (
|
|
6
|
+
PromptedChoiceOption,
|
|
7
|
+
PromptedChoiceType,
|
|
8
|
+
PromptedPathOption,
|
|
9
|
+
print_next_step,
|
|
10
|
+
prompt_choice,
|
|
11
|
+
prompt_for_path,
|
|
12
|
+
)
|
|
4
13
|
from .env import mask_str, resolve_env_var, write_env_var_to_dotenv, write_env_var_to_json
|
|
5
14
|
from .http import AsyncHttpClient, HTTPError, http_request
|
|
15
|
+
from .json import create_and_write_json, load_json_to_dict, strip_json_comments
|
|
6
16
|
from .modal import (
|
|
7
17
|
ensure_modal_installed,
|
|
8
18
|
ensure_task_app_ready,
|
|
@@ -11,6 +21,12 @@ from .modal import (
|
|
|
11
21
|
is_modal_public_url,
|
|
12
22
|
normalize_endpoint_url,
|
|
13
23
|
)
|
|
24
|
+
from .paths import (
|
|
25
|
+
find_bin_path,
|
|
26
|
+
find_config_path,
|
|
27
|
+
get_env_file_paths,
|
|
28
|
+
get_home_config_file_paths,
|
|
29
|
+
)
|
|
14
30
|
from .process import ensure_local_port_available, popen_capture, popen_stream, popen_stream_capture
|
|
15
31
|
from .sqld import SQLD_VERSION, find_sqld_binary, install_sqld
|
|
16
32
|
from .task_app_discovery import AppChoice, discover_eval_config_paths, select_app_choice
|
|
@@ -50,8 +66,11 @@ __all__ = [
|
|
|
50
66
|
"PROD_BASE_URL_DEFAULT",
|
|
51
67
|
"PromptedChoiceOption",
|
|
52
68
|
"PromptedChoiceType",
|
|
69
|
+
"PromptedPathOption",
|
|
70
|
+
"prompt_for_path",
|
|
53
71
|
"SQLD_VERSION",
|
|
54
72
|
"USER_CONFIG_PATH",
|
|
73
|
+
"create_and_write_json",
|
|
55
74
|
"current_task_app_id",
|
|
56
75
|
"discover_eval_config_paths",
|
|
57
76
|
"ensure_env_credentials",
|
|
@@ -60,14 +79,20 @@ __all__ = [
|
|
|
60
79
|
"ensure_port_free",
|
|
61
80
|
"ensure_task_app_ready",
|
|
62
81
|
"find_asgi_apps",
|
|
82
|
+
"find_bin_path",
|
|
83
|
+
"find_config_path",
|
|
63
84
|
"find_sqld_binary",
|
|
64
85
|
"get_backend_from_env",
|
|
86
|
+
"get_env_file_paths",
|
|
87
|
+
"get_home_config_file_paths",
|
|
65
88
|
"get_learning_v2_base_url",
|
|
66
89
|
"http_request",
|
|
90
|
+
"install_bin",
|
|
67
91
|
"install_sqld",
|
|
68
92
|
"is_local_demo_url",
|
|
69
93
|
"is_modal_public_url",
|
|
70
94
|
"load_demo_dir",
|
|
95
|
+
"load_json_to_dict",
|
|
71
96
|
"load_template_id",
|
|
72
97
|
"load_user_config",
|
|
73
98
|
"load_user_env",
|
|
@@ -84,17 +109,21 @@ __all__ = [
|
|
|
84
109
|
"popen_stream_capture",
|
|
85
110
|
"preflight_env_key",
|
|
86
111
|
"print_next_step",
|
|
112
|
+
"prompt_choice",
|
|
87
113
|
"read_task_app_config",
|
|
88
114
|
"record_task_app",
|
|
89
115
|
"resolve_env_var",
|
|
90
116
|
"resolve_task_app_entry",
|
|
91
117
|
"save_user_config",
|
|
92
118
|
"select_app_choice",
|
|
119
|
+
"strip_json_comments",
|
|
93
120
|
"task_app_config_path",
|
|
94
121
|
"task_app_id_from_path",
|
|
95
122
|
"task_app_state",
|
|
96
123
|
"update_task_app_entry",
|
|
97
124
|
"update_user_config",
|
|
125
|
+
"verify_bin",
|
|
126
|
+
'write_agents_md',
|
|
98
127
|
"write_env_var_to_dotenv",
|
|
99
128
|
"write_env_var_to_json",
|
|
100
129
|
"write_task_app_config",
|
synth_ai/utils/agents.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
AGENTS_TEXT = """
|
|
4
|
+
sinf
|
|
5
|
+
"""
|
|
6
|
+
SYNTH_DIV_START = "### --- SYNTH SECTION START ---"
|
|
7
|
+
SYNTH_DIV_END = "### ---- SYNTH SECTION END ----"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _render_block() -> str:
|
|
11
|
+
return f"{SYNTH_DIV_START}\n{AGENTS_TEXT}\n{SYNTH_DIV_END}"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _append_block(prefix: str) -> str:
|
|
15
|
+
prefix = prefix.rstrip()
|
|
16
|
+
block = _render_block()
|
|
17
|
+
if prefix:
|
|
18
|
+
return f"{prefix}\n\n{block}\n"
|
|
19
|
+
return f"{block}\n"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def write_agents_md() -> None:
|
|
23
|
+
path = Path.cwd() / "AGENTS.md"
|
|
24
|
+
if not path.exists():
|
|
25
|
+
path.write_text(_append_block(""), encoding="utf-8")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
file_text = path.read_text(encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
# Remove orphan end markers first (end markers without a preceding start marker)
|
|
31
|
+
cleaned = file_text
|
|
32
|
+
end_pos = cleaned.find(SYNTH_DIV_END)
|
|
33
|
+
start_pos = cleaned.find(SYNTH_DIV_START)
|
|
34
|
+
|
|
35
|
+
# If there's an end marker before any start marker
|
|
36
|
+
if end_pos != -1 and (start_pos == -1 or end_pos < start_pos):
|
|
37
|
+
if start_pos == -1:
|
|
38
|
+
# No start markers at all - remove everything including content before orphan
|
|
39
|
+
cleaned = cleaned[end_pos + len(SYNTH_DIV_END):].lstrip()
|
|
40
|
+
else:
|
|
41
|
+
# There are start markers after the orphan - preserve content before orphan
|
|
42
|
+
before_orphan = cleaned[:end_pos].rstrip()
|
|
43
|
+
after_orphan = cleaned[end_pos + len(SYNTH_DIV_END):].lstrip()
|
|
44
|
+
cleaned = "\n\n".join(filter(None, [before_orphan, after_orphan]))
|
|
45
|
+
|
|
46
|
+
# Find the first start and last end marker to consolidate multiple sections
|
|
47
|
+
first_start = cleaned.find(SYNTH_DIV_START)
|
|
48
|
+
last_end = cleaned.rfind(SYNTH_DIV_END)
|
|
49
|
+
|
|
50
|
+
if first_start != -1 and last_end != -1 and last_end > first_start:
|
|
51
|
+
# We have at least one valid section, consolidate all into one
|
|
52
|
+
before = cleaned[:first_start].rstrip()
|
|
53
|
+
after = cleaned[last_end + len(SYNTH_DIV_END):].lstrip()
|
|
54
|
+
|
|
55
|
+
parts: list[str] = []
|
|
56
|
+
if before:
|
|
57
|
+
parts.append(before)
|
|
58
|
+
parts.append(_render_block())
|
|
59
|
+
if after:
|
|
60
|
+
parts.append(after)
|
|
61
|
+
|
|
62
|
+
new_text = "\n\n".join(parts)
|
|
63
|
+
if not new_text.endswith("\n"):
|
|
64
|
+
new_text += "\n"
|
|
65
|
+
path.write_text(new_text, encoding="utf-8")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# No valid sections found, remove any remaining orphan markers
|
|
69
|
+
cleaned = cleaned.replace(SYNTH_DIV_END, "")
|
|
70
|
+
cleaned = cleaned.replace(AGENTS_TEXT, "")
|
|
71
|
+
cleaned = cleaned.strip()
|
|
72
|
+
if cleaned:
|
|
73
|
+
cleaned += "\n\n"
|
|
74
|
+
path.write_text(f"{cleaned}{_render_block()}\n", encoding="utf-8")
|
synth_ai/utils/bin.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .cli import prompt_choice
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def install_bin(name: str, install_options: list[str]) -> bool:
|
|
9
|
+
cmd = prompt_choice(
|
|
10
|
+
f"How would you like to install {name}?",
|
|
11
|
+
install_options
|
|
12
|
+
)
|
|
13
|
+
div_start = f"{'-' * 29} INSTALL START {'-' * 29}"
|
|
14
|
+
div_end = f"{'-' * 30} INSTALL END {'-' * 30}"
|
|
15
|
+
try:
|
|
16
|
+
print(f"Installing {name} via `{cmd}`")
|
|
17
|
+
print('\n' + div_start)
|
|
18
|
+
subprocess.run(shlex.split(cmd), check=True)
|
|
19
|
+
print(div_end + '\n')
|
|
20
|
+
return True
|
|
21
|
+
except subprocess.CalledProcessError as e:
|
|
22
|
+
print(f"Failed to install {name}: {e}")
|
|
23
|
+
print(div_end + '\n')
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def verify_bin(bin_path: Path) -> bool:
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
[str(bin_path), "--version"],
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
timeout=3,
|
|
34
|
+
check=False
|
|
35
|
+
)
|
|
36
|
+
return result.returncode == 0
|
|
37
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
38
|
+
print(e)
|
|
39
|
+
return False
|