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.

Files changed (169) hide show
  1. examples/baseline/banking77_baseline.py +204 -0
  2. examples/baseline/crafter_baseline.py +407 -0
  3. examples/baseline/pokemon_red_baseline.py +326 -0
  4. examples/baseline/simple_baseline.py +56 -0
  5. examples/baseline/warming_up_to_rl_baseline.py +239 -0
  6. examples/blog_posts/gepa/README.md +355 -0
  7. examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
  8. examples/blog_posts/gepa/configs/banking77_gepa_test.toml +82 -0
  9. examples/blog_posts/gepa/configs/banking77_mipro_local.toml +52 -0
  10. examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +59 -0
  11. examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +36 -0
  12. examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +53 -0
  13. examples/blog_posts/gepa/configs/hover_gepa_local.toml +59 -0
  14. examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +36 -0
  15. examples/blog_posts/gepa/configs/hover_mipro_local.toml +53 -0
  16. examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +59 -0
  17. examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +36 -0
  18. examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +53 -0
  19. examples/blog_posts/gepa/configs/pupa_gepa_local.toml +60 -0
  20. examples/blog_posts/gepa/configs/pupa_mipro_local.toml +54 -0
  21. examples/blog_posts/gepa/deploy_banking77_task_app.sh +41 -0
  22. examples/blog_posts/gepa/gepa_baseline.py +204 -0
  23. examples/blog_posts/gepa/query_prompts_example.py +97 -0
  24. examples/blog_posts/gepa/run_gepa_banking77.sh +87 -0
  25. examples/blog_posts/gepa/task_apps.py +105 -0
  26. examples/blog_posts/gepa/test_gepa_local.sh +67 -0
  27. examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
  28. examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
  29. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +12 -10
  30. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +1 -0
  31. examples/blog_posts/pokemon_vl/extract_images.py +239 -0
  32. examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
  33. examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
  34. examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
  35. examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
  36. examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
  37. examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
  38. examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
  39. examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
  40. examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
  41. examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
  42. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
  43. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +1 -1
  44. examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
  45. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +60 -10
  46. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +1 -1
  47. examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
  48. examples/multi_step/configs/VERILOG_REWARDS.md +4 -0
  49. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +4 -0
  50. examples/multi_step/configs/crafter_rl_outcome.toml +1 -0
  51. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -0
  52. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -0
  53. examples/rl/configs/rl_from_base_qwen17.toml +1 -0
  54. examples/swe/task_app/hosted/inference/openai_client.py +0 -34
  55. examples/swe/task_app/hosted/policy_routes.py +17 -0
  56. examples/swe/task_app/hosted/rollout.py +4 -2
  57. examples/task_apps/banking77/__init__.py +6 -0
  58. examples/task_apps/banking77/banking77_task_app.py +841 -0
  59. examples/task_apps/banking77/deploy_wrapper.py +46 -0
  60. examples/task_apps/crafter/CREATE_SFT_DATASET.md +4 -0
  61. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +4 -0
  62. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +4 -0
  63. examples/task_apps/crafter/task_app/grpo_crafter.py +24 -2
  64. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +49 -0
  65. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +355 -58
  66. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +68 -7
  67. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +78 -21
  68. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -1
  69. examples/task_apps/gepa_benchmarks/__init__.py +7 -0
  70. examples/task_apps/gepa_benchmarks/common.py +260 -0
  71. examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
  72. examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
  73. examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
  74. examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
  75. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
  76. examples/task_apps/pokemon_red/task_app.py +254 -36
  77. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +1 -0
  78. examples/warming_up_to_rl/task_app/grpo_crafter.py +53 -4
  79. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +49 -0
  80. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +152 -41
  81. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +31 -1
  82. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +33 -3
  83. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +67 -0
  84. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +1 -0
  85. synth_ai/api/train/builders.py +90 -1
  86. synth_ai/api/train/cli.py +396 -21
  87. synth_ai/api/train/config_finder.py +13 -2
  88. synth_ai/api/train/configs/__init__.py +15 -1
  89. synth_ai/api/train/configs/prompt_learning.py +442 -0
  90. synth_ai/api/train/configs/rl.py +29 -0
  91. synth_ai/api/train/task_app.py +1 -1
  92. synth_ai/api/train/validators.py +277 -0
  93. synth_ai/baseline/__init__.py +25 -0
  94. synth_ai/baseline/config.py +209 -0
  95. synth_ai/baseline/discovery.py +214 -0
  96. synth_ai/baseline/execution.py +146 -0
  97. synth_ai/cli/__init__.py +85 -17
  98. synth_ai/cli/__main__.py +0 -0
  99. synth_ai/cli/claude.py +70 -0
  100. synth_ai/cli/codex.py +84 -0
  101. synth_ai/cli/commands/__init__.py +1 -0
  102. synth_ai/cli/commands/baseline/__init__.py +12 -0
  103. synth_ai/cli/commands/baseline/core.py +637 -0
  104. synth_ai/cli/commands/baseline/list.py +93 -0
  105. synth_ai/cli/commands/eval/core.py +13 -10
  106. synth_ai/cli/commands/filter/core.py +53 -17
  107. synth_ai/cli/commands/help/core.py +0 -1
  108. synth_ai/cli/commands/smoke/__init__.py +7 -0
  109. synth_ai/cli/commands/smoke/core.py +1436 -0
  110. synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
  111. synth_ai/cli/commands/status/subcommands/usage.py +203 -0
  112. synth_ai/cli/commands/train/judge_schemas.py +1 -0
  113. synth_ai/cli/commands/train/judge_validation.py +1 -0
  114. synth_ai/cli/commands/train/validation.py +0 -57
  115. synth_ai/cli/demo.py +35 -3
  116. synth_ai/cli/deploy/__init__.py +40 -25
  117. synth_ai/cli/deploy.py +162 -0
  118. synth_ai/cli/legacy_root_backup.py +14 -8
  119. synth_ai/cli/opencode.py +107 -0
  120. synth_ai/cli/root.py +9 -5
  121. synth_ai/cli/task_app_deploy.py +1 -1
  122. synth_ai/cli/task_apps.py +53 -53
  123. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
  124. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
  125. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
  126. synth_ai/judge_schemas.py +1 -0
  127. synth_ai/learning/__init__.py +10 -0
  128. synth_ai/learning/prompt_learning_client.py +276 -0
  129. synth_ai/learning/prompt_learning_types.py +184 -0
  130. synth_ai/pricing/__init__.py +2 -0
  131. synth_ai/pricing/model_pricing.py +57 -0
  132. synth_ai/streaming/handlers.py +53 -4
  133. synth_ai/streaming/streamer.py +19 -0
  134. synth_ai/task/apps/__init__.py +1 -0
  135. synth_ai/task/config.py +2 -0
  136. synth_ai/task/tracing_utils.py +25 -25
  137. synth_ai/task/validators.py +44 -8
  138. synth_ai/task_app_cfgs.py +21 -0
  139. synth_ai/tracing_v3/config.py +162 -19
  140. synth_ai/tracing_v3/constants.py +1 -1
  141. synth_ai/tracing_v3/db_config.py +24 -38
  142. synth_ai/tracing_v3/storage/config.py +47 -13
  143. synth_ai/tracing_v3/storage/factory.py +3 -3
  144. synth_ai/tracing_v3/turso/daemon.py +113 -11
  145. synth_ai/tracing_v3/turso/native_manager.py +92 -16
  146. synth_ai/types.py +8 -0
  147. synth_ai/urls.py +11 -0
  148. synth_ai/utils/__init__.py +30 -1
  149. synth_ai/utils/agents.py +74 -0
  150. synth_ai/utils/bin.py +39 -0
  151. synth_ai/utils/cli.py +149 -5
  152. synth_ai/utils/env.py +17 -17
  153. synth_ai/utils/json.py +72 -0
  154. synth_ai/utils/modal.py +283 -1
  155. synth_ai/utils/paths.py +48 -0
  156. synth_ai/utils/uvicorn.py +113 -0
  157. {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/METADATA +102 -4
  158. {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/RECORD +162 -88
  159. synth_ai/cli/commands/deploy/__init__.py +0 -23
  160. synth_ai/cli/commands/deploy/core.py +0 -614
  161. synth_ai/cli/commands/deploy/errors.py +0 -72
  162. synth_ai/cli/commands/deploy/validation.py +0 -11
  163. synth_ai/cli/deploy/core.py +0 -5
  164. synth_ai/cli/deploy/errors.py +0 -23
  165. synth_ai/cli/deploy/validation.py +0 -5
  166. {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/WHEEL +0 -0
  167. {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
  168. {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
  169. {synth_ai-0.2.17.dist-info → synth_ai-0.2.19.dist-info}/top_level.txt +0 -0
@@ -1,614 +0,0 @@
1
- import os
2
- from collections.abc import Sequence
3
- from functools import lru_cache
4
- from pathlib import Path
5
- from typing import Any, Literal
6
-
7
- import click
8
- from synth_ai.demos import core as demo_core
9
- from synth_ai.utils.modal import is_modal_public_url
10
- from synth_ai.utils.process import popen_capture
11
-
12
- from .errors import (
13
- DeployCliError,
14
- EnvFileDiscoveryError,
15
- EnvironmentKeyLoadError,
16
- EnvKeyPreflightError,
17
- MissingEnvironmentApiKeyError,
18
- ModalCliResolutionError,
19
- ModalExecutionError,
20
- TaskAppNotFoundError,
21
- )
22
-
23
- try:
24
- from synth_ai.cli.commands.help import DEPLOY_HELP
25
- except ImportError:
26
- DEPLOY_HELP = """
27
- Deploy a Synth AI task app locally or to Modal.
28
-
29
- OVERVIEW
30
- --------
31
- The deploy command supports two runtimes:
32
- • modal: Deploy to Modal's cloud platform (default)
33
- • uvicorn: Run locally with FastAPI/Uvicorn
34
-
35
- BASIC USAGE
36
- -----------
37
- # Deploy to Modal (production)
38
- uvx synth-ai deploy
39
-
40
- # Deploy specific task app
41
- uvx synth-ai deploy my-math-app
42
-
43
- # Run locally for development
44
- uvx synth-ai deploy --runtime=uvicorn --port 8001
45
-
46
- MODAL DEPLOYMENT
47
- ----------------
48
- Modal deployment requires:
49
- 1. Modal authentication (run: modal token new)
50
- 2. ENVIRONMENT_API_KEY (run: uvx synth-ai setup)
51
-
52
- Options:
53
- --modal-mode [deploy|serve] Use 'deploy' for production (default),
54
- 'serve' for ephemeral development
55
- --name TEXT Override Modal app name
56
- --dry-run Preview the deploy command without executing
57
- --env-file PATH Env file(s) to load (can be repeated)
58
-
59
- Examples:
60
- # Standard production deployment
61
- uvx synth-ai deploy --runtime=modal
62
-
63
- # Deploy with custom name
64
- uvx synth-ai deploy --runtime=modal --name my-task-app-v2
65
-
66
- # Preview deployment command
67
- uvx synth-ai deploy --dry-run
68
-
69
- # Deploy with custom env file
70
- uvx synth-ai deploy --env-file .env.production
71
-
72
- LOCAL DEVELOPMENT
73
- -----------------
74
- Run locally with auto-reload and tracing:
75
-
76
- uvx synth-ai deploy --runtime=uvicorn --port 8001 --reload
77
-
78
- Options:
79
- --host TEXT Bind address (default: 0.0.0.0)
80
- --port INTEGER Port number (prompted if not provided)
81
- --reload/--no-reload Enable auto-reload on code changes
82
- --force/--no-force Kill existing process on port
83
- --trace PATH Enable tracing to directory (default: traces/v3)
84
- --trace-db PATH SQLite DB for traces
85
-
86
- Examples:
87
- # Basic local server
88
- uvx synth-ai deploy --runtime=uvicorn
89
-
90
- # Development with auto-reload
91
- uvx synth-ai deploy --runtime=uvicorn --reload --port 8001
92
-
93
- # With custom trace directory
94
- uvx synth-ai deploy --runtime=uvicorn --trace ./my-traces
95
-
96
- TROUBLESHOOTING
97
- ---------------
98
- Common issues:
99
-
100
- 1. "ENVIRONMENT_API_KEY is required"
101
- → Run: uvx synth-ai setup
102
-
103
- 2. "Modal CLI not found"
104
- → Install: pip install modal
105
- → Authenticate: modal token new
106
-
107
- 3. "Task app not found"
108
- → Check app_id matches your task_app.py configuration
109
- → Run: uvx synth-ai task-app list (if available)
110
-
111
- 4. "Port already in use" (uvicorn)
112
- → Use --force to kill existing process
113
- → Or specify different --port
114
-
115
- 5. "No env file discovered"
116
- → Create .env file with required keys
117
- → Or pass --env-file explicitly
118
-
119
- ENVIRONMENT VARIABLES
120
- ---------------------
121
- SYNTH_API_KEY Your Synth platform API key
122
- ENVIRONMENT_API_KEY Task environment authentication
123
- TASK_APP_BASE_URL Base URL for deployed task app
124
- DEMO_DIR Demo directory path
125
- SYNTH_DEMO_DIR Alternative demo directory
126
-
127
- For more information: https://docs.usesynth.ai/deploy
128
- """ # type: ignore[assignment]
129
-
130
- try: # Click >= 8.1
131
- from click.core import ParameterSource
132
- except ImportError: # pragma: no cover - fallback for older versions
133
- ParameterSource = None # type: ignore[assignment]
134
-
135
- __all__ = [
136
- "command",
137
- "get_command",
138
- "modal_serve_command",
139
- "register_task_app_commands",
140
- "run_modal_runtime",
141
- "run_uvicorn_runtime",
142
- ]
143
-
144
-
145
- def _translate_click_exception(err: click.ClickException) -> DeployCliError | None:
146
- message = getattr(err, "message", str(err)).strip()
147
- lower = message.lower()
148
-
149
- def _missing_env_hint() -> str:
150
- return (
151
- "Run `uvx synth-ai setup` to mint credentials or pass --env-file pointing to a file "
152
- "with ENVIRONMENT_API_KEY."
153
- )
154
-
155
- if "environment_api_key missing" in lower:
156
- return MissingEnvironmentApiKeyError(hint=_missing_env_hint())
157
- if "environment api key is required" in lower:
158
- return MissingEnvironmentApiKeyError(hint=_missing_env_hint())
159
- if "failed to load environment_api_key from generated .env" in lower:
160
- return EnvironmentKeyLoadError()
161
-
162
- if message.startswith("Env file not found:"):
163
- path = message.split(":", 1)[1].strip()
164
- return EnvFileDiscoveryError(attempted=(path,), hint=_missing_env_hint())
165
- if "env file required (--env-file) for this task app" in lower:
166
- return EnvFileDiscoveryError(hint=_missing_env_hint())
167
- if message.startswith("No .env file discovered automatically"):
168
- return EnvFileDiscoveryError(hint=_missing_env_hint())
169
- if message == "No environment values found":
170
- return EnvFileDiscoveryError(hint=_missing_env_hint())
171
-
172
- if message.startswith("Task app '") and " not found. Available:" in message:
173
- try:
174
- before, after = message.split(" not found. Available:", 1)
175
- app_id = before.split("Task app '", 1)[1].rstrip("'")
176
- available = tuple(item.strip() for item in after.split(",") if item.strip())
177
- except Exception:
178
- app_id = None
179
- available = ()
180
- return TaskAppNotFoundError(app_id=app_id, available=available)
181
- if message == "No task apps discovered for this command.":
182
- return TaskAppNotFoundError()
183
-
184
- if "modal cli not found" in lower:
185
- return ModalCliResolutionError(detail=message)
186
- if "--modal-cli path does not exist" in lower or "--modal-cli is not executable" in lower:
187
- return ModalCliResolutionError(detail=message)
188
- if "modal cli resolution found the synth-ai shim" in lower:
189
- return ModalCliResolutionError(detail=message)
190
-
191
- if message.startswith("modal ") and "failed with exit code" in message:
192
- parts = message.split(" failed with exit code ")
193
- command = parts[0].replace("modal ", "").strip() if len(parts) > 1 else "deploy"
194
- exit_code = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else -1
195
- return ModalExecutionError(command=command, exit_code=exit_code)
196
-
197
- if message.startswith("[CRITICAL] ") or "[CRITICAL]" in message:
198
- return EnvKeyPreflightError(detail=message.removeprefix("[CRITICAL] ").strip())
199
-
200
- return None
201
-
202
-
203
- def _format_deploy_error(err: DeployCliError) -> str:
204
- if isinstance(err, MissingEnvironmentApiKeyError):
205
- hint = err.hint or "Provide ENVIRONMENT_API_KEY via --env-file or run `uvx synth-ai setup`."
206
- return f"ENVIRONMENT_API_KEY is required. {hint}"
207
- if isinstance(err, EnvironmentKeyLoadError):
208
- base = "Failed to persist or reload ENVIRONMENT_API_KEY"
209
- if err.path:
210
- base += f" from {err.path}"
211
- return f"{base}. Regenerate the env file with `uvx synth-ai setup` or edit it manually."
212
- if isinstance(err, EnvFileDiscoveryError):
213
- attempted = ", ".join(err.attempted) if err.attempted else "No env files located"
214
- hint = err.hint or "Pass --env-file explicitly or run `uvx synth-ai setup`."
215
- return f"Unable to locate a usable env file ({attempted}). {hint}"
216
- if isinstance(err, TaskAppNotFoundError):
217
- available = ", ".join(err.available) if err.available else "no registered apps"
218
- app_id = err.app_id or "requested app"
219
- return f"Could not find task app '{app_id}'. Available choices: {available}."
220
- if isinstance(err, ModalCliResolutionError):
221
- detail = err.detail or "Modal CLI could not be resolved."
222
- return (
223
- f"{detail} Install the `modal` package or pass --modal-cli with the path to the Modal binary."
224
- )
225
- if isinstance(err, ModalExecutionError):
226
- return (
227
- f"Modal {err.command} exited with status {err.exit_code}. "
228
- "Review the Modal output above or rerun with --dry-run."
229
- )
230
- if isinstance(err, EnvKeyPreflightError):
231
- detail = err.detail or "Failed to upload ENVIRONMENT_API_KEY to the backend."
232
- return f"{detail} Ensure SYNTH_API_KEY is set and retry `uvx synth-ai setup`."
233
- return str(err)
234
-
235
-
236
- @lru_cache(maxsize=1)
237
- def _task_apps_module():
238
- from synth_ai.cli import task_apps as module # local import to avoid circular deps
239
-
240
- return module
241
-
242
-
243
- def _maybe_fix_task_url(modal_name: str | None = None, demo_dir: str | None = None) -> None:
244
- """Look up the Modal public URL and persist it to the task app config if needed."""
245
- env = demo_core.load_env()
246
- task_app_name = modal_name or env.task_app_name
247
- if not task_app_name:
248
- return
249
- current = env.task_app_base_url
250
- needs_lookup = not current or not is_modal_public_url(current)
251
- if not needs_lookup:
252
- return
253
- code, out = popen_capture(
254
- [
255
- "uv",
256
- "run",
257
- "python",
258
- "-m",
259
- "modal",
260
- "app",
261
- "url",
262
- task_app_name,
263
- ]
264
- )
265
- if code != 0 or not out:
266
- return
267
- new_url = ""
268
- for token in out.split():
269
- if is_modal_public_url(token):
270
- new_url = token.strip().rstrip("/")
271
- break
272
- if new_url and new_url != current:
273
- click.echo(f"Updating TASK_APP_BASE_URL from Modal CLI → {new_url}")
274
- persist_path = demo_dir or os.getcwd()
275
- demo_core.persist_task_url(new_url, name=task_app_name, path=persist_path)
276
- os.environ["TASK_APP_BASE_URL"] = new_url
277
-
278
-
279
- def run_uvicorn_runtime(
280
- app_id: str | None,
281
- host: str,
282
- port: int | None,
283
- env_file: Sequence[str],
284
- reload_flag: bool,
285
- force: bool,
286
- trace_dir: str | None,
287
- trace_db: str | None,
288
- ) -> None:
289
- module = _task_apps_module()
290
-
291
- if not host:
292
- host = "0.0.0.0"
293
-
294
- try:
295
- demo_dir_path = module._load_demo_directory()
296
- if demo_dir_path:
297
- if not demo_dir_path.is_dir():
298
- raise click.ClickException(
299
- f"Demo directory not found: {demo_dir_path}\nRun 'synth-ai setup' to create a demo."
300
- )
301
- os.chdir(demo_dir_path)
302
- click.echo(f"Using demo directory: {demo_dir_path}\n")
303
- os.environ["SYNTH_DEMO_DIR"] = str(demo_dir_path.resolve())
304
-
305
- if port is None:
306
- port = click.prompt("Port to serve on", type=int, default=8001)
307
-
308
- if trace_dir is None:
309
- click.echo(
310
- "\nTracing captures rollout data (actions, rewards, model outputs) to a local SQLite DB."
311
- )
312
- click.echo("This data can be exported to JSONL for supervised fine-tuning (SFT).")
313
- enable_tracing = click.confirm("Enable tracing?", default=True)
314
- if enable_tracing:
315
- demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
316
- default_trace_dir = str((demo_base / "traces/v3").resolve())
317
- trace_dir = click.prompt(
318
- "Trace directory", type=str, default=default_trace_dir, show_default=True
319
- )
320
- else:
321
- trace_dir = None
322
-
323
- if trace_dir and trace_db is None:
324
- demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
325
- default_trace_db = str((demo_base / "traces/v3/synth_ai.db").resolve())
326
- trace_db = click.prompt(
327
- "Trace DB path", type=str, default=default_trace_db, show_default=True
328
- )
329
-
330
- choice = module._select_app_choice(app_id, purpose="serve")
331
- entry = choice.ensure_entry()
332
- module._serve_entry(
333
- entry,
334
- host,
335
- port,
336
- env_file,
337
- reload_flag,
338
- force,
339
- trace_dir=trace_dir,
340
- trace_db=trace_db,
341
- )
342
- except DeployCliError:
343
- raise
344
- except click.ClickException as err:
345
- converted = _translate_click_exception(err)
346
- if converted:
347
- raise converted from err
348
- raise
349
-
350
-
351
- def run_modal_runtime(
352
- app_id: str | None,
353
- *,
354
- command: Literal["deploy", "serve"],
355
- modal_name: str | None,
356
- dry_run: bool,
357
- modal_cli: str,
358
- env_file: Sequence[str],
359
- use_demo_dir: bool = True,
360
- ) -> None:
361
- module = _task_apps_module()
362
-
363
- try:
364
- demo_dir_path = None
365
- if use_demo_dir:
366
- demo_dir_path = module._load_demo_directory()
367
- if demo_dir_path:
368
- if not demo_dir_path.is_dir():
369
- raise click.ClickException(
370
- f"Demo directory not found: {demo_dir_path}\nRun 'synth-ai demo' to create a demo."
371
- )
372
- os.chdir(demo_dir_path)
373
- click.echo(f"Using demo directory: {demo_dir_path}\n")
374
-
375
- purpose = "modal-serve" if command == "serve" else "deploy"
376
- choice = module._select_app_choice(app_id, purpose=purpose)
377
-
378
- if choice.modal_script:
379
- env_paths = module._resolve_env_paths_for_script(choice.modal_script, env_file)
380
- click.echo("Using env file(s): " + ", ".join(str(p.resolve()) for p in env_paths))
381
- module._run_modal_script(
382
- choice.modal_script,
383
- modal_cli,
384
- command,
385
- env_paths,
386
- modal_name=modal_name,
387
- dry_run=dry_run if command == "deploy" else False,
388
- )
389
- if command == "deploy" and not dry_run:
390
- _maybe_fix_task_url(
391
- modal_name=modal_name,
392
- demo_dir=str(demo_dir_path) if demo_dir_path else None
393
- )
394
- return
395
-
396
- entry = choice.ensure_entry()
397
- if command == "serve":
398
- click.echo(f"[modal-serve] serving entry {entry.app_id} from {choice.path}")
399
- module._modal_serve_entry(entry, modal_name, modal_cli, env_file, original_path=choice.path)
400
- else:
401
- module._deploy_entry(entry, modal_name, dry_run, modal_cli, env_file, original_path=choice.path)
402
- if not dry_run:
403
- _maybe_fix_task_url(
404
- modal_name=modal_name,
405
- demo_dir=str(demo_dir_path) if demo_dir_path else None
406
- )
407
- except DeployCliError:
408
- raise
409
- except click.ClickException as err:
410
- converted = _translate_click_exception(err)
411
- if converted:
412
- raise converted from err
413
- raise
414
-
415
-
416
- @click.command(
417
- "deploy",
418
- help=DEPLOY_HELP,
419
- epilog="Run 'uvx synth-ai deploy --help' for detailed usage information.",
420
- )
421
- @click.argument("app_id", type=str, required=False)
422
- @click.option(
423
- "--runtime",
424
- type=click.Choice(["modal", "uvicorn"], case_sensitive=False),
425
- default="modal",
426
- show_default=True,
427
- help="Runtime to execute: 'modal' for remote Modal jobs, 'uvicorn' for the local FastAPI server.",
428
- )
429
- @click.option("--name", "modal_name", default=None, help="Override Modal app name")
430
- @click.option("--dry-run", is_flag=True, help="Print modal deploy command without executing")
431
- @click.option("--modal-cli", default="modal", help="Path to modal CLI executable")
432
- @click.option(
433
- "--modal-mode",
434
- type=click.Choice(["deploy", "serve"], case_sensitive=False),
435
- default="deploy",
436
- show_default=True,
437
- help="Modal operation to run when --runtime=modal.",
438
- )
439
- @click.option(
440
- "--env-file",
441
- multiple=True,
442
- type=click.Path(),
443
- help="Env file to load into the container (can be repeated)",
444
- )
445
- @click.option("--host", default="0.0.0.0", show_default=True, help="Host for --runtime=uvicorn")
446
- @click.option("--port", default=None, type=int, help="Port to serve on when --runtime=uvicorn")
447
- @click.option(
448
- "--reload/--no-reload",
449
- "reload_flag",
450
- default=False,
451
- help="Enable uvicorn auto-reload when --runtime=uvicorn",
452
- )
453
- @click.option(
454
- "--force/--no-force",
455
- "force",
456
- default=False,
457
- help="Kill any process already bound to the selected port (uvicorn runtime)",
458
- )
459
- @click.option(
460
- "--trace",
461
- "trace_dir",
462
- type=click.Path(),
463
- default=None,
464
- help="Enable tracing and write SFT JSONL files when --runtime=uvicorn (default: traces/v3).",
465
- )
466
- @click.option(
467
- "--trace-db",
468
- "trace_db",
469
- type=click.Path(),
470
- default=None,
471
- help="Override local trace DB path when --runtime=uvicorn (default: traces/v3/synth_ai.db).",
472
- )
473
- def deploy_command(
474
- app_id: str | None,
475
- runtime: str,
476
- modal_name: str | None,
477
- dry_run: bool,
478
- modal_cli: str,
479
- modal_mode: str,
480
- env_file: Sequence[str],
481
- host: str,
482
- port: int | None,
483
- reload_flag: bool,
484
- force: bool,
485
- trace_dir: str | None,
486
- trace_db: str | None,
487
- ) -> None:
488
- """Deploy a task app locally or on Modal.
489
-
490
- This command deploys your Synth AI task app either to Modal's cloud platform
491
- or runs it locally with Uvicorn for development. Use --help for detailed usage.
492
- """
493
-
494
- runtime_normalized = runtime.lower()
495
- modal_mode_normalized = modal_mode.lower()
496
- ctx = click.get_current_context()
497
-
498
- def _source(name: str) -> Any:
499
- if ctx is None:
500
- return None
501
- return ctx.get_parameter_source(name)
502
-
503
- def _was_user_provided(name: str) -> bool:
504
- source = _source(name)
505
- if ParameterSource is None:
506
- return bool(source) and str(source) not in {"ParameterSource.DEFAULT", "ParameterSource.NONE"}
507
- none_sentinel = getattr(ParameterSource, "NONE", None)
508
- default_sources = {ParameterSource.DEFAULT}
509
- if none_sentinel is not None:
510
- default_sources.add(none_sentinel)
511
- return bool(source) and source not in default_sources
512
-
513
- try:
514
- if runtime_normalized == "modal":
515
- uvicorn_only_options = [
516
- ("host", "--host"),
517
- ("port", "--port"),
518
- ("reload_flag", "--reload/--no-reload"),
519
- ("force", "--force/--no-force"),
520
- ("trace_dir", "--trace"),
521
- ("trace_db", "--trace-db"),
522
- ]
523
- invalid = [label for param, label in uvicorn_only_options if _was_user_provided(param)]
524
- if invalid:
525
- raise click.ClickException(
526
- f"{', '.join(invalid)} cannot be used with --runtime=modal."
527
- )
528
-
529
- if modal_mode_normalized == "serve" and _was_user_provided("dry_run"):
530
- raise click.ClickException("--dry-run is not supported with --modal-mode=serve.")
531
-
532
- command_choice: Literal["deploy", "serve"] = (
533
- "serve" if modal_mode_normalized == "serve" else "deploy"
534
- )
535
- run_modal_runtime(
536
- app_id,
537
- command=command_choice,
538
- modal_name=modal_name,
539
- dry_run=dry_run,
540
- modal_cli=modal_cli,
541
- env_file=env_file,
542
- )
543
- return
544
-
545
- modal_only_options = [
546
- ("modal_name", "--name"),
547
- ("dry_run", "--dry-run"),
548
- ("modal_cli", "--modal-cli"),
549
- ("modal_mode", "--modal-mode"),
550
- ]
551
- invalid = [label for param, label in modal_only_options if _was_user_provided(param)]
552
- if invalid:
553
- raise click.ClickException(
554
- f"{', '.join(invalid)} cannot be used with --runtime=uvicorn."
555
- )
556
-
557
- run_uvicorn_runtime(app_id, host, port, env_file, reload_flag, force, trace_dir, trace_db)
558
- except DeployCliError as exc:
559
- raise click.ClickException(_format_deploy_error(exc)) from exc
560
- except click.ClickException as err:
561
- converted = _translate_click_exception(err)
562
- if converted:
563
- raise click.ClickException(_format_deploy_error(converted)) from err
564
- raise
565
-
566
-
567
- @click.command("modal-serve")
568
- @click.argument("app_id", type=str, required=False)
569
- @click.option("--modal-cli", default="modal", help="Path to modal CLI executable")
570
- @click.option("--name", "modal_name", default=None, help="Override Modal app name (optional)")
571
- @click.option(
572
- "--env-file",
573
- multiple=True,
574
- type=click.Path(),
575
- help="Env file to load into the container (can be repeated)",
576
- )
577
- def modal_serve_command(
578
- app_id: str | None, modal_cli: str, modal_name: str | None, env_file: Sequence[str]
579
- ) -> None:
580
- click.echo(f"[modal-serve] requested app_id={app_id or '(auto)'} modal_cli={modal_cli}")
581
- try:
582
- run_modal_runtime(
583
- app_id,
584
- command="serve",
585
- modal_name=modal_name,
586
- dry_run=False,
587
- modal_cli=modal_cli,
588
- env_file=env_file,
589
- use_demo_dir=False,
590
- )
591
- except DeployCliError as exc:
592
- raise click.ClickException(_format_deploy_error(exc)) from exc
593
- except click.ClickException as err:
594
- converted = _translate_click_exception(err)
595
- if converted:
596
- raise click.ClickException(_format_deploy_error(converted)) from err
597
- raise
598
- except SystemExit as exc: # bubble up with context (legacy argparse would trigger this)
599
- raise click.ClickException(
600
- f"Legacy CLI intercepted modal-serve (exit {exc.code}). "
601
- "Make sure you're running the Click CLI (synth_ai.cli:cli)."
602
- ) from exc
603
-
604
-
605
- command = deploy_command
606
-
607
-
608
- def get_command() -> click.Command:
609
- return command
610
-
611
-
612
- def register_task_app_commands(task_app_group: click.Group) -> None:
613
- task_app_group.add_command(command)
614
- task_app_group.add_command(modal_serve_command)
@@ -1,72 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
-
5
-
6
- class DeployCliError(RuntimeError):
7
- """Base exception for deploy CLI failures."""
8
-
9
-
10
- @dataclass(slots=True)
11
- class MissingEnvironmentApiKeyError(DeployCliError):
12
- """Raised when ENVIRONMENT_API_KEY is absent and cannot be collected interactively."""
13
-
14
- hint: str | None = None
15
-
16
-
17
- @dataclass(slots=True)
18
- class EnvironmentKeyLoadError(DeployCliError):
19
- """Raised when we fail to persist or reload ENVIRONMENT_API_KEY from disk."""
20
-
21
- path: str | None = None
22
-
23
-
24
- @dataclass(slots=True)
25
- class EnvFileDiscoveryError(DeployCliError):
26
- """Raised when no suitable env file can be found for a task app."""
27
-
28
- attempted: tuple[str, ...] = ()
29
- hint: str | None = None
30
-
31
-
32
- @dataclass(slots=True)
33
- class TaskAppNotFoundError(DeployCliError):
34
- """Raised when the requested task app identifier cannot be resolved."""
35
-
36
- app_id: str | None = None
37
- available: tuple[str, ...] = ()
38
-
39
-
40
- @dataclass(slots=True)
41
- class ModalCliResolutionError(DeployCliError):
42
- """Raised when the Modal CLI executable cannot be located or invoked."""
43
-
44
- cli_path: str | None = None
45
- detail: str | None = None
46
-
47
-
48
- @dataclass(slots=True)
49
- class ModalExecutionError(DeployCliError):
50
- """Raised when a Modal subprocess exits with a non-zero status."""
51
-
52
- command: str
53
- exit_code: int
54
-
55
-
56
- @dataclass(slots=True)
57
- class EnvKeyPreflightError(DeployCliError):
58
- """Raised when uploading or minting ENVIRONMENT_API_KEY to the backend fails."""
59
-
60
- detail: str | None = None
61
-
62
-
63
- __all__ = [
64
- "DeployCliError",
65
- "MissingEnvironmentApiKeyError",
66
- "EnvironmentKeyLoadError",
67
- "EnvFileDiscoveryError",
68
- "TaskAppNotFoundError",
69
- "ModalCliResolutionError",
70
- "ModalExecutionError",
71
- "EnvKeyPreflightError",
72
- ]
@@ -1,11 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import MutableMapping
4
- from typing import Any
5
-
6
- __all__ = ["validate_deploy_options"]
7
-
8
-
9
- def validate_deploy_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
10
- """Validate parameters passed to the deploy CLI command."""
11
- return options
@@ -1,5 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from synth_ai.cli.commands.deploy.core import command, get_command
4
-
5
- __all__ = ["command", "get_command"]