synth-ai 0.2.4.dev7__py3-none-any.whl → 0.2.4.dev9__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.
- synth_ai/__init__.py +1 -1
- synth_ai/cli/__init__.py +6 -0
- synth_ai/cli/balance.py +3 -15
- synth_ai/cli/demo.py +68 -9
- synth_ai/cli/rl_demo.py +137 -0
- synth_ai/cli/root.py +65 -0
- synth_ai/config/base_url.py +47 -0
- synth_ai/demos/core/__init__.py +1 -0
- synth_ai/demos/core/cli.py +621 -0
- synth_ai/demos/demo_task_apps/__init__.py +1 -0
- synth_ai/demos/demo_task_apps/core.py +374 -0
- synth_ai/demos/demo_task_apps/math/__init__.py +1 -0
- synth_ai/demos/demo_task_apps/math/app.py +37 -0
- synth_ai/demos/demo_task_apps/math/config.toml +44 -0
- synth_ai/demos/demo_task_apps/math/deploy_modal.py +60 -0
- synth_ai/demos/demo_task_apps/math/deploy_task_app.sh +22 -0
- synth_ai/environments/examples/bandit/__init__.py +33 -0
- synth_ai/environments/examples/bandit/engine.py +294 -0
- synth_ai/environments/examples/bandit/environment.py +194 -0
- synth_ai/environments/examples/bandit/taskset.py +200 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +250 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +59 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +152 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_config.toml +24 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +1194 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/crafter_synth_config.toml +56 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_config_modal.toml +32 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +724 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_modal.py +384 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_action_results.py +53 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_agent_actions.py +178 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_latest_run.py +222 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_lm_traces.py +183 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_no_rewards.py +210 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_trace_issue.py +206 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_db_schema.py +49 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_latest_results.py +64 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/debug_agent_responses.py +88 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/quick_trace_check.py +77 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/compare_experiments.py +324 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +580 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/kick_off_ft_oai.py +362 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/multi_model_config.toml +49 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_enhanced_hooks.py +332 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_events.py +97 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_results.py +217 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_hook_storage.py +87 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_seeds.py +88 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/compare_seed_performance.py +195 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/custom_eval_pipelines.py +400 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/plot_hook_frequency.py +195 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/seed_analysis_summary.py +56 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v3.py +858 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +52 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +874 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +1412 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/example_v3_usage.py +216 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/compare_traces.py +296 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_comprehensive_evaluation.py +58 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_env_serialization.py +464 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_evaluation_browser.py +152 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_quick_evaluation.py +51 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_trace_evaluation.py +1412 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/debug_player_loss.py +112 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_service.py +203 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_slowness.py +305 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_by_difficulty.py +126 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_example.py +94 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/explore_saved_states.py +142 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft.py +26 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft_OLD.py +984 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_gemini.py +724 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_modal.py +386 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_metadata.py +205 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_gemini.py +150 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_modal.py +283 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/prepare_vertex_ft.py +280 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/profile_env_slowness.py +456 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/replicate_issue.py +166 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/run_and_eval.py +102 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/run_comparison.py +128 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/run_qwen_rollouts.py +655 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/trace_eval_OLD.py +202 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/old/validate_openai_format.py +166 -0
- synth_ai/environments/examples/crafter_classic/environment.py +41 -2
- synth_ai/environments/examples/crafter_custom/agent_demos/__init__.py +1 -0
- synth_ai/environments/examples/crafter_custom/agent_demos/trace_eval.py +202 -0
- synth_ai/environments/examples/crafter_custom/old/analyze_diamond_issue.py +159 -0
- synth_ai/environments/examples/crafter_custom/old/analyze_diamond_spawning.py +158 -0
- synth_ai/environments/examples/crafter_custom/old/compare_worlds.py +71 -0
- synth_ai/environments/examples/crafter_custom/old/dataset_stats.py +105 -0
- synth_ai/environments/examples/crafter_custom/old/diamond_spawning_summary.py +119 -0
- synth_ai/environments/examples/crafter_custom/old/example_dataset_usage.py +52 -0
- synth_ai/environments/examples/enron/units/keyword_stats.py +112 -0
- synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +1188 -0
- synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +48 -0
- synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +562 -0
- synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +221 -0
- synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +981 -0
- synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +74 -0
- synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +831 -0
- synth_ai/environments/examples/red/agent_demos/__init__.py +1 -0
- synth_ai/environments/examples/red/units/__init__.py +1 -0
- synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +899 -0
- synth_ai/environments/examples/sokoban/units/astar_common.py +95 -0
- synth_ai/environments/service/app.py +8 -0
- synth_ai/http.py +102 -0
- synth_ai/inference/__init__.py +7 -0
- synth_ai/inference/client.py +20 -0
- synth_ai/install_sqld.sh +40 -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/health.py +43 -0
- synth_ai/learning/jobs.py +205 -0
- 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/core/main_v3.py +13 -0
- synth_ai/lm/core/synth_models.py +48 -0
- synth_ai/lm/core/vendor_clients.py +9 -6
- synth_ai/lm/vendors/core/openai_api.py +31 -3
- synth_ai/lm/vendors/openai_standard.py +45 -14
- synth_ai/lm/vendors/supported/custom_endpoint.py +12 -2
- synth_ai/lm/vendors/synth_client.py +372 -28
- 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_v3/hooks.py +3 -1
- synth_ai/tracing_v3/session_tracer.py +123 -2
- synth_ai/tracing_v3/turso/manager.py +218 -0
- synth_ai/tracing_v3/turso/models.py +53 -0
- synth_ai-0.2.4.dev9.dist-info/METADATA +91 -0
- {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/RECORD +147 -30
- {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/entry_points.txt +1 -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 -164
- synth_ai/tui/cli/query_experiments_v3.py +0 -164
- synth_ai/tui/dashboard.py +0 -340
- synth_ai-0.2.4.dev7.dist-info/METADATA +0 -193
- {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
astar_common.py – one A* routine usable by both engine-level and
|
|
3
|
+
environment-level unit tests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import heapq
|
|
7
|
+
import itertools
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Awaitable, Callable, List, Tuple
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------- generic utilities ------------------------------------ #
|
|
15
|
+
def _boxes_left(env_pkg) -> int:
|
|
16
|
+
"""#targets – #boxes-on-targets (uses raw grids, never the counter)."""
|
|
17
|
+
return int(np.sum(env_pkg.room_fixed == 2) - np.sum(env_pkg.room_state == 3))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def solved(obj: Any) -> bool:
|
|
21
|
+
"""Expects obj to have a .package_sokoban_env attribute."""
|
|
22
|
+
return _boxes_left(obj.package_sokoban_env) == 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def heuristic(obj: Any) -> int:
|
|
26
|
+
"""Expects obj to have a .package_sokoban_env attribute."""
|
|
27
|
+
return _boxes_left(obj.package_sokoban_env)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------- single reusable A* ----------------------------------- #
|
|
31
|
+
async def astar(
|
|
32
|
+
root_obj: Any,
|
|
33
|
+
step_fn: Callable[[Any, int], Awaitable[None]],
|
|
34
|
+
deserialize_fn: Callable[[Any], Awaitable[Any]],
|
|
35
|
+
max_nodes: int = 1000,
|
|
36
|
+
) -> List[int]:
|
|
37
|
+
"""
|
|
38
|
+
Generic A* over Sokoban snapshots.
|
|
39
|
+
|
|
40
|
+
• `root_obj` - current engine *or* environment
|
|
41
|
+
• `step_fn(obj, action)` - async: apply one move to *obj*
|
|
42
|
+
• `deserialize_fn(snapshot)` - async: new obj from snapshot
|
|
43
|
+
"""
|
|
44
|
+
start_snap = await root_obj._serialize_engine()
|
|
45
|
+
|
|
46
|
+
frontier: List[Tuple[int, int, Any, List[int]]] = []
|
|
47
|
+
counter = itertools.count()
|
|
48
|
+
frontier.append((heuristic(root_obj), next(counter), start_snap, []))
|
|
49
|
+
seen: set[str] = set()
|
|
50
|
+
|
|
51
|
+
nodes = 0
|
|
52
|
+
while frontier and nodes < max_nodes:
|
|
53
|
+
f, _, snap, path = heapq.heappop(frontier)
|
|
54
|
+
cur = await deserialize_fn(snap)
|
|
55
|
+
key = json.dumps(snap.engine_snapshot, sort_keys=True)
|
|
56
|
+
if key in seen:
|
|
57
|
+
continue
|
|
58
|
+
seen.add(key)
|
|
59
|
+
if solved(cur):
|
|
60
|
+
return path
|
|
61
|
+
|
|
62
|
+
nodes += 1
|
|
63
|
+
for action in range(cur.package_sokoban_env.action_space.n):
|
|
64
|
+
child = await deserialize_fn(snap) # fresh copy
|
|
65
|
+
try:
|
|
66
|
+
await step_fn(child, action)
|
|
67
|
+
except Exception: # illegal/off-board
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
child_snap = await child._serialize_engine()
|
|
71
|
+
g = len(path) + 1
|
|
72
|
+
heapq.heappush(
|
|
73
|
+
frontier,
|
|
74
|
+
(g + heuristic(child), next(counter), child_snap, path + [action]),
|
|
75
|
+
)
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# convenience lambdas for the two concrete APIs
|
|
80
|
+
async def _engine_step(e, a): # `SokobanEngine`
|
|
81
|
+
await e._step_engine(a)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def _env_step(env, a): # `SokobanEnvironment` (expects Move wrapper)
|
|
85
|
+
from synth_ai.environments.examples.sokoban.units.test_sokoban_environment import Move
|
|
86
|
+
|
|
87
|
+
await env.step([[Move(a)]])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
ENGINE_ASTAR = lambda eng, **kw: astar(eng, _engine_step, eng.__class__._deserialize_engine, **kw)
|
|
91
|
+
ENV_ASTAR = lambda env, **kw: astar(
|
|
92
|
+
env.engine, _env_step, env.engine.__class__._deserialize_engine, **kw
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# ----------------------------------------------------------------- #
|
|
@@ -47,6 +47,14 @@ except Exception as _e:
|
|
|
47
47
|
# Keep service robust even if example env import fails
|
|
48
48
|
logging.getLogger(__name__).warning(f"Wordle env not registered: {_e}")
|
|
49
49
|
|
|
50
|
+
# Register Bandit example environment
|
|
51
|
+
try:
|
|
52
|
+
import synth_ai.environments.examples.bandit.environment as bandit_mod
|
|
53
|
+
|
|
54
|
+
register_environment("Bandit", bandit_mod.BanditEnvironment)
|
|
55
|
+
except Exception as _e:
|
|
56
|
+
logging.getLogger(__name__).warning(f"Bandit env not registered: {_e}")
|
|
57
|
+
|
|
50
58
|
app = FastAPI(title="Environment Service")
|
|
51
59
|
|
|
52
60
|
|
synth_ai/http.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class HTTPError(Exception):
|
|
12
|
+
status: int
|
|
13
|
+
url: str
|
|
14
|
+
message: str
|
|
15
|
+
body_snippet: str | None = None
|
|
16
|
+
detail: Any | None = None
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
19
|
+
base = f"HTTP {self.status} for {self.url}: {self.message}"
|
|
20
|
+
if self.body_snippet:
|
|
21
|
+
base += f" | body[0:200]={self.body_snippet[:200]}"
|
|
22
|
+
return base
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncHttpClient:
|
|
26
|
+
def __init__(self, base_url: str, api_key: str, timeout: float = 30.0) -> None:
|
|
27
|
+
self._base_url = base_url.rstrip("/")
|
|
28
|
+
self._api_key = api_key
|
|
29
|
+
self._timeout = aiohttp.ClientTimeout(total=timeout)
|
|
30
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
31
|
+
|
|
32
|
+
async def __aenter__(self) -> "AsyncHttpClient":
|
|
33
|
+
if self._session is None:
|
|
34
|
+
headers = {"authorization": f"Bearer {self._api_key}"}
|
|
35
|
+
self._session = aiohttp.ClientSession(headers=headers, timeout=self._timeout)
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
|
|
39
|
+
if self._session is not None:
|
|
40
|
+
await self._session.close()
|
|
41
|
+
self._session = None
|
|
42
|
+
|
|
43
|
+
def _abs(self, path: str) -> str:
|
|
44
|
+
if path.startswith("http://") or path.startswith("https://"):
|
|
45
|
+
return path
|
|
46
|
+
# If base_url already ends with /api and path starts with /api, remove duplicate
|
|
47
|
+
if self._base_url.endswith("/api") and path.startswith("/api"):
|
|
48
|
+
path = path[4:] # Remove leading /api
|
|
49
|
+
return f"{self._base_url}/{path.lstrip('/')}"
|
|
50
|
+
|
|
51
|
+
async def get(self, path: str, *, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None) -> Any:
|
|
52
|
+
url = self._abs(path)
|
|
53
|
+
assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
|
|
54
|
+
async with self._session.get(url, params=params, headers=headers) as resp:
|
|
55
|
+
return await self._handle_response(resp, url)
|
|
56
|
+
|
|
57
|
+
async def post_json(self, path: str, *, json: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> Any:
|
|
58
|
+
url = self._abs(path)
|
|
59
|
+
assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
|
|
60
|
+
async with self._session.post(url, json=json, headers=headers) as resp:
|
|
61
|
+
return await self._handle_response(resp, url)
|
|
62
|
+
|
|
63
|
+
async def post_multipart(self, path: str, *, data: Dict[str, Any], files: Dict[str, tuple[str, bytes, str | None]], headers: Optional[Dict[str, str]] = None) -> Any:
|
|
64
|
+
url = self._abs(path)
|
|
65
|
+
assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
|
|
66
|
+
form = aiohttp.FormData()
|
|
67
|
+
for k, v in data.items():
|
|
68
|
+
form.add_field(k, str(v))
|
|
69
|
+
for field, (filename, content, content_type) in files.items():
|
|
70
|
+
form.add_field(field, content, filename=filename, content_type=content_type or "application/octet-stream")
|
|
71
|
+
async with self._session.post(url, data=form, headers=headers) as resp:
|
|
72
|
+
return await self._handle_response(resp, url)
|
|
73
|
+
|
|
74
|
+
async def delete(self, path: str, *, headers: Optional[Dict[str, str]] = None) -> Any:
|
|
75
|
+
url = self._abs(path)
|
|
76
|
+
assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
|
|
77
|
+
async with self._session.delete(url, headers=headers) as resp:
|
|
78
|
+
return await self._handle_response(resp, url)
|
|
79
|
+
|
|
80
|
+
async def _handle_response(self, resp: aiohttp.ClientResponse, url: str) -> Any:
|
|
81
|
+
text = await resp.text()
|
|
82
|
+
body_snippet = text[:200] if text else None
|
|
83
|
+
if 200 <= resp.status < 300:
|
|
84
|
+
ctype = resp.headers.get("content-type", "")
|
|
85
|
+
if "application/json" in ctype:
|
|
86
|
+
try:
|
|
87
|
+
return await resp.json()
|
|
88
|
+
except Exception:
|
|
89
|
+
# Fallback to text
|
|
90
|
+
return text
|
|
91
|
+
return text
|
|
92
|
+
# error
|
|
93
|
+
detail: Any | None = None
|
|
94
|
+
try:
|
|
95
|
+
detail = await resp.json()
|
|
96
|
+
except Exception:
|
|
97
|
+
detail = None
|
|
98
|
+
raise HTTPError(status=resp.status, url=url, message="request_failed", body_snippet=body_snippet, detail=detail)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def sleep(seconds: float) -> None:
|
|
102
|
+
await asyncio.sleep(seconds)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from ..http import AsyncHttpClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InferenceClient:
|
|
9
|
+
def __init__(self, base_url: str, api_key: str, *, timeout: float = 30.0) -> None:
|
|
10
|
+
self._base_url = base_url.rstrip("/")
|
|
11
|
+
self._api_key = api_key
|
|
12
|
+
self._timeout = timeout
|
|
13
|
+
|
|
14
|
+
async def create_chat_completion(self, *, model: str, messages: list[dict], **kwargs: Any) -> Dict[str, Any]:
|
|
15
|
+
body: Dict[str, Any] = {"model": model, "messages": messages}
|
|
16
|
+
body.update(kwargs)
|
|
17
|
+
async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
|
|
18
|
+
return await http.post_json("/v1/chat/completions", json=body)
|
|
19
|
+
|
|
20
|
+
|
synth_ai/install_sqld.sh
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Install sqld binary for Synth AI
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
SQLD_VERSION="v0.26.2"
|
|
7
|
+
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
8
|
+
ARCH=$(uname -m)
|
|
9
|
+
|
|
10
|
+
# Map architecture names
|
|
11
|
+
case "$ARCH" in
|
|
12
|
+
x86_64) ARCH="x86_64" ;;
|
|
13
|
+
aarch64|arm64) ARCH="aarch64" ;;
|
|
14
|
+
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
|
15
|
+
esac
|
|
16
|
+
|
|
17
|
+
# Construct download URL
|
|
18
|
+
URL="https://github.com/tursodatabase/libsql/releases/download/libsql-server-${SQLD_VERSION}/sqld-${OS}-${ARCH}.tar.xz"
|
|
19
|
+
|
|
20
|
+
echo "📥 Downloading sqld ${SQLD_VERSION} for ${OS}-${ARCH}..."
|
|
21
|
+
|
|
22
|
+
# Download and extract
|
|
23
|
+
TMP_DIR=$(mktemp -d)
|
|
24
|
+
cd "$TMP_DIR"
|
|
25
|
+
curl -L -o sqld.tar.xz "$URL"
|
|
26
|
+
tar -xf sqld.tar.xz
|
|
27
|
+
|
|
28
|
+
# Install to user's local bin
|
|
29
|
+
mkdir -p ~/.local/bin
|
|
30
|
+
mv sqld ~/.local/bin/
|
|
31
|
+
chmod +x ~/.local/bin/sqld
|
|
32
|
+
|
|
33
|
+
# Clean up
|
|
34
|
+
cd -
|
|
35
|
+
rm -rf "$TMP_DIR"
|
|
36
|
+
|
|
37
|
+
echo "✅ sqld installed to ~/.local/bin/sqld"
|
|
38
|
+
echo ""
|
|
39
|
+
echo "🔧 Add ~/.local/bin to your PATH if needed:"
|
|
40
|
+
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
|
synth_ai/jobs/client.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from synth_ai.http import AsyncHttpClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FilesApi:
|
|
9
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
10
|
+
self._http = http
|
|
11
|
+
|
|
12
|
+
async def upload(self, *, filename: str, content: bytes, purpose: str, content_type: Optional[str] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
|
|
13
|
+
data = {"purpose": purpose}
|
|
14
|
+
files = {"file": (filename, content, content_type)}
|
|
15
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
16
|
+
return await self._http.post_multipart("/api/files", data=data, files=files, headers=headers)
|
|
17
|
+
|
|
18
|
+
async def list(self, *, purpose: Optional[str] = None, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
|
|
19
|
+
params: Dict[str, Any] = {}
|
|
20
|
+
if purpose is not None:
|
|
21
|
+
params["purpose"] = purpose
|
|
22
|
+
if after is not None:
|
|
23
|
+
params["after"] = after
|
|
24
|
+
params["limit"] = limit
|
|
25
|
+
return await self._http.get("/api/files", params=params)
|
|
26
|
+
|
|
27
|
+
async def retrieve(self, file_id: str) -> Dict[str, Any]:
|
|
28
|
+
return await self._http.get(f"/api/files/{file_id}")
|
|
29
|
+
|
|
30
|
+
async def delete(self, file_id: str) -> Any:
|
|
31
|
+
return await self._http.delete(f"/api/files/{file_id}")
|
|
32
|
+
|
|
33
|
+
async def list_jobs(self, file_id: str, *, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
|
|
34
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
35
|
+
if after is not None:
|
|
36
|
+
params["after"] = after
|
|
37
|
+
return await self._http.get(f"/api/files/{file_id}/jobs", params=params)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SftJobsApi:
|
|
41
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
42
|
+
self._http = http
|
|
43
|
+
|
|
44
|
+
async def create(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
training_file: str,
|
|
48
|
+
model: str,
|
|
49
|
+
validation_file: Optional[str] = None,
|
|
50
|
+
hyperparameters: Optional[Dict[str, Any]] = None,
|
|
51
|
+
suffix: Optional[str] = None,
|
|
52
|
+
integrations: Optional[Dict[str, Any]] = None,
|
|
53
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
54
|
+
idempotency_key: Optional[str] = None,
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
payload: Dict[str, Any] = {
|
|
57
|
+
"training_file": training_file,
|
|
58
|
+
"model": model,
|
|
59
|
+
}
|
|
60
|
+
if validation_file is not None:
|
|
61
|
+
payload["validation_file"] = validation_file
|
|
62
|
+
if hyperparameters is not None:
|
|
63
|
+
payload["hyperparameters"] = hyperparameters
|
|
64
|
+
if suffix is not None:
|
|
65
|
+
payload["suffix"] = suffix
|
|
66
|
+
if integrations is not None:
|
|
67
|
+
payload["integrations"] = integrations
|
|
68
|
+
if metadata is not None:
|
|
69
|
+
payload["metadata"] = metadata
|
|
70
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
71
|
+
return await self._http.post_json("/api/sft/jobs", json=payload, headers=headers)
|
|
72
|
+
|
|
73
|
+
async def list(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
status: Optional[str] = None,
|
|
77
|
+
model: Optional[str] = None,
|
|
78
|
+
file_id: Optional[str] = None,
|
|
79
|
+
created_after: Optional[int] = None,
|
|
80
|
+
created_before: Optional[int] = None,
|
|
81
|
+
after: Optional[str] = None,
|
|
82
|
+
limit: int = 20,
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
85
|
+
if status is not None:
|
|
86
|
+
params["status"] = status
|
|
87
|
+
if model is not None:
|
|
88
|
+
params["model"] = model
|
|
89
|
+
if file_id is not None:
|
|
90
|
+
params["file_id"] = file_id
|
|
91
|
+
if created_after is not None:
|
|
92
|
+
params["created_after"] = created_after
|
|
93
|
+
if created_before is not None:
|
|
94
|
+
params["created_before"] = created_before
|
|
95
|
+
if after is not None:
|
|
96
|
+
params["after"] = after
|
|
97
|
+
return await self._http.get("/api/sft/jobs", params=params)
|
|
98
|
+
|
|
99
|
+
async def retrieve(self, job_id: str) -> Dict[str, Any]:
|
|
100
|
+
return await self._http.get(f"/api/sft/jobs/{job_id}")
|
|
101
|
+
|
|
102
|
+
async def cancel(self, job_id: str) -> Dict[str, Any]:
|
|
103
|
+
return await self._http.post_json(f"/api/sft/jobs/{job_id}/cancel", json={})
|
|
104
|
+
|
|
105
|
+
async def list_events(self, job_id: str, *, since_seq: int = 0, limit: int = 200) -> Dict[str, Any]:
|
|
106
|
+
params = {"since_seq": since_seq, "limit": limit}
|
|
107
|
+
return await self._http.get(f"/api/sft/jobs/{job_id}/events", params=params)
|
|
108
|
+
|
|
109
|
+
async def checkpoints(self, job_id: str, *, after: Optional[str] = None, limit: int = 10) -> Dict[str, Any]:
|
|
110
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
111
|
+
if after is not None:
|
|
112
|
+
params["after"] = after
|
|
113
|
+
return await self._http.get(f"/api/sft/jobs/{job_id}/checkpoints", params=params)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RlJobsApi:
|
|
117
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
118
|
+
self._http = http
|
|
119
|
+
|
|
120
|
+
async def create(
|
|
121
|
+
self,
|
|
122
|
+
*,
|
|
123
|
+
model: str,
|
|
124
|
+
endpoint_base_url: str,
|
|
125
|
+
trainer_id: str,
|
|
126
|
+
trainer: Optional[Dict[str, Any]] = None,
|
|
127
|
+
job_config_id: Optional[str] = None,
|
|
128
|
+
config: Optional[Dict[str, Any]] = None,
|
|
129
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
130
|
+
idempotency_key: Optional[str] = None,
|
|
131
|
+
) -> Dict[str, Any]:
|
|
132
|
+
payload: Dict[str, Any] = {
|
|
133
|
+
"model": model,
|
|
134
|
+
"endpoint_base_url": endpoint_base_url,
|
|
135
|
+
"trainer_id": trainer_id,
|
|
136
|
+
}
|
|
137
|
+
if trainer is not None:
|
|
138
|
+
payload["trainer"] = trainer
|
|
139
|
+
if job_config_id is not None:
|
|
140
|
+
payload["job_config_id"] = job_config_id
|
|
141
|
+
if config is not None:
|
|
142
|
+
payload["config"] = config
|
|
143
|
+
if metadata is not None:
|
|
144
|
+
payload["metadata"] = metadata
|
|
145
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
146
|
+
return await self._http.post_json("/api/rl/jobs", json=payload, headers=headers)
|
|
147
|
+
|
|
148
|
+
async def list(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
status: Optional[str] = None,
|
|
152
|
+
model: Optional[str] = None,
|
|
153
|
+
created_after: Optional[int] = None,
|
|
154
|
+
created_before: Optional[int] = None,
|
|
155
|
+
after: Optional[str] = None,
|
|
156
|
+
limit: int = 20,
|
|
157
|
+
) -> Dict[str, Any]:
|
|
158
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
159
|
+
if status is not None:
|
|
160
|
+
params["status"] = status
|
|
161
|
+
if model is not None:
|
|
162
|
+
params["model"] = model
|
|
163
|
+
if created_after is not None:
|
|
164
|
+
params["created_after"] = created_after
|
|
165
|
+
if created_before is not None:
|
|
166
|
+
params["created_before"] = created_before
|
|
167
|
+
if after is not None:
|
|
168
|
+
params["after"] = after
|
|
169
|
+
return await self._http.get("/api/rl/jobs", params=params)
|
|
170
|
+
|
|
171
|
+
async def retrieve(self, job_id: str) -> Dict[str, Any]:
|
|
172
|
+
return await self._http.get(f"/api/rl/jobs/{job_id}")
|
|
173
|
+
|
|
174
|
+
async def cancel(self, job_id: str) -> Dict[str, Any]:
|
|
175
|
+
return await self._http.post_json(f"/api/rl/jobs/{job_id}/cancel", json={})
|
|
176
|
+
|
|
177
|
+
async def list_events(self, job_id: str, *, since_seq: int = 0, limit: int = 200) -> Dict[str, Any]:
|
|
178
|
+
params = {"since_seq": since_seq, "limit": limit}
|
|
179
|
+
return await self._http.get(f"/api/rl/jobs/{job_id}/events", params=params)
|
|
180
|
+
|
|
181
|
+
async def metrics(self, job_id: str, *, after_step: int = -1, limit: int = 200) -> Dict[str, Any]:
|
|
182
|
+
params = {"after_step": after_step, "limit": limit}
|
|
183
|
+
return await self._http.get(f"/api/rl/jobs/{job_id}/metrics", params=params)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ModelsApi:
|
|
187
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
188
|
+
self._http = http
|
|
189
|
+
|
|
190
|
+
async def list(
|
|
191
|
+
self,
|
|
192
|
+
*,
|
|
193
|
+
source: Optional[str] = None,
|
|
194
|
+
base_model: Optional[str] = None,
|
|
195
|
+
status: Optional[str] = None,
|
|
196
|
+
after: Optional[str] = None,
|
|
197
|
+
limit: int = 20,
|
|
198
|
+
) -> Dict[str, Any]:
|
|
199
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
200
|
+
if source is not None:
|
|
201
|
+
params["source"] = source
|
|
202
|
+
if base_model is not None:
|
|
203
|
+
params["base_model"] = base_model
|
|
204
|
+
if status is not None:
|
|
205
|
+
params["status"] = status
|
|
206
|
+
if after is not None:
|
|
207
|
+
params["after"] = after
|
|
208
|
+
return await self._http.get("/api/models", params=params)
|
|
209
|
+
|
|
210
|
+
async def retrieve(self, model_id: str) -> Dict[str, Any]:
|
|
211
|
+
return await self._http.get(f"/api/models/{model_id}")
|
|
212
|
+
|
|
213
|
+
async def delete(self, model_id: str) -> Any:
|
|
214
|
+
return await self._http.delete(f"/api/models/{model_id}")
|
|
215
|
+
|
|
216
|
+
async def list_jobs(self, model_id: str, *, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
|
|
217
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
218
|
+
if after is not None:
|
|
219
|
+
params["after"] = after
|
|
220
|
+
return await self._http.get(f"/api/models/{model_id}/jobs", params=params)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class JobsClient:
|
|
224
|
+
"""High-level client aggregating job APIs.
|
|
225
|
+
|
|
226
|
+
Usage:
|
|
227
|
+
async with JobsClient(base_url, api_key) as c:
|
|
228
|
+
await c.files.list()
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def __init__(self, base_url: str, api_key: str, timeout: float = 30.0, http: Optional[AsyncHttpClient] = None) -> None:
|
|
232
|
+
self._base_url = base_url
|
|
233
|
+
self._api_key = api_key
|
|
234
|
+
self._timeout = timeout
|
|
235
|
+
self._http = http or AsyncHttpClient(base_url, api_key, timeout=timeout)
|
|
236
|
+
self.files = FilesApi(self._http)
|
|
237
|
+
self.sft = SftJobsApi(self._http)
|
|
238
|
+
self.rl = RlJobsApi(self._http)
|
|
239
|
+
self.models = ModelsApi(self._http)
|
|
240
|
+
|
|
241
|
+
async def __aenter__(self) -> "JobsClient":
|
|
242
|
+
await self._http.__aenter__()
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
|
|
246
|
+
await self._http.__aexit__(exc_type, exc, tb)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .client import LearningClient
|
|
2
|
+
from .rl_client import RlClient
|
|
3
|
+
from .ft_client import FtClient
|
|
4
|
+
from .validators import validate_training_jsonl, validate_trainer_cfg_rl
|
|
5
|
+
from synth_ai.task import validate_task_app_url, task_app_health
|
|
6
|
+
from .health import backend_health, pricing_preflight, balance_autumn_normalized
|
|
7
|
+
from .sse import stream_events as stream_job_events
|
|
8
|
+
from .jobs import JobHandle, JobsApiResolver
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"LearningClient",
|
|
12
|
+
"RlClient",
|
|
13
|
+
"FtClient",
|
|
14
|
+
"validate_training_jsonl",
|
|
15
|
+
"validate_trainer_cfg_rl",
|
|
16
|
+
"validate_task_app_url",
|
|
17
|
+
"backend_health",
|
|
18
|
+
"task_app_health",
|
|
19
|
+
"pricing_preflight",
|
|
20
|
+
"balance_autumn_normalized",
|
|
21
|
+
"stream_job_events",
|
|
22
|
+
"JobHandle",
|
|
23
|
+
"JobsApiResolver",
|
|
24
|
+
]
|