synth-ai 0.4.1__py3-none-any.whl → 0.4.4__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 (153) hide show
  1. synth_ai/__init__.py +13 -13
  2. synth_ai/cli/__init__.py +6 -15
  3. synth_ai/cli/commands/eval/__init__.py +6 -15
  4. synth_ai/cli/commands/eval/config.py +338 -0
  5. synth_ai/cli/commands/eval/core.py +236 -1091
  6. synth_ai/cli/commands/eval/runner.py +704 -0
  7. synth_ai/cli/commands/eval/validation.py +44 -117
  8. synth_ai/cli/commands/filter/core.py +7 -7
  9. synth_ai/cli/commands/filter/validation.py +2 -2
  10. synth_ai/cli/commands/smoke/core.py +7 -17
  11. synth_ai/cli/commands/status/__init__.py +1 -64
  12. synth_ai/cli/commands/status/client.py +50 -151
  13. synth_ai/cli/commands/status/config.py +3 -83
  14. synth_ai/cli/commands/status/errors.py +4 -13
  15. synth_ai/cli/commands/status/subcommands/__init__.py +2 -8
  16. synth_ai/cli/commands/status/subcommands/config.py +13 -0
  17. synth_ai/cli/commands/status/subcommands/files.py +18 -63
  18. synth_ai/cli/commands/status/subcommands/jobs.py +28 -311
  19. synth_ai/cli/commands/status/subcommands/models.py +18 -62
  20. synth_ai/cli/commands/status/subcommands/runs.py +16 -63
  21. synth_ai/cli/commands/status/subcommands/session.py +67 -172
  22. synth_ai/cli/commands/status/subcommands/summary.py +24 -32
  23. synth_ai/cli/commands/status/subcommands/utils.py +41 -0
  24. synth_ai/cli/commands/status/utils.py +16 -107
  25. synth_ai/cli/commands/train/__init__.py +18 -20
  26. synth_ai/cli/commands/train/errors.py +3 -3
  27. synth_ai/cli/commands/train/prompt_learning_validation.py +15 -16
  28. synth_ai/cli/commands/train/validation.py +7 -7
  29. synth_ai/cli/commands/train/{judge_schemas.py → verifier_schemas.py} +33 -34
  30. synth_ai/cli/commands/train/verifier_validation.py +235 -0
  31. synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +0 -1
  32. synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +2 -6
  33. synth_ai/cli/demo_apps/math/config.toml +0 -1
  34. synth_ai/cli/demo_apps/math/modal_task_app.py +2 -6
  35. synth_ai/cli/demo_apps/mipro/task_app.py +25 -47
  36. synth_ai/cli/lib/apps/task_app.py +12 -13
  37. synth_ai/cli/lib/task_app_discovery.py +6 -6
  38. synth_ai/cli/lib/train_cfgs.py +10 -10
  39. synth_ai/cli/task_apps/__init__.py +11 -0
  40. synth_ai/cli/task_apps/commands.py +7 -15
  41. synth_ai/core/env.py +12 -1
  42. synth_ai/core/errors.py +1 -2
  43. synth_ai/core/integrations/cloudflare.py +209 -33
  44. synth_ai/core/tracing_v3/abstractions.py +46 -0
  45. synth_ai/data/__init__.py +3 -30
  46. synth_ai/data/enums.py +1 -20
  47. synth_ai/data/rewards.py +100 -3
  48. synth_ai/products/graph_evolve/__init__.py +1 -2
  49. synth_ai/products/graph_evolve/config.py +16 -16
  50. synth_ai/products/graph_evolve/converters/__init__.py +3 -3
  51. synth_ai/products/graph_evolve/converters/openai_sft.py +7 -7
  52. synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +1 -1
  53. synth_ai/products/graph_gepa/__init__.py +23 -0
  54. synth_ai/products/graph_gepa/converters/__init__.py +19 -0
  55. synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
  56. synth_ai/sdk/__init__.py +45 -35
  57. synth_ai/sdk/api/eval/__init__.py +33 -0
  58. synth_ai/sdk/api/eval/job.py +732 -0
  59. synth_ai/sdk/api/research_agent/__init__.py +276 -66
  60. synth_ai/sdk/api/train/builders.py +181 -0
  61. synth_ai/sdk/api/train/cli.py +41 -33
  62. synth_ai/sdk/api/train/configs/__init__.py +6 -4
  63. synth_ai/sdk/api/train/configs/prompt_learning.py +127 -33
  64. synth_ai/sdk/api/train/configs/rl.py +264 -16
  65. synth_ai/sdk/api/train/configs/sft.py +165 -1
  66. synth_ai/sdk/api/train/graph_validators.py +12 -12
  67. synth_ai/sdk/api/train/graphgen.py +169 -51
  68. synth_ai/sdk/api/train/graphgen_models.py +95 -45
  69. synth_ai/sdk/api/train/local_api.py +10 -0
  70. synth_ai/sdk/api/train/pollers.py +36 -0
  71. synth_ai/sdk/api/train/prompt_learning.py +390 -60
  72. synth_ai/sdk/api/train/rl.py +41 -5
  73. synth_ai/sdk/api/train/sft.py +2 -0
  74. synth_ai/sdk/api/train/task_app.py +20 -0
  75. synth_ai/sdk/api/train/validators.py +17 -17
  76. synth_ai/sdk/graphs/completions.py +239 -33
  77. synth_ai/sdk/{judging/schemas.py → graphs/verifier_schemas.py} +23 -23
  78. synth_ai/sdk/learning/__init__.py +35 -5
  79. synth_ai/sdk/learning/context_learning_client.py +531 -0
  80. synth_ai/sdk/learning/context_learning_types.py +294 -0
  81. synth_ai/sdk/learning/prompt_learning_client.py +1 -1
  82. synth_ai/sdk/learning/prompt_learning_types.py +2 -1
  83. synth_ai/sdk/learning/rl/__init__.py +0 -4
  84. synth_ai/sdk/learning/rl/contracts.py +0 -4
  85. synth_ai/sdk/localapi/__init__.py +40 -0
  86. synth_ai/sdk/localapi/apps/__init__.py +28 -0
  87. synth_ai/sdk/localapi/client.py +10 -0
  88. synth_ai/sdk/localapi/contracts.py +10 -0
  89. synth_ai/sdk/localapi/helpers.py +519 -0
  90. synth_ai/sdk/localapi/rollouts.py +93 -0
  91. synth_ai/sdk/localapi/server.py +29 -0
  92. synth_ai/sdk/localapi/template.py +49 -0
  93. synth_ai/sdk/streaming/handlers.py +6 -6
  94. synth_ai/sdk/streaming/streamer.py +10 -6
  95. synth_ai/sdk/task/__init__.py +18 -5
  96. synth_ai/sdk/task/apps/__init__.py +37 -1
  97. synth_ai/sdk/task/client.py +9 -1
  98. synth_ai/sdk/task/config.py +6 -11
  99. synth_ai/sdk/task/contracts.py +137 -95
  100. synth_ai/sdk/task/in_process.py +32 -22
  101. synth_ai/sdk/task/in_process_runner.py +9 -4
  102. synth_ai/sdk/task/rubrics/__init__.py +2 -3
  103. synth_ai/sdk/task/rubrics/loaders.py +4 -4
  104. synth_ai/sdk/task/rubrics/strict.py +3 -4
  105. synth_ai/sdk/task/server.py +76 -16
  106. synth_ai/sdk/task/trace_correlation_helpers.py +190 -139
  107. synth_ai/sdk/task/validators.py +34 -49
  108. synth_ai/sdk/training/__init__.py +7 -16
  109. synth_ai/sdk/tunnels/__init__.py +118 -0
  110. synth_ai/sdk/tunnels/cleanup.py +83 -0
  111. synth_ai/sdk/tunnels/ports.py +120 -0
  112. synth_ai/sdk/tunnels/tunneled_api.py +363 -0
  113. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/METADATA +71 -4
  114. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/RECORD +118 -128
  115. synth_ai/cli/commands/baseline/__init__.py +0 -12
  116. synth_ai/cli/commands/baseline/core.py +0 -636
  117. synth_ai/cli/commands/baseline/list.py +0 -94
  118. synth_ai/cli/commands/eval/errors.py +0 -81
  119. synth_ai/cli/commands/status/formatters.py +0 -164
  120. synth_ai/cli/commands/status/subcommands/pricing.py +0 -23
  121. synth_ai/cli/commands/status/subcommands/usage.py +0 -203
  122. synth_ai/cli/commands/train/judge_validation.py +0 -305
  123. synth_ai/cli/usage.py +0 -159
  124. synth_ai/data/specs.py +0 -36
  125. synth_ai/sdk/api/research_agent/cli.py +0 -428
  126. synth_ai/sdk/api/research_agent/config.py +0 -357
  127. synth_ai/sdk/api/research_agent/job.py +0 -717
  128. synth_ai/sdk/baseline/__init__.py +0 -25
  129. synth_ai/sdk/baseline/config.py +0 -209
  130. synth_ai/sdk/baseline/discovery.py +0 -216
  131. synth_ai/sdk/baseline/execution.py +0 -154
  132. synth_ai/sdk/judging/__init__.py +0 -15
  133. synth_ai/sdk/judging/base.py +0 -24
  134. synth_ai/sdk/judging/client.py +0 -191
  135. synth_ai/sdk/judging/types.py +0 -42
  136. synth_ai/sdk/research_agent/__init__.py +0 -34
  137. synth_ai/sdk/research_agent/container_builder.py +0 -328
  138. synth_ai/sdk/research_agent/container_spec.py +0 -198
  139. synth_ai/sdk/research_agent/defaults.py +0 -34
  140. synth_ai/sdk/research_agent/results_collector.py +0 -69
  141. synth_ai/sdk/specs/__init__.py +0 -46
  142. synth_ai/sdk/specs/dataclasses.py +0 -149
  143. synth_ai/sdk/specs/loader.py +0 -144
  144. synth_ai/sdk/specs/serializer.py +0 -199
  145. synth_ai/sdk/specs/validation.py +0 -250
  146. synth_ai/sdk/tracing/__init__.py +0 -39
  147. synth_ai/sdk/usage/__init__.py +0 -37
  148. synth_ai/sdk/usage/client.py +0 -171
  149. synth_ai/sdk/usage/models.py +0 -261
  150. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/WHEEL +0 -0
  151. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/entry_points.txt +0 -0
  152. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/licenses/LICENSE +0 -0
  153. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/top_level.txt +0 -0
@@ -409,40 +409,51 @@ async def _verify_preconfigured_url_ready(
409
409
 
410
410
 
411
411
  class InProcessTaskApp:
412
- """
413
- Context manager for running task apps in-process with automatic tunneling.
414
-
412
+ """Context manager for running Local APIs in-process with automatic tunneling.
413
+
415
414
  This class simplifies local development and demos by:
416
- 1. Starting a task app server in a background thread
415
+ 1. Starting a Local API server in a background thread
417
416
  2. Opening a tunnel automatically (Cloudflare by default, or use preconfigured URL)
418
- 3. Providing the tunnel URL for GEPA/MIPRO jobs
417
+ 3. Providing the tunnel URL for GEPA/MIPRO/RL jobs
419
418
  4. Cleaning up everything on exit
420
-
419
+
420
+ (Alias: also known as "task app" in older documentation)
421
+
421
422
  Supports multiple input methods:
422
423
  - FastAPI app instance (most direct)
423
424
  - TaskAppConfig object
424
425
  - Config factory function (Callable[[], TaskAppConfig])
425
- - Task app file path (fallback for compatibility)
426
-
426
+ - Local API file path (fallback for compatibility)
427
+
427
428
  Tunnel modes:
428
429
  - "quick": Cloudflare quick tunnel (default for local dev)
429
430
  - "named": Cloudflare named/managed tunnel
430
431
  - "local": No tunnel, use localhost URL directly
431
432
  - "preconfigured": Use externally-provided URL (set via preconfigured_url param or
432
433
  SYNTH_TASK_APP_URL env var). Useful for ngrok or other external tunnel providers.
433
-
434
+
435
+ Attributes:
436
+ url: The public URL of the running Local API (tunnel URL or localhost).
437
+ Available after entering the context manager.
438
+ local_url: The local URL (http://host:port) where the server is running.
439
+ port: The actual port the server is bound to (may differ from requested
440
+ port if auto_find_port=True).
441
+ host: The host the server is bound to.
442
+ tunnel_mode: The tunnel mode being used.
443
+ is_running: Whether the server is currently running.
444
+
434
445
  Example:
435
446
  ```python
436
447
  from synth_ai.sdk.task.in_process import InProcessTaskApp
437
448
  from heartdisease_task_app import build_config
438
-
449
+
439
450
  # Default: use Cloudflare quick tunnel
440
451
  async with InProcessTaskApp(
441
452
  config_factory=build_config,
442
453
  port=8114,
443
454
  ) as task_app:
444
- print(f"Task app running at: {task_app.url}")
445
-
455
+ print(f"Local API running at: {task_app.url}")
456
+
446
457
  # Use preconfigured URL (e.g., from ngrok, localtunnel, etc.)
447
458
  async with InProcessTaskApp(
448
459
  config_factory=build_config,
@@ -450,7 +461,7 @@ class InProcessTaskApp:
450
461
  tunnel_mode="preconfigured",
451
462
  preconfigured_url="https://abc123.ngrok.io",
452
463
  ) as task_app:
453
- print(f"Task app running at: {task_app.url}")
464
+ print(f"Local API running at: {task_app.url}")
454
465
  ```
455
466
  """
456
467
 
@@ -475,14 +486,13 @@ class InProcessTaskApp:
475
486
  on_start: Optional[Callable[[InProcessTaskApp], None]] = None,
476
487
  on_stop: Optional[Callable[[InProcessTaskApp], None]] = None,
477
488
  ):
478
- """
479
- Initialize in-process task app.
480
-
489
+ """Initialize in-process Local API.
490
+
481
491
  Args:
482
492
  app: FastAPI app instance (most direct)
483
493
  config: TaskAppConfig object
484
494
  config_factory: Callable that returns TaskAppConfig
485
- task_app_path: Path to task app .py file (fallback)
495
+ task_app_path: Path to Local API .py file (fallback, alias: task app)
486
496
  port: Local port to run server on
487
497
  host: Host to bind to (default: 127.0.0.1, use 0.0.0.0 for external access)
488
498
  tunnel_mode: Tunnel mode - "quick", "named", "local", or "preconfigured"
@@ -594,7 +604,7 @@ class InProcessTaskApp:
594
604
  else:
595
605
  self._prefetched_tunnel_config = None
596
606
 
597
- logger.info(f"Starting in-process task app on {self.host}:{self.port}")
607
+ logger.debug(f"Starting in-process task app on {self.host}:{self.port}")
598
608
 
599
609
  # For named tunnels, the port is baked into the tunnel config - we MUST use it
600
610
  tunnel_config = getattr(self, "_prefetched_tunnel_config", None) or {}
@@ -624,7 +634,7 @@ class InProcessTaskApp:
624
634
  f"Port {self.port} is in use, attempting to find available port..."
625
635
  )
626
636
  self.port = _find_available_port(self.host, self.port)
627
- logger.info(f"Using port {self.port} instead")
637
+ logger.debug(f"Using port {self.port} instead")
628
638
  else:
629
639
  # Try to kill process on port
630
640
  logger.warning(
@@ -683,7 +693,7 @@ class InProcessTaskApp:
683
693
  f"Task app at {self._task_app_path} must expose either:\n"
684
694
  f" - An ASGI app via `app = FastAPI(...)` or factory function\n"
685
695
  f" - A `build_config()` function that returns TaskAppConfig\n"
686
- f" - Be registered with register_task_app()"
696
+ f" - Be registered with register_local_api() or register_task_app()"
687
697
  ) from None
688
698
 
689
699
  # 2. Start uvicorn in background thread
@@ -719,7 +729,7 @@ class InProcessTaskApp:
719
729
  await wait_for_health_check(
720
730
  self.host, self.port, api_key, timeout=self.health_check_timeout
721
731
  )
722
- logger.info(f"Health check passed for {self.host}:{self.port}")
732
+ logger.debug(f"Health check passed for {self.host}:{self.port}")
723
733
 
724
734
  # 4. Determine tunnel mode (env var can override)
725
735
  mode = os.getenv("SYNTH_TUNNEL_MODE", self.tunnel_mode)
@@ -774,7 +784,7 @@ class InProcessTaskApp:
774
784
  # Local mode: skip tunnel, use localhost
775
785
  self.url = f"http://{self.host}:{self.port}"
776
786
  self._tunnel_proc = None
777
- logger.info(f"Using local mode: {self.url}")
787
+ logger.debug(f"Using local mode: {self.url}")
778
788
  elif mode == "named":
779
789
  # Named tunnel mode: fully automatic managed tunnel
780
790
  # 1. Check for existing tunnel
@@ -31,7 +31,7 @@ from synth_ai.core.env import get_backend_from_env
31
31
  from synth_ai.core.telemetry import log_info
32
32
  from synth_ai.sdk.api.train.prompt_learning import PromptLearningJob
33
33
  from synth_ai.sdk.api.train.rl import RLJob
34
- from synth_ai.sdk.api.train.task_app import TaskAppHealth, check_task_app_health
34
+ from synth_ai.sdk.api.train.local_api import LocalAPIHealth, check_local_api_health
35
35
  from synth_ai.sdk.api.train.utils import ensure_api_base
36
36
  from synth_ai.sdk.task.in_process import InProcessTaskApp
37
37
 
@@ -52,7 +52,7 @@ class InProcessJobResult:
52
52
  status: Dict[str, Any]
53
53
  task_app_url: str
54
54
  backend_url: str
55
- task_app_health: TaskAppHealth | None = None
55
+ task_app_health: LocalAPIHealth | None = None
56
56
 
57
57
 
58
58
  def _normalize_base_url(url: str) -> str:
@@ -234,9 +234,14 @@ async def run_in_process_job(
234
234
  should_skip_health_check = skip_tunnel_verification or dns_verified_by_backend
235
235
  if should_skip_health_check:
236
236
  reason = "tunnel verification disabled" if skip_tunnel_verification else "backend verified DNS"
237
- health = TaskAppHealth(ok=True, health_status=200, task_info_status=200, detail=f"Skipped ({reason})")
237
+ health = LocalAPIHealth(
238
+ ok=True,
239
+ health_status=200,
240
+ task_info_status=200,
241
+ detail=f"Skipped ({reason})",
242
+ )
238
243
  else:
239
- health = check_task_app_health(task_url, resolved_task_app_key)
244
+ health = check_local_api_health(task_url, resolved_task_app_key)
240
245
  if not health.ok:
241
246
  raise RuntimeError(f"Task app health check failed for {task_url}: {health.detail}")
242
247
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  This module provides:
4
4
  - Flexible rubric models (Criterion, Rubric) for general task app use
5
- - Strict validators (StrictCriterion, StrictRubric) for step-wise judges
5
+ - Strict validators (StrictCriterion, StrictRubric) for step-wise verifiers
6
6
  - Loading utilities supporting JSON, YAML, and HTTP sources
7
7
  - Blending utilities for composing rubrics
8
8
  - Scoring utilities for events and outcomes
@@ -16,7 +16,7 @@ from .models import Criterion, Rubric
16
16
  # Scoring
17
17
  from .scoring import score_events_against_rubric, score_outcome_against_rubric
18
18
 
19
- # Strict validators (for judge configs)
19
+ # Strict validators (for verifier configs)
20
20
  from .strict import (
21
21
  StrictCriterion,
22
22
  StrictRubric,
@@ -52,4 +52,3 @@ RubricSpec = StrictRubric
52
52
 
53
53
 
54
54
 
55
-
@@ -62,7 +62,7 @@ def load_rubric(source: str | dict[str, Any] | Rubric | None) -> Rubric | None:
62
62
  Parsed Rubric instance or None if source is None
63
63
 
64
64
  Raises:
65
- ValueError: If the rubric format is incorrect (e.g., backend judge format)
65
+ ValueError: If the rubric format is incorrect (e.g., backend verifier format)
66
66
  ValidationError: If the rubric fails schema validation
67
67
  """
68
68
  if source is None:
@@ -77,7 +77,7 @@ def load_rubric(source: str | dict[str, Any] | Rubric | None) -> Rubric | None:
77
77
  text, suffix = _load_text(str(source))
78
78
  data = _parse_structured(text, suffix)
79
79
 
80
- # Check if this looks like a backend judge rubric (wrong format)
80
+ # Check if this looks like a backend verifier rubric (wrong format)
81
81
  if (
82
82
  isinstance(data, dict)
83
83
  and "event" in data
@@ -88,9 +88,9 @@ def load_rubric(source: str | dict[str, Any] | Rubric | None) -> Rubric | None:
88
88
  ):
89
89
  source_hint = f" ({source})" if isinstance(source, str) else ""
90
90
  raise ValueError(
91
- f"Rubric appears to be in backend judge format (has 'event'/'outcome' keys){source_hint}. "
91
+ f"Rubric appears to be in backend verifier format (has 'event'/'outcome' keys){source_hint}. "
92
92
  f"Task apps require rubrics with 'version', 'goal_text', and 'criteria' fields. "
93
- f"Backend judge rubrics should be named '*_backend_judge.json' and loaded by judge functions."
93
+ f"Backend verifier rubrics should be named '*_backend_verifier.json' and loaded by verifier functions."
94
94
  )
95
95
 
96
96
  return Rubric.model_validate(data)
@@ -1,11 +1,11 @@
1
- """Strict rubric validators for step-wise judges.
1
+ """Strict rubric validators for step-wise verifiers.
2
2
 
3
3
  These validators enforce stricter constraints than the general-purpose rubrics:
4
4
  - Weights must be ≤ 1.0 and sum to exactly 1.0
5
5
  - Only weighted_sum aggregation is allowed
6
6
  - All required fields must be non-empty
7
7
 
8
- Used primarily for validation in judge configurations.
8
+ Used primarily for validation in verifier configurations.
9
9
  """
10
10
 
11
11
  from __future__ import annotations
@@ -53,7 +53,7 @@ class StrictCriterion(pydantic.BaseModel):
53
53
 
54
54
 
55
55
  class StrictRubric(pydantic.BaseModel):
56
- """Strict rubric definition for step-wise judges.
56
+ """Strict rubric definition for step-wise verifiers.
57
57
 
58
58
  Enforces:
59
59
  - Weights must sum to 1.0
@@ -146,4 +146,3 @@ def validate_rubric_files(paths: Iterable[Path]) -> list[StrictRubric]:
146
146
  for path in paths:
147
147
  validated.append(validate_rubric_file(path))
148
148
  return validated
149
-
@@ -1,10 +1,15 @@
1
- """FastAPI scaffolding for Task Apps (local dev + deployment)."""
1
+ """FastAPI scaffolding for Task Apps (local dev + deployment).
2
+
3
+ Prefer synth_ai.sdk.localapi.server moving forward. This module remains for
4
+ backward compatibility during the naming transition.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
9
  import asyncio
6
10
  import inspect
7
11
  import os
12
+ import threading
8
13
  from collections.abc import Awaitable, Callable, Iterable, Mapping, MutableMapping, Sequence
9
14
  from contextlib import asynccontextmanager
10
15
  from dataclasses import dataclass, field
@@ -66,7 +71,7 @@ class TaskAppConfig:
66
71
 
67
72
  A TaskAppConfig defines all the components needed to create a task app:
68
73
  - Task information (base_task_info)
69
- - Task set description (describe_taskset)
74
+ - Task set description (provide_taskset_description)
70
75
  - Task instance provider (provide_task_instances)
71
76
  - Rollout executor (rollout)
72
77
  - Optional rubrics, datasets, proxy config, etc.
@@ -76,19 +81,18 @@ class TaskAppConfig:
76
81
 
77
82
  Example:
78
83
  >>> from synth_ai.sdk.task.server import TaskAppConfig, create_task_app
79
- >>> from synth_ai.sdk.task.contracts import TaskInfo
80
- >>>
84
+ >>>
81
85
  >>> def build_config() -> TaskAppConfig:
82
86
  ... return TaskAppConfig(
83
87
  ... app_id="my_task",
84
88
  ... name="My Task App",
85
89
  ... description="A simple task app",
86
- ... base_task_info=TaskInfo(...),
87
- ... describe_taskset=lambda: {"splits": ["train", "val"]},
90
+ ... provide_taskset_description=lambda: {"splits": ["train", "val"]},
88
91
  ... provide_task_instances=lambda seeds: [...],
89
92
  ... rollout=lambda req, r: {...},
93
+ ... # base_task_info is optional - auto-derived from app_id/name
90
94
  ... )
91
- >>>
95
+ >>>
92
96
  >>> app = create_task_app(build_config())
93
97
  >>> # app is a FastAPI instance ready to run
94
98
 
@@ -96,8 +100,8 @@ class TaskAppConfig:
96
100
  app_id: Unique identifier for this task app
97
101
  name: Human-readable name
98
102
  description: Description of what this task app does
99
- base_task_info: Base TaskInfo that all instances inherit from
100
- describe_taskset: Function that returns taskset metadata
103
+ base_task_info: Base TaskInfo (optional - auto-derived from app_id/name if not provided)
104
+ provide_taskset_description: Function that returns taskset metadata
101
105
  provide_task_instances: Function that yields TaskInfo instances for given seeds
102
106
  rollout: Function that executes a rollout request and returns response
103
107
  dataset_registry: Optional registry for task datasets
@@ -116,10 +120,10 @@ class TaskAppConfig:
116
120
  app_id: str
117
121
  name: str
118
122
  description: str
119
- base_task_info: TaskInfo
120
- describe_taskset: TasksetDescriptor
123
+ provide_taskset_description: TasksetDescriptor
121
124
  provide_task_instances: InstanceProvider
122
125
  rollout: RolloutExecutor
126
+ base_task_info: TaskInfo | None = None # Auto-derived from app_id/name if not provided
123
127
  dataset_registry: TaskDatasetRegistry | None = None
124
128
  rubrics: RubricBundle | None = field(default_factory=RubricBundle)
125
129
  proxy: ProxyConfig | None = None
@@ -139,10 +143,10 @@ class TaskAppConfig:
139
143
  app_id=self.app_id,
140
144
  name=self.name,
141
145
  description=self.description,
142
- base_task_info=self.base_task_info,
143
- describe_taskset=self.describe_taskset,
146
+ provide_taskset_description=self.provide_taskset_description,
144
147
  provide_task_instances=self.provide_task_instances,
145
148
  rollout=self.rollout,
149
+ base_task_info=self.base_task_info, # May be None - auto-derived in create_task_app
146
150
  dataset_registry=self.dataset_registry,
147
151
  rubrics=self.rubrics or RubricBundle(),
148
152
  proxy=self.proxy,
@@ -157,6 +161,10 @@ class TaskAppConfig:
157
161
  )
158
162
 
159
163
 
164
+ class LocalAPIConfig(TaskAppConfig):
165
+ """Alias for TaskAppConfig with LocalAPI naming."""
166
+
167
+
160
168
  def _maybe_await(result: Any) -> Awaitable[Any]:
161
169
  if inspect.isawaitable(result):
162
170
  return asyncio.ensure_future(result)
@@ -299,10 +307,10 @@ def create_task_app(config: TaskAppConfig) -> FastAPI:
299
307
  ... app_id="my_task",
300
308
  ... name="My Task",
301
309
  ... description="A task app",
302
- ... base_task_info=TaskInfo(...),
303
- ... describe_taskset=lambda: {"splits": ["train"]},
310
+ ... provide_taskset_description=lambda: {"splits": ["train"]},
304
311
  ... provide_task_instances=lambda seeds: [...],
305
312
  ... rollout=lambda req, r: {...},
313
+ ... # base_task_info is optional - auto-derived from app_id/name
306
314
  ... )
307
315
  >>>
308
316
  >>> app = create_task_app(build_config())
@@ -312,6 +320,16 @@ def create_task_app(config: TaskAppConfig) -> FastAPI:
312
320
  log_info("create_task_app invoked", ctx=ctx)
313
321
  cfg = config.clone()
314
322
  cfg.rubrics = cfg.rubrics or RubricBundle()
323
+
324
+ # Auto-derive base_task_info from app_id/name if not provided
325
+ if cfg.base_task_info is None:
326
+ cfg.base_task_info = TaskInfo(
327
+ task={"id": cfg.app_id, "name": cfg.name},
328
+ dataset={"id": cfg.app_id},
329
+ inference={},
330
+ limits={},
331
+ )
332
+
315
333
  app = FastAPI(title=cfg.name, description=cfg.description)
316
334
 
317
335
  for key, value in cfg.app_state.items():
@@ -431,7 +449,7 @@ def create_task_app(config: TaskAppConfig) -> FastAPI:
431
449
  all_seeds.extend(_normalise_seeds(seeds))
432
450
 
433
451
  if not all_seeds:
434
- descriptor_result = await _maybe_await(cfg.describe_taskset())
452
+ descriptor_result = await _maybe_await(cfg.provide_taskset_description())
435
453
  return to_jsonable({"taskset": descriptor_result})
436
454
 
437
455
  instances = await _maybe_await(cfg.provide_task_instances(all_seeds))
@@ -578,3 +596,45 @@ def run_task_app(
578
596
  remove_service_record(port)
579
597
  except Exception:
580
598
  pass
599
+
600
+
601
+ def run_server_background(
602
+ app: Any,
603
+ port: int,
604
+ host: str = "0.0.0.0",
605
+ ) -> threading.Thread:
606
+ """Start uvicorn server in a background daemon thread.
607
+
608
+ For manual control over task app lifecycle. If you want automatic
609
+ tunnel management, use InProcessTaskApp instead.
610
+
611
+ Args:
612
+ app: ASGI/FastAPI application
613
+ port: Port to bind
614
+ host: Host to bind (default 0.0.0.0 for tunnel access)
615
+
616
+ Returns:
617
+ Daemon thread running the server (stops when main process exits)
618
+
619
+ Example:
620
+ from synth_ai.sdk.task import run_server_background
621
+ from synth_ai.sdk.tunnels import wait_for_health_check
622
+
623
+ thread = run_server_background(my_app, port=8001)
624
+ await wait_for_health_check("localhost", 8001, api_key)
625
+ """
626
+ import asyncio
627
+ import threading
628
+
629
+ import uvicorn
630
+
631
+ def serve() -> None:
632
+ config = uvicorn.Config(app, host=host, port=port, log_level="warning")
633
+ server = uvicorn.Server(config)
634
+ loop = asyncio.new_event_loop()
635
+ asyncio.set_event_loop(loop)
636
+ loop.run_until_complete(server.serve())
637
+
638
+ thread = threading.Thread(target=serve, daemon=True, name=f"uvicorn-{port}")
639
+ thread.start()
640
+ return thread