synth-ai 0.2.10__py3-none-any.whl → 0.2.12__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.

Files changed (38) hide show
  1. examples/multi_step/task_app_config_notes.md +488 -0
  2. examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +33 -0
  3. examples/warming_up_to_rl/configs/eval_stepwise_consistent.toml +26 -0
  4. examples/warming_up_to_rl/configs/eval_stepwise_per_achievement.toml +36 -0
  5. examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +30 -0
  6. examples/warming_up_to_rl/run_eval.py +142 -25
  7. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +146 -2
  8. synth_ai/api/train/builders.py +25 -14
  9. synth_ai/api/train/cli.py +29 -6
  10. synth_ai/api/train/env_resolver.py +18 -19
  11. synth_ai/api/train/supported_algos.py +8 -5
  12. synth_ai/api/train/utils.py +6 -1
  13. synth_ai/cli/__init__.py +4 -2
  14. synth_ai/cli/_storage.py +19 -0
  15. synth_ai/cli/balance.py +14 -2
  16. synth_ai/cli/calc.py +37 -22
  17. synth_ai/cli/legacy_root_backup.py +12 -14
  18. synth_ai/cli/recent.py +12 -7
  19. synth_ai/cli/status.py +4 -3
  20. synth_ai/cli/task_apps.py +143 -137
  21. synth_ai/cli/traces.py +4 -3
  22. synth_ai/cli/watch.py +3 -2
  23. synth_ai/jobs/client.py +15 -3
  24. synth_ai/task/server.py +14 -7
  25. synth_ai/tracing_v3/decorators.py +51 -26
  26. synth_ai/tracing_v3/examples/basic_usage.py +12 -7
  27. synth_ai/tracing_v3/llm_call_record_helpers.py +107 -53
  28. synth_ai/tracing_v3/replica_sync.py +8 -4
  29. synth_ai/tracing_v3/storage/utils.py +11 -9
  30. synth_ai/tracing_v3/turso/__init__.py +12 -0
  31. synth_ai/tracing_v3/turso/daemon.py +2 -1
  32. synth_ai/tracing_v3/turso/native_manager.py +28 -15
  33. {synth_ai-0.2.10.dist-info → synth_ai-0.2.12.dist-info}/METADATA +4 -2
  34. {synth_ai-0.2.10.dist-info → synth_ai-0.2.12.dist-info}/RECORD +38 -31
  35. {synth_ai-0.2.10.dist-info → synth_ai-0.2.12.dist-info}/WHEEL +0 -0
  36. {synth_ai-0.2.10.dist-info → synth_ai-0.2.12.dist-info}/entry_points.txt +0 -0
  37. {synth_ai-0.2.10.dist-info → synth_ai-0.2.12.dist-info}/licenses/LICENSE +0 -0
  38. {synth_ai-0.2.10.dist-info → synth_ai-0.2.12.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib
3
4
  import json
4
5
  import os
5
6
  import re
@@ -13,7 +14,11 @@ from pathlib import Path
13
14
  from typing import Any
14
15
 
15
16
  import requests
16
- from synth_ai.learning.sft import collect_sft_jsonl_errors
17
+
18
+ try:
19
+ collect_sft_jsonl_errors = importlib.import_module("synth_ai.learning.sft").collect_sft_jsonl_errors
20
+ except Exception as exc: # pragma: no cover - critical dependency
21
+ raise RuntimeError("Unable to load SFT JSONL helpers") from exc
17
22
 
18
23
  REPO_ROOT = Path(__file__).resolve().parents[3]
19
24
 
synth_ai/cli/__init__.py CHANGED
@@ -7,6 +7,8 @@ pyproject entry point `synth_ai.cli:cli`.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import importlib
11
+
10
12
  # Load environment variables from a local .env if present (repo root)
11
13
  try:
12
14
  from dotenv import find_dotenv, load_dotenv
@@ -89,8 +91,8 @@ try:
89
91
  except Exception:
90
92
  pass
91
93
  try:
92
- from synth_ai.api.train import register as _train_register
93
-
94
+ _train_module = importlib.import_module("synth_ai.api.train")
95
+ _train_register = _train_module.register
94
96
  _train_register(cli)
95
97
  except Exception:
96
98
  pass
@@ -0,0 +1,19 @@
1
+ """Dynamic loader for tracing storage components.
2
+
3
+ This avoids hard dependencies on the tracing_v3 storage package during import
4
+ time, which keeps CLI modules usable in constrained environments while still
5
+ allowing type checkers to resolve the symbols dynamically.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ from typing import Any
12
+
13
+
14
+ def load_storage() -> tuple[Any, Any]:
15
+ """Return (create_storage, StorageConfig) from tracing_v3.storage."""
16
+ storage_module = importlib.import_module("synth_ai.tracing_v3.storage")
17
+ create_storage = storage_module.create_storage
18
+ storage_config = storage_module.StorageConfig
19
+ return create_storage, storage_config
synth_ai/cli/balance.py CHANGED
@@ -5,7 +5,9 @@ CLI: check remaining credit balance from Synth backend.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import importlib
8
9
  import os
10
+ from collections.abc import Callable
9
11
 
10
12
  import click
11
13
  import requests
@@ -14,8 +16,18 @@ from rich import box
14
16
  from rich.console import Console
15
17
  from rich.table import Table
16
18
 
17
- from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT, get_backend_from_env
18
19
 
20
+ def _load_base_url_module() -> tuple[str, Callable[[], tuple[str, str]]]:
21
+ try:
22
+ module = importlib.import_module("synth_ai.config.base_url")
23
+ default = module.PROD_BASE_URL_DEFAULT
24
+ getter = module.get_backend_from_env
25
+ return str(default), getter
26
+ except Exception:
27
+ return "https://agent-learning.onrender.com", lambda: ("https://agent-learning.onrender.com", "")
28
+
29
+
30
+ PROD_BASE_URL_DEFAULT, _get_backend_from_env = _load_base_url_module()
19
31
  PROD_BACKEND_BASE = f"{PROD_BASE_URL_DEFAULT.rstrip('/')}/api/v1"
20
32
 
21
33
 
@@ -25,7 +37,7 @@ def _get_default_base_url() -> str:
25
37
  val = os.getenv(var)
26
38
  if val:
27
39
  return val
28
- base, _ = get_backend_from_env()
40
+ base, _ = _get_backend_from_env()
29
41
  base = base.rstrip("/")
30
42
  if base.endswith("/api"):
31
43
  base = base[: -len("/api")]
synth_ai/cli/calc.py CHANGED
@@ -5,46 +5,61 @@ Safe evaluation of arithmetic expressions.
5
5
  """
6
6
 
7
7
  import ast
8
- import operator as op
8
+ import operator
9
+ from collections.abc import Callable
9
10
 
10
11
  import click
11
12
  from rich.console import Console
12
13
 
13
14
  # Supported operators
14
- _OPS = {
15
- ast.Add: op.add,
16
- ast.Sub: op.sub,
17
- ast.Mult: op.mul,
18
- ast.Div: op.truediv,
19
- ast.FloorDiv: op.floordiv,
20
- ast.Mod: op.mod,
21
- ast.Pow: op.pow,
22
- ast.USub: op.neg,
23
- ast.UAdd: op.pos,
15
+ _BINARY_OPS: dict[type[ast.AST], Callable[[float, float], float]] = {
16
+ ast.Add: operator.add,
17
+ ast.Sub: operator.sub,
18
+ ast.Mult: operator.mul,
19
+ ast.Div: operator.truediv,
20
+ ast.FloorDiv: operator.floordiv,
21
+ ast.Mod: operator.mod,
22
+ ast.Pow: operator.pow,
23
+ }
24
+
25
+ _UNARY_OPS: dict[type[ast.AST], Callable[[float], float]] = {
26
+ ast.USub: operator.neg,
27
+ ast.UAdd: operator.pos,
24
28
  }
25
29
 
26
30
 
27
31
  def _safe_eval(expr: str) -> float:
28
32
  node = ast.parse(expr, mode="eval")
29
33
 
30
- def _eval(n):
34
+ def _eval(n: ast.AST) -> float:
31
35
  if isinstance(n, ast.Expression):
32
36
  return _eval(n.body)
33
- if hasattr(ast, "Num") and isinstance(n, ast.Num): # 3.8 and earlier
34
- return n.n
35
- if isinstance(n, ast.Constant): # 3.8+
36
- if isinstance(n.value, int | float):
37
- return n.value
37
+ if isinstance(n, ast.Constant):
38
+ if isinstance(n.value, (int, float)):
39
+ return float(n.value)
40
+ raise ValueError("Only numeric constants are allowed")
41
+ num_node = getattr(ast, "Num", None)
42
+ if num_node is not None and isinstance(n, num_node): # pragma: no cover
43
+ numeric_value = getattr(n, "n", None)
44
+ if isinstance(numeric_value, (int, float)):
45
+ return float(numeric_value)
38
46
  raise ValueError("Only numeric constants are allowed")
39
- if isinstance(n, ast.BinOp) and type(n.op) in _OPS:
40
- return _OPS[type(n.op)](_eval(n.left), _eval(n.right))
41
- if isinstance(n, ast.UnaryOp) and type(n.op) in _OPS:
42
- return _OPS[type(n.op)](_eval(n.operand))
47
+ if isinstance(n, ast.BinOp):
48
+ op_type = type(n.op)
49
+ func = _BINARY_OPS.get(op_type)
50
+ if func:
51
+ return func(_eval(n.left), _eval(n.right))
52
+ if isinstance(n, ast.UnaryOp):
53
+ op_type = type(n.op)
54
+ func = _UNARY_OPS.get(op_type)
55
+ if func:
56
+ return func(_eval(n.operand))
43
57
  if isinstance(n, ast.Expr):
44
58
  return _eval(n.value)
45
59
  raise ValueError("Unsupported expression")
46
60
 
47
- return _eval(node)
61
+ result = _eval(node)
62
+ return float(result)
48
63
 
49
64
 
50
65
  def register(cli):
@@ -3,6 +3,7 @@
3
3
  Synth AI CLI - Command line interface for Synth AI services.
4
4
  """
5
5
 
6
+ import importlib
6
7
  import logging
7
8
  import os
8
9
  import shutil
@@ -12,6 +13,9 @@ import sys
12
13
  import time
13
14
 
14
15
  import click
16
+ import requests
17
+ from requests.exceptions import ConnectionError as RequestsConnectionError
18
+ from requests.exceptions import HTTPError
15
19
 
16
20
  logger = logging.getLogger(__name__)
17
21
 
@@ -140,8 +144,6 @@ def register_env(
140
144
  name: str, module_path: str, class_name: str, description: str | None, service_url: str
141
145
  ):
142
146
  """Register a new environment with the service."""
143
- import requests
144
-
145
147
  payload = {
146
148
  "name": name,
147
149
  "module_path": module_path,
@@ -157,10 +159,10 @@ def register_env(
157
159
  result = response.json()
158
160
  click.echo(f"✅ {result['message']}")
159
161
 
160
- except requests.exceptions.ConnectionError:
162
+ except RequestsConnectionError:
161
163
  click.echo(f"❌ Could not connect to environment service at {service_url}")
162
164
  click.echo("💡 Make sure the service is running: synth-ai serve")
163
- except requests.exceptions.HTTPError as e:
165
+ except HTTPError as e:
164
166
  error_detail = e.response.json().get("detail", str(e)) if e.response else str(e)
165
167
  click.echo(f"❌ Registration failed: {error_detail}")
166
168
  except Exception as e:
@@ -171,8 +173,6 @@ def register_env(
171
173
  @click.option("--service-url", default="http://localhost:8901", help="Environment service URL")
172
174
  def list_envs(service_url: str):
173
175
  """List all registered environments."""
174
- import requests
175
-
176
176
  try:
177
177
  response = requests.get(f"{service_url}/registry/environments", timeout=10)
178
178
  response.raise_for_status()
@@ -195,7 +195,7 @@ def list_envs(service_url: str):
195
195
  click.echo(f" Description: {env['description']}")
196
196
  click.echo()
197
197
 
198
- except requests.exceptions.ConnectionError:
198
+ except RequestsConnectionError:
199
199
  click.echo(f"❌ Could not connect to environment service at {service_url}")
200
200
  click.echo("💡 Make sure the service is running: synth-ai serve")
201
201
  except Exception as e:
@@ -207,8 +207,6 @@ def list_envs(service_url: str):
207
207
  @click.option("--service-url", default="http://localhost:8901", help="Environment service URL")
208
208
  def unregister_env(name: str, service_url: str):
209
209
  """Unregister an environment from the service."""
210
- import requests
211
-
212
210
  try:
213
211
  response = requests.delete(f"{service_url}/registry/environments/{name}", timeout=10)
214
212
  response.raise_for_status()
@@ -216,10 +214,10 @@ def unregister_env(name: str, service_url: str):
216
214
  result = response.json()
217
215
  click.echo(f"✅ {result['message']}")
218
216
 
219
- except requests.exceptions.ConnectionError:
217
+ except RequestsConnectionError:
220
218
  click.echo(f"❌ Could not connect to environment service at {service_url}")
221
219
  click.echo("💡 Make sure the service is running: synth-ai serve")
222
- except requests.exceptions.HTTPError as e:
220
+ except HTTPError as e:
223
221
  if e.response.status_code == 404:
224
222
  click.echo(f"❌ Environment '{name}' not found in registry")
225
223
  else:
@@ -236,9 +234,9 @@ def unregister_env(name: str, service_url: str):
236
234
  def view(url: str):
237
235
  """Launch the interactive TUI dashboard."""
238
236
  try:
239
- from .tui.dashboard import SynthDashboard
240
-
241
- app = SynthDashboard(db_url=url)
237
+ module = importlib.import_module(".tui.dashboard", __package__)
238
+ synth_dashboard_cls = module.SynthDashboard
239
+ app = synth_dashboard_cls(db_url=url)
242
240
  app.run()
243
241
  except ImportError:
244
242
  click.echo("❌ Textual not installed. Install with: pip install textual", err=True)
synth_ai/cli/recent.py CHANGED
@@ -5,40 +5,45 @@ CLI: experiments active in the last K hours with summary stats.
5
5
 
6
6
  import asyncio
7
7
  from datetime import datetime, timedelta
8
+ from typing import TYPE_CHECKING, Any
8
9
 
9
10
  import click
10
11
  from rich import box
11
12
  from rich.console import Console
12
13
  from rich.table import Table
13
14
 
15
+ from ._storage import load_storage
14
16
 
15
- def _fmt_int(v) -> str:
17
+ if TYPE_CHECKING: # pragma: no cover - typing only
18
+ import pandas as pd
19
+ else:
20
+ pd = Any # type: ignore[assignment]
21
+ def _fmt_int(v: Any) -> str:
16
22
  try:
17
23
  return f"{int(v):,}"
18
24
  except Exception:
19
25
  return "0"
20
26
 
21
27
 
22
- def _fmt_money(v) -> str:
28
+ def _fmt_money(v: Any) -> str:
23
29
  try:
24
30
  return f"${float(v or 0.0):.4f}"
25
31
  except Exception:
26
32
  return "$0.0000"
27
33
 
28
34
 
29
- def _fmt_time(v) -> str:
35
+ def _fmt_time(v: Any) -> str:
30
36
  try:
31
37
  return str(v)
32
38
  except Exception:
33
39
  return "-"
34
40
 
35
41
 
36
- async def _fetch_recent(db_url: str, hours: float):
37
- from synth_ai.tracing_v3.storage.factory import create_storage, StorageConfig
38
-
42
+ async def _fetch_recent(db_url: str, hours: float) -> "pd.DataFrame":
39
43
  start_time = datetime.now() - timedelta(hours=hours)
40
44
 
41
- db = create_storage(StorageConfig(connection_string=db_url))
45
+ create_storage, storage_config = load_storage()
46
+ db: Any = create_storage(storage_config(connection_string=db_url))
42
47
  await db.initialize()
43
48
  try:
44
49
  query = """
synth_ai/cli/status.py CHANGED
@@ -12,11 +12,12 @@ from rich.console import Console
12
12
  from rich.panel import Panel
13
13
  from rich.table import Table
14
14
 
15
+ from ._storage import load_storage
15
16
 
16
- async def _db_stats(db_url: str) -> dict:
17
- from synth_ai.tracing_v3.storage.factory import StorageConfig, create_storage
18
17
 
19
- db = create_storage(StorageConfig(connection_string=db_url))
18
+ async def _db_stats(db_url: str) -> dict:
19
+ create_storage, storage_config = load_storage()
20
+ db = create_storage(storage_config(connection_string=db_url))
20
21
  await db.initialize()
21
22
  try:
22
23
  out: dict = {}