synth-ai 0.2.16__py3-none-any.whl → 0.2.17__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 (192) hide show
  1. examples/analyze_semantic_words.sh +2 -2
  2. examples/blog_posts/pokemon_vl/README.md +98 -0
  3. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -0
  4. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  5. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  6. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +42 -0
  7. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  8. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  9. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  10. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  11. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  12. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  13. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +41 -0
  14. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  15. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  16. examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
  17. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
  18. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
  19. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
  20. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  21. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  22. examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
  23. examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
  24. examples/qwen_coder/configs/coder_lora_small.toml +1 -3
  25. examples/qwen_vl/README.md +10 -12
  26. examples/qwen_vl/SETUP_COMPLETE.md +7 -8
  27. examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
  28. examples/qwen_vl/collect_data_via_cli.md +76 -84
  29. examples/qwen_vl/collect_vision_traces.py +4 -4
  30. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
  31. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
  32. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
  33. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
  34. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  35. examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
  36. examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
  37. examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
  38. examples/qwen_vl/run_vision_comparison.sh +6 -7
  39. examples/rl/README.md +5 -5
  40. examples/rl/configs/rl_from_base_qwen.toml +26 -1
  41. examples/rl/configs/rl_from_base_qwen17.toml +5 -2
  42. examples/rl/task_app/README.md +1 -2
  43. examples/rl/task_app/math_single_step.py +2 -2
  44. examples/run_crafter_demo.sh +2 -2
  45. examples/sft/README.md +1 -1
  46. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
  47. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
  48. examples/swe/task_app/README.md +32 -2
  49. examples/swe/task_app/grpo_swe_mini.py +4 -0
  50. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  51. examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
  52. examples/swe/task_app/hosted/inference/openai_client.py +4 -4
  53. examples/swe/task_app/morph_backend.py +178 -0
  54. examples/task_apps/crafter/task_app/README.md +1 -1
  55. examples/task_apps/crafter/task_app/grpo_crafter.py +66 -3
  56. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  57. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
  58. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  59. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +17 -49
  60. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +13 -5
  61. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +15 -1
  62. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  63. examples/task_apps/math/README.md +1 -2
  64. examples/task_apps/pokemon_red/README.md +3 -4
  65. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  66. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  67. examples/task_apps/pokemon_red/task_app.py +36 -5
  68. examples/task_apps/sokoban/README.md +2 -3
  69. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  70. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  71. examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
  72. examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
  73. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
  74. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -2
  75. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  76. examples/warming_up_to_rl/task_app/README.md +1 -1
  77. examples/warming_up_to_rl/task_app/grpo_crafter.py +134 -3
  78. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
  79. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
  80. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
  81. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +4 -4
  82. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +6 -3
  83. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  84. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
  85. synth_ai/api/train/builders.py +9 -3
  86. synth_ai/api/train/cli.py +125 -10
  87. synth_ai/api/train/configs/__init__.py +8 -1
  88. synth_ai/api/train/configs/rl.py +32 -7
  89. synth_ai/api/train/configs/sft.py +6 -2
  90. synth_ai/api/train/configs/shared.py +59 -2
  91. synth_ai/auth/credentials.py +119 -0
  92. synth_ai/cli/__init__.py +12 -4
  93. synth_ai/cli/commands/__init__.py +17 -0
  94. synth_ai/cli/commands/demo/__init__.py +6 -0
  95. synth_ai/cli/commands/demo/core.py +163 -0
  96. synth_ai/cli/commands/deploy/__init__.py +23 -0
  97. synth_ai/cli/commands/deploy/core.py +614 -0
  98. synth_ai/cli/commands/deploy/errors.py +72 -0
  99. synth_ai/cli/commands/deploy/validation.py +11 -0
  100. synth_ai/cli/commands/eval/__init__.py +19 -0
  101. synth_ai/cli/commands/eval/core.py +1109 -0
  102. synth_ai/cli/commands/eval/errors.py +81 -0
  103. synth_ai/cli/commands/eval/validation.py +133 -0
  104. synth_ai/cli/commands/filter/__init__.py +12 -0
  105. synth_ai/cli/commands/filter/core.py +388 -0
  106. synth_ai/cli/commands/filter/errors.py +55 -0
  107. synth_ai/cli/commands/filter/validation.py +77 -0
  108. synth_ai/cli/commands/help/__init__.py +177 -0
  109. synth_ai/cli/commands/help/core.py +73 -0
  110. synth_ai/cli/commands/status/__init__.py +64 -0
  111. synth_ai/cli/commands/status/client.py +192 -0
  112. synth_ai/cli/commands/status/config.py +92 -0
  113. synth_ai/cli/commands/status/errors.py +20 -0
  114. synth_ai/cli/commands/status/formatters.py +164 -0
  115. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  116. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  117. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  118. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  119. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  120. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  121. synth_ai/cli/commands/status/utils.py +114 -0
  122. synth_ai/cli/commands/train/__init__.py +53 -0
  123. synth_ai/cli/commands/train/core.py +21 -0
  124. synth_ai/cli/commands/train/errors.py +117 -0
  125. synth_ai/cli/commands/train/judge_schemas.py +199 -0
  126. synth_ai/cli/commands/train/judge_validation.py +304 -0
  127. synth_ai/cli/commands/train/validation.py +443 -0
  128. synth_ai/cli/demo.py +2 -162
  129. synth_ai/cli/deploy/__init__.py +28 -0
  130. synth_ai/cli/deploy/core.py +5 -0
  131. synth_ai/cli/deploy/errors.py +23 -0
  132. synth_ai/cli/deploy/validation.py +5 -0
  133. synth_ai/cli/eval/__init__.py +36 -0
  134. synth_ai/cli/eval/core.py +5 -0
  135. synth_ai/cli/eval/errors.py +31 -0
  136. synth_ai/cli/eval/validation.py +5 -0
  137. synth_ai/cli/filter/__init__.py +28 -0
  138. synth_ai/cli/filter/core.py +5 -0
  139. synth_ai/cli/filter/errors.py +23 -0
  140. synth_ai/cli/filter/validation.py +5 -0
  141. synth_ai/cli/modal_serve/__init__.py +12 -0
  142. synth_ai/cli/modal_serve/core.py +14 -0
  143. synth_ai/cli/modal_serve/errors.py +8 -0
  144. synth_ai/cli/modal_serve/validation.py +11 -0
  145. synth_ai/cli/serve/__init__.py +12 -0
  146. synth_ai/cli/serve/core.py +14 -0
  147. synth_ai/cli/serve/errors.py +8 -0
  148. synth_ai/cli/serve/validation.py +11 -0
  149. synth_ai/cli/setup.py +20 -265
  150. synth_ai/cli/status.py +7 -126
  151. synth_ai/cli/task_app_deploy.py +1 -10
  152. synth_ai/cli/task_app_modal_serve.py +4 -9
  153. synth_ai/cli/task_app_serve.py +4 -11
  154. synth_ai/cli/task_apps.py +58 -1487
  155. synth_ai/cli/train/__init__.py +12 -0
  156. synth_ai/cli/train/core.py +21 -0
  157. synth_ai/cli/train/errors.py +8 -0
  158. synth_ai/cli/train/validation.py +24 -0
  159. synth_ai/cli/train.py +1 -14
  160. synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
  161. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  162. synth_ai/environments/examples/red/engine.py +33 -12
  163. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  164. synth_ai/environments/examples/red/environment.py +26 -0
  165. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  166. synth_ai/http.py +12 -0
  167. synth_ai/judge_schemas.py +10 -11
  168. synth_ai/learning/rl/client.py +3 -1
  169. synth_ai/streaming/__init__.py +29 -0
  170. synth_ai/streaming/config.py +94 -0
  171. synth_ai/streaming/handlers.py +469 -0
  172. synth_ai/streaming/streamer.py +301 -0
  173. synth_ai/streaming/types.py +95 -0
  174. synth_ai/task/validators.py +2 -2
  175. synth_ai/tracing_v3/migration_helper.py +1 -2
  176. synth_ai/utils/env.py +25 -18
  177. synth_ai/utils/http.py +4 -1
  178. synth_ai/utils/modal.py +2 -2
  179. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/METADATA +8 -3
  180. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/RECORD +184 -109
  181. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
  182. synth_ai/cli/tui.py +0 -62
  183. synth_ai/tui/__init__.py +0 -5
  184. synth_ai/tui/__main__.py +0 -13
  185. synth_ai/tui/cli/__init__.py +0 -1
  186. synth_ai/tui/cli/query_experiments.py +0 -164
  187. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  188. synth_ai/tui/dashboard.py +0 -911
  189. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
  190. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
  191. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
  192. {synth_ai-0.2.16.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import (
5
+ EvalCliError,
6
+ EvalConfigNotFoundError,
7
+ EvalConfigParseError,
8
+ InvalidEvalConfigError,
9
+ MetadataFilterFormatError,
10
+ MetadataSQLExecutionError,
11
+ MetadataSQLResultError,
12
+ MissingEvalTableError,
13
+ NoSeedsMatchedError,
14
+ SeedParseError,
15
+ TaskInfoUnavailableError,
16
+ TomlUnavailableError,
17
+ )
18
+ from .validation import validate_eval_options
19
+
20
+ __all__ = [
21
+ "command",
22
+ "get_command",
23
+ "EvalCliError",
24
+ "TomlUnavailableError",
25
+ "EvalConfigNotFoundError",
26
+ "EvalConfigParseError",
27
+ "MissingEvalTableError",
28
+ "InvalidEvalConfigError",
29
+ "SeedParseError",
30
+ "MetadataFilterFormatError",
31
+ "TaskInfoUnavailableError",
32
+ "NoSeedsMatchedError",
33
+ "MetadataSQLExecutionError",
34
+ "MetadataSQLResultError",
35
+ "validate_eval_options",
36
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.eval.core import command, get_command
4
+
5
+ __all__ = ["command", "get_command"]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.eval.errors import (
4
+ EvalCliError,
5
+ EvalConfigNotFoundError,
6
+ EvalConfigParseError,
7
+ InvalidEvalConfigError,
8
+ MetadataFilterFormatError,
9
+ MetadataSQLExecutionError,
10
+ MetadataSQLResultError,
11
+ MissingEvalTableError,
12
+ NoSeedsMatchedError,
13
+ SeedParseError,
14
+ TaskInfoUnavailableError,
15
+ TomlUnavailableError,
16
+ )
17
+
18
+ __all__ = [
19
+ "EvalCliError",
20
+ "TomlUnavailableError",
21
+ "EvalConfigNotFoundError",
22
+ "EvalConfigParseError",
23
+ "MissingEvalTableError",
24
+ "InvalidEvalConfigError",
25
+ "SeedParseError",
26
+ "MetadataFilterFormatError",
27
+ "TaskInfoUnavailableError",
28
+ "NoSeedsMatchedError",
29
+ "MetadataSQLExecutionError",
30
+ "MetadataSQLResultError",
31
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.eval.validation import validate_eval_options
4
+
5
+ __all__ = ["validate_eval_options"]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import (
5
+ FilterCliError,
6
+ FilterConfigNotFoundError,
7
+ FilterConfigParseError,
8
+ InvalidFilterConfigError,
9
+ MissingFilterTableError,
10
+ NoSessionsMatchedError,
11
+ NoTracesFoundError,
12
+ TomlUnavailableError,
13
+ )
14
+ from .validation import validate_filter_options
15
+
16
+ __all__ = [
17
+ "command",
18
+ "get_command",
19
+ "FilterCliError",
20
+ "TomlUnavailableError",
21
+ "FilterConfigNotFoundError",
22
+ "FilterConfigParseError",
23
+ "MissingFilterTableError",
24
+ "InvalidFilterConfigError",
25
+ "NoTracesFoundError",
26
+ "NoSessionsMatchedError",
27
+ "validate_filter_options",
28
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.filter.core import command, get_command
4
+
5
+ __all__ = ["command", "get_command"]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.filter.errors import (
4
+ FilterCliError,
5
+ FilterConfigNotFoundError,
6
+ FilterConfigParseError,
7
+ InvalidFilterConfigError,
8
+ MissingFilterTableError,
9
+ NoSessionsMatchedError,
10
+ NoTracesFoundError,
11
+ TomlUnavailableError,
12
+ )
13
+
14
+ __all__ = [
15
+ "FilterCliError",
16
+ "TomlUnavailableError",
17
+ "FilterConfigNotFoundError",
18
+ "FilterConfigParseError",
19
+ "MissingFilterTableError",
20
+ "InvalidFilterConfigError",
21
+ "NoTracesFoundError",
22
+ "NoSessionsMatchedError",
23
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from synth_ai.cli.commands.filter.validation import validate_filter_options
4
+
5
+ __all__ = ["validate_filter_options"]
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import ModalServeCliError
5
+ from .validation import validate_modal_serve_options
6
+
7
+ __all__ = [
8
+ "command",
9
+ "get_command",
10
+ "ModalServeCliError",
11
+ "validate_modal_serve_options",
12
+ ]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ from synth_ai.cli.task_apps import task_app_group
5
+
6
+ __all__ = ["command", "get_command"]
7
+
8
+ command = task_app_group.commands.get("modal-serve")
9
+
10
+
11
+ def get_command() -> click.Command:
12
+ if command is None:
13
+ raise RuntimeError("modal-serve command is not registered on task_app_group")
14
+ return command
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ModalServeCliError(RuntimeError):
5
+ """Base exception for modal-serve CLI failures."""
6
+
7
+
8
+ __all__ = ["ModalServeCliError"]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping
4
+ from typing import Any
5
+
6
+ __all__ = ["validate_modal_serve_options"]
7
+
8
+
9
+ def validate_modal_serve_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
10
+ """Validate parameters passed to the modal-serve CLI command."""
11
+ return options
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import command, get_command
4
+ from .errors import ServeCliError
5
+ from .validation import validate_serve_options
6
+
7
+ __all__ = [
8
+ "command",
9
+ "get_command",
10
+ "ServeCliError",
11
+ "validate_serve_options",
12
+ ]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+ from synth_ai.cli.task_apps import task_app_group
5
+
6
+ __all__ = ["command", "get_command"]
7
+
8
+ command = task_app_group.commands.get("serve")
9
+
10
+
11
+ def get_command() -> click.Command:
12
+ if command is None:
13
+ raise RuntimeError("Serve command is not registered on task_app_group")
14
+ return command
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ServeCliError(RuntimeError):
5
+ """Base exception for serve CLI failures."""
6
+
7
+
8
+ __all__ = ["ServeCliError"]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping
4
+ from typing import Any
5
+
6
+ __all__ = ["validate_serve_options"]
7
+
8
+
9
+ def validate_serve_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
10
+ """Validate parameters passed to the serve CLI command."""
11
+ return options
synth_ai/cli/setup.py CHANGED
@@ -1,266 +1,21 @@
1
- from __future__ import annotations
2
-
3
- import contextlib
4
- import os
5
- import time
6
- import webbrowser
7
- from pathlib import Path
8
- from typing import Any, cast
9
- from urllib.parse import urljoin, urlsplit, urlunsplit
10
-
11
- import requests
12
- from click.exceptions import Exit
13
- from synth_ai.demos import core as demo_core
14
- from synth_ai.utils.cli import print_next_step
15
- from synth_ai.utils.env import mask_str
16
- from synth_ai.utils.modal import is_modal_public_url
17
- from synth_ai.utils.process import popen_capture
18
- from synth_ai.utils.user_config import USER_CONFIG_PATH, update_user_config
19
-
20
-
21
- class HandshakeError(Exception):
22
- pass
23
-
24
-
25
- def _get_canonical_origin() -> str:
26
- """Resolve the dashboard origin for the browser handshake.
27
-
28
- Priority order:
29
- 1. Explicit ``SYNTH_CANONICAL_ORIGIN`` override.
30
- 2. Development flag ``SYNTH_CANONICAL_DEV`` (case-insensitive truthy) → localhost.
31
- 3. Production dashboard at ``https://www.usesynth.ai/dashboard``.
32
- """
33
-
34
- override = (os.getenv("SYNTH_CANONICAL_ORIGIN") or "").strip()
35
- if override:
36
- return override.rstrip("/")
37
-
38
- dev_flag = (os.getenv("SYNTH_CANONICAL_DEV") or "").strip().lower()
39
- if dev_flag in { "1", "true", "yes", "on" }:
40
- print("USING DEV ORIGIN")
41
- return "http://localhost:3000"
42
-
43
- return "https://www.usesynth.ai/dashboard"
44
-
45
-
46
- def _split_origin(origin: str) -> tuple[str, str]:
47
- parsed = urlsplit(origin)
48
- bare = cast(str, urlunsplit((parsed.scheme, parsed.netloc, "", "", "")))
49
- path = parsed.path.rstrip("/")
50
- return bare, path
51
-
52
-
53
- def _ensure_verification_uri(data: dict[str, Any], base_with_path: str) -> None:
54
- uri = data.get("verification_uri")
55
- if not isinstance(uri, str) or not uri:
56
- return
57
- if uri.startswith("http://") or uri.startswith("https://"):
58
- return
59
- data["verification_uri"] = urljoin(base_with_path.rstrip("/") + "/", uri.lstrip("/"))
60
-
61
-
62
- def _start_handshake_session(origin: str | None = None) -> tuple[str, str, int, int]:
63
- base = (origin or _get_canonical_origin()).rstrip("/")
64
- api_origin, _ = _split_origin(base)
65
- url = urljoin(api_origin.rstrip("/") + "/", "api/sdk/handshake/init")
66
- r = requests.post(url, timeout=10)
67
- if r.status_code != 200:
68
- raise HandshakeError(f"init failed: {r.status_code} {r.text}")
69
- try:
70
- data = r.json()
71
- except ValueError as exc: # pragma: no cover - network dependent
72
- raise HandshakeError(f"init returned malformed JSON: {exc}") from exc
73
- _ensure_verification_uri(data, base)
74
- return (
75
- str(data.get("device_code")),
76
- str(data.get("verification_uri")),
77
- int(data.get("expires_in", 600)),
78
- int(data.get("interval", 3)),
1
+ """Instructions on Docs → https://usesynth.ai/cli-cmds/setup"""
2
+
3
+ import click
4
+ from synth_ai.auth.credentials import fetch_credentials_from_web_browser_session
5
+
6
+
7
+ @click.command("setup")
8
+ @click.option(
9
+ "--local",
10
+ is_flag=True,
11
+ help="Load your credentials from your local machine"
12
+ )
13
+ @click.option(
14
+ "--dev",
15
+ is_flag=True
16
+ )
17
+ def setup_cmd(local: bool, dev: bool) -> None:
18
+ fetch_credentials_from_web_browser_session(
19
+ browser=not local,
20
+ prod=not dev
79
21
  )
80
-
81
-
82
- def _poll_handshake_token(
83
- device_code: str, origin: str | None = None, *, timeout_s: int | None = None
84
- ) -> dict[str, Any]:
85
- base = (origin or _get_canonical_origin()).rstrip("/")
86
- api_origin, _ = _split_origin(base)
87
- url = urljoin(api_origin.rstrip("/") + "/", "api/sdk/handshake/token")
88
- deadline = time.time() + (timeout_s or 600)
89
- while True:
90
- if time.time() > deadline:
91
- raise HandshakeError("handshake timed out")
92
- try:
93
- r = requests.post(url, json={"device_code": device_code}, timeout=10)
94
- except Exception:
95
- time.sleep(2)
96
- continue
97
- if r.status_code == 200:
98
- try:
99
- data = r.json()
100
- except ValueError as exc: # pragma: no cover - network dependent
101
- raise HandshakeError(f"token returned malformed JSON: {exc}") from exc
102
- _ensure_verification_uri(data, base)
103
- return data
104
- elif r.status_code in (404, 410):
105
- raise HandshakeError(f"handshake failed: {r.status_code}")
106
- # 428 authorization_pending or others → wait and retry
107
- time.sleep(2)
108
-
109
-
110
- def _run_handshake(origin: str | None = None) -> dict[str, Any]:
111
- device_code, verification_uri, expires_in, interval = _start_handshake_session(origin)
112
- with contextlib.suppress(Exception):
113
- webbrowser.open(verification_uri)
114
- return _poll_handshake_token(device_code, origin, timeout_s=expires_in)
115
-
116
-
117
-
118
- def setup() -> int:
119
- # Prefer the demo directory provided in the current shell session, then fall back to persisted state
120
- demo_dir_env = (os.environ.get("DEMO_DIR") or "").strip()
121
- demo_dir: str | None = None
122
- if demo_dir_env:
123
- candidate = Path(demo_dir_env).expanduser()
124
- if candidate.is_dir():
125
- demo_dir = str(candidate.resolve())
126
- else:
127
- print(f"Warning: DEMO_DIR={demo_dir_env} does not exist; falling back to stored demo directory.")
128
-
129
- if demo_dir is None:
130
- loaded = demo_core.load_demo_dir()
131
- if loaded:
132
- demo_dir = loaded
133
-
134
- if demo_dir and os.path.isdir(demo_dir):
135
- os.chdir(demo_dir)
136
- print(f"Using demo directory: {demo_dir}")
137
-
138
- synth_key = ""
139
- rl_env_key = ""
140
- org_name = ""
141
-
142
- try:
143
- print("\n⏳ Connecting to your browser session…")
144
- res = _run_handshake()
145
- org = res.get("org") or {}
146
- keys = res.get("keys") or {}
147
- synth_key = str(keys.get("synth") or "").strip()
148
- rl_env_key = str(keys.get("rl_env") or "").strip()
149
- org_name = org.get("name") or "Unamed Organization ™️"
150
- print(f"✅ Connected to {org_name}!")
151
- except (HandshakeError, Exception) as exc:
152
- print(f"⚠️ Failed to fetch keys from frontend: {exc}")
153
- print("Falling back to manual entry...")
154
-
155
- if not synth_key:
156
- try:
157
- synth_key = input(
158
- "Failed to fetch your Synth API key. Please enter your Synth API key here:\n> "
159
- ).strip()
160
- except (EOFError, KeyboardInterrupt):
161
- print("\nSetup cancelled.")
162
- return 1
163
- if not synth_key:
164
- print("Synth API key is required.")
165
- return 1
166
-
167
- if not rl_env_key:
168
- try:
169
- rl_env_key = input(
170
- "Failed to fetch your Environment API key. Please enter your Environment API key here:\n> "
171
- ).strip()
172
- except (EOFError, KeyboardInterrupt):
173
- print("\nSetup cancelled.")
174
- return 1
175
- if not rl_env_key:
176
- print("Environment API key is required.")
177
- return 1
178
-
179
- # Persist keys to user config
180
- config_updates = {
181
- "SYNTH_API_KEY": synth_key,
182
- "ENVIRONMENT_API_KEY": rl_env_key,
183
- }
184
- update_user_config(config_updates)
185
-
186
- os.environ["SYNTH_API_KEY"] = synth_key
187
- os.environ["ENVIRONMENT_API_KEY"] = rl_env_key
188
-
189
- env = demo_core.load_env()
190
-
191
- def _refresh_env() -> None:
192
- nonlocal env
193
- env = demo_core.load_env()
194
-
195
- def _maybe_fix_task_url() -> None:
196
- if not env.task_app_name:
197
- return
198
- current = env.task_app_base_url
199
- needs_lookup = not current or not is_modal_public_url(current)
200
- if not needs_lookup:
201
- return
202
- code, out = popen_capture(
203
- [
204
- "uv",
205
- "run",
206
- "python",
207
- "-m",
208
- "modal",
209
- "app",
210
- "url",
211
- env.task_app_name,
212
- ]
213
- )
214
- if code != 0 or not out:
215
- return
216
- new_url = ""
217
- for token in out.split():
218
- if is_modal_public_url(token):
219
- new_url = token.strip().rstrip("/")
220
- break
221
- if new_url and new_url != current:
222
- print(f"Updating TASK_APP_BASE_URL from Modal CLI → {new_url}")
223
- persist_path = demo_dir or os.getcwd()
224
- demo_core.persist_task_url(new_url, name=env.task_app_name, path=persist_path)
225
- os.environ["TASK_APP_BASE_URL"] = new_url
226
- _refresh_env()
227
-
228
- modal_ok, modal_msg = demo_core.modal_auth_status()
229
- if modal_ok:
230
- print(f"✓ Modal authenticated: {modal_msg}")
231
- else:
232
- print(f"[setup] Modal authentication status: {modal_msg}")
233
-
234
- _maybe_fix_task_url()
235
-
236
- if env.dev_backend_url:
237
- api = env.dev_backend_url.rstrip("/") + (
238
- "" if env.dev_backend_url.endswith("/api") else "/api"
239
- )
240
- demo_core.assert_http_ok(api + "/health", method="GET")
241
- if env.task_app_base_url:
242
- base = env.task_app_base_url.rstrip("/")
243
- demo_core.assert_http_ok(
244
- base + "/health", method="GET"
245
- ) or demo_core.assert_http_ok(
246
- base, method="GET"
247
- )
248
- print("\nSaved keys:")
249
- print(f" SYNTH_API_KEY={mask_str(synth_key)}")
250
- print(f" ENVIRONMENT_API_KEY={mask_str(rl_env_key)}")
251
- if env.task_app_base_url:
252
- print(f" TASK_APP_BASE_URL={env.task_app_base_url}")
253
- print(f"Configuration persisted to: {USER_CONFIG_PATH}")
254
-
255
- demo_core.persist_demo_dir(os.getcwd())
256
-
257
- print_next_step("deploy our task app", ["uvx synth-ai deploy"])
258
- return 0
259
-
260
-
261
- def register(group):
262
- @group.command("setup")
263
- def demo_setup():
264
- code = setup()
265
- if code:
266
- raise Exit(code)
synth_ai/cli/status.py CHANGED
@@ -1,134 +1,15 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- CLI: status of agent runs/versions and environment service.
4
- """
2
+ """Compatibility wrapper for legacy status imports."""
5
3
 
6
- import asyncio
4
+ from __future__ import annotations
7
5
 
8
6
  import click
9
- import requests
10
- from rich import box
11
- from rich.console import Console
12
- from rich.panel import Panel
13
- from rich.table import Table
7
+ from synth_ai.cli.commands.status import register as _register_status
14
8
 
15
- from ._storage import load_storage
16
9
 
10
+ def register(cli: click.Group) -> None:
11
+ """Register status subcommands on the provided CLI group."""
12
+ _register_status(cli)
17
13
 
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))
21
- await db.initialize()
22
- try:
23
- out: dict = {}
24
- # Totals
25
- totals = await db.query_traces(
26
- """
27
- SELECT
28
- (SELECT COUNT(*) FROM session_traces) AS sessions,
29
- (SELECT COUNT(*) FROM experiments) AS experiments,
30
- (SELECT COUNT(*) FROM events) AS events,
31
- (SELECT COUNT(*) FROM messages) AS messages,
32
- (SELECT COALESCE(SUM(CASE WHEN event_type='cais' THEN cost_usd ELSE 0 END),0)/100.0 FROM events) AS total_cost_usd,
33
- (SELECT COALESCE(SUM(CASE WHEN event_type='cais' THEN total_tokens ELSE 0 END),0) FROM events) AS total_tokens
34
- """
35
- )
36
- if not totals.empty:
37
- out["totals"] = totals.iloc[0].to_dict()
38
- else:
39
- out["totals"] = {}
40
14
 
41
- # Systems summary
42
- systems = await db.query_traces(
43
- """
44
- SELECT system_type, COUNT(*) as count FROM systems GROUP BY system_type
45
- """
46
- )
47
- out["systems"] = systems
48
-
49
- versions = await db.query_traces(
50
- """
51
- SELECT COUNT(*) as version_count FROM system_versions
52
- """
53
- )
54
- if not versions.empty:
55
- out["version_count"] = int(versions.iloc[0]["version_count"])
56
- else:
57
- out["version_count"] = 0
58
- return out
59
- finally:
60
- await db.close()
61
-
62
-
63
- def register(cli):
64
- @cli.command()
65
- @click.option(
66
- "--url",
67
- "db_url",
68
- default="sqlite+aiosqlite:///./synth_ai.db/dbs/default/data",
69
- help="Database URL",
70
- )
71
- @click.option("--service-url", default="http://127.0.0.1:8901", help="Environment service URL")
72
- def status(db_url: str, service_url: str):
73
- """Show DB stats, agent/environment system counts, and env service health."""
74
- console = Console()
75
-
76
- async def _run():
77
- # DB
78
- stats = await _db_stats(db_url)
79
-
80
- # Env service
81
- health_text = "[red]unreachable[/red]"
82
- envs_list = []
83
- try:
84
- r = requests.get(f"{service_url}/health", timeout=2)
85
- if r.ok:
86
- data = r.json()
87
- health_text = "[green]ok[/green]"
88
- envs_list = data.get("supported_environments", [])
89
- else:
90
- health_text = f"[red]{r.status_code}[/red]"
91
- except Exception:
92
- pass
93
-
94
- # Render
95
- totals = stats.get("totals", {})
96
- lines = []
97
- lines.append(f"DB: [dim]{db_url}[/dim]")
98
- lines.append(
99
- f"Experiments: {int(totals.get('experiments', 0)):,} "
100
- f"Sessions: {int(totals.get('sessions', 0)):,} "
101
- f"Events: {int(totals.get('events', 0)):,} "
102
- f"Messages: {int(totals.get('messages', 0)):,}"
103
- )
104
- lines.append(
105
- f"Cost: ${float(totals.get('total_cost_usd', 0.0) or 0.0):.4f} "
106
- f"Tokens: {int(totals.get('total_tokens', 0)):,}"
107
- )
108
- lines.append("")
109
- lines.append(f"Env Service: {health_text} [dim]{service_url}[/dim]")
110
- if envs_list:
111
- lines.append(
112
- "Environments: "
113
- + ", ".join(sorted(envs_list)[:10])
114
- + (" ..." if len(envs_list) > 10 else "")
115
- )
116
-
117
- panel_main = Panel("\n".join(lines), title="Synth AI Status", border_style="cyan")
118
- console.print(panel_main)
119
-
120
- # Systems table
121
- sys_df = stats.get("systems")
122
- if sys_df is not None and not sys_df.empty:
123
- tbl = Table(
124
- title=f"Systems (versions: {stats.get('version_count', 0)})",
125
- box=box.SIMPLE,
126
- header_style="bold",
127
- )
128
- tbl.add_column("Type")
129
- tbl.add_column("Count", justify="right")
130
- for _, r in sys_df.iterrows():
131
- tbl.add_row(str(r.get("system_type", "-")), f"{int(r.get('count', 0)):,}")
132
- console.print(tbl)
133
-
134
- asyncio.run(_run())
15
+ __all__ = ["register"]