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
@@ -1,198 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import io
4
- import textwrap
5
- import time
6
- from dataclasses import dataclass, field
7
- from importlib import import_module
8
- from pathlib import Path
9
- from typing import Dict, Iterable, List, Optional
10
-
11
- from synth_ai.sdk.research_agent.defaults import (
12
- DEFAULT_BACKEND,
13
- DEFAULT_BASE_IMAGE,
14
- DEFAULT_INSTRUCTIONS,
15
- DEFAULT_PACKAGES,
16
- DEFAULT_PYTHON_VERSION,
17
- DEFAULT_RESULT_PATTERNS,
18
- DEFAULT_SYNTH_PIP_SPEC,
19
- )
20
-
21
-
22
- def _load_box_bootstrap() -> str:
23
- """Load the canonical box_bootstrap.sh content from regenerate_bootstrap."""
24
- try:
25
- mod = import_module("scripts.regenerate_bootstrap")
26
- content = getattr(mod, "bootstrap_content", None)
27
- if content:
28
- return content
29
- except Exception:
30
- pass
31
- return "#!/bin/bash\nset -euo pipefail\necho 'box_bootstrap.sh missing; ensure regenerate_bootstrap.py is available.'\nexit 1\n"
32
-
33
-
34
- @dataclass
35
- class ContainerSpec:
36
- """Declarative container configuration for running the research agent."""
37
-
38
- repo_url: str
39
- repo_branch: str = "main"
40
- repo_commit: Optional[str] = None
41
-
42
- agent_instructions: str = DEFAULT_INSTRUCTIONS
43
- base_image: str = DEFAULT_BASE_IMAGE
44
- python_version: str = DEFAULT_PYTHON_VERSION
45
- apt_packages: List[str] = field(default_factory=lambda: list(DEFAULT_PACKAGES))
46
-
47
- env_vars: Dict[str, str] = field(default_factory=dict)
48
- secrets: Dict[str, str] = field(default_factory=dict)
49
- files: Dict[str, str | bytes] = field(default_factory=dict)
50
- preflight_script: Optional[str] = None
51
- postflight_script: Optional[str] = None
52
-
53
- artifacts_dir: Path = Path("/app/artifacts")
54
- result_patterns: List[str] = field(default_factory=lambda: list(DEFAULT_RESULT_PATTERNS))
55
- workdir: Path = Path("/app/repo")
56
-
57
- backend: str = DEFAULT_BACKEND
58
- overlay_dir: Optional[Path] = None
59
- bootstrap_content: str = field(default_factory=_load_box_bootstrap)
60
- synth_pip_spec: str = DEFAULT_SYNTH_PIP_SPEC
61
-
62
- def validate(self) -> None:
63
- """Lightweight validation before provisioning."""
64
- if not self.repo_url:
65
- raise ValueError("repo_url is required")
66
- if not self.agent_instructions:
67
- raise ValueError("agent_instructions is required")
68
- if not self.base_image:
69
- raise ValueError("base_image is required")
70
- if not self.artifacts_dir.is_absolute():
71
- raise ValueError("artifacts_dir must be absolute")
72
-
73
- @property
74
- def build_args(self) -> Dict[str, str]:
75
- """Build args passed to Docker/Modal image creation."""
76
- return {
77
- "GIT_URL": self.repo_url,
78
- "GIT_BRANCH": self.repo_branch,
79
- "GIT_COMMIT": self.repo_commit or "",
80
- "PYTHON_VERSION": self.python_version,
81
- "SYNTH_PIP_SPEC": self.synth_pip_spec or "",
82
- }
83
-
84
- def to_dockerfile(self) -> str:
85
- """Render a Dockerfile that mirrors the existing OneShot bootstrap."""
86
- package_line = " ".join(sorted(set(self.apt_packages)))
87
- return textwrap.dedent(
88
- f"""
89
- FROM {self.base_image}
90
-
91
- ARG GIT_URL
92
- ARG GIT_BRANCH
93
- ARG GIT_COMMIT
94
- ARG PYTHON_VERSION="{self.python_version}"
95
-
96
- ENV DEBIAN_FRONTEND=noninteractive
97
- ENV PIP_DISABLE_PIP_VERSION_CHECK=1
98
- ENV PIP_NO_PYTHON_VERSION_WARNING=1
99
- ENV PIP_BREAK_SYSTEM_PACKAGES=1
100
- ENV PYTHONWARNINGS=ignore
101
-
102
- RUN apt-get update && \\
103
- apt-get install -y --no-install-recommends {package_line} && \\
104
- ln -sf /usr/bin/python3 /usr/bin/python && \\
105
- mkdir -p {self.artifacts_dir} /app/overlay_files && \\
106
- apt-get clean
107
-
108
- # Install uv for fast Python installs
109
- RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \\
110
- ln -sf /root/.local/bin/uv /usr/local/bin/uv || true
111
-
112
- WORKDIR /app
113
- RUN git clone --branch "$GIT_BRANCH" "$GIT_URL" repo && \\
114
- cd repo && if [ -n "$GIT_COMMIT" ]; then git checkout "$GIT_COMMIT"; fi && \\
115
- python3 -m venv /app/repo/.venv && \\
116
- . /app/repo/.venv/bin/activate && \\
117
- pip install --no-cache-dir --upgrade pip && \\
118
- if [ -n "$SYNTH_PIP_SPEC" ]; then \\
119
- pip install --no-cache-dir "$SYNTH_PIP_SPEC"; \\
120
- else \\
121
- pip install --no-cache-dir -e .; \\
122
- fi
123
- ENV VIRTUAL_ENV="/app/repo/.venv"
124
- ENV PATH="/app/repo/.venv/bin:${{PATH}}"
125
-
126
- COPY overlay_files/ /app/
127
-
128
- WORKDIR {self.workdir}
129
- """
130
- ).strip() + "\n"
131
-
132
- def rendered_overlay_files(self) -> Dict[str, bytes]:
133
- """Overlay files placed into the build context / runtime container."""
134
- files: Dict[str, bytes] = {}
135
-
136
- if self.overlay_dir and self.overlay_dir.exists():
137
- for path in self.overlay_dir.rglob("*"):
138
- if path.is_dir():
139
- continue
140
- rel_path = path.relative_to(self.overlay_dir)
141
- files.setdefault(str(rel_path), path.read_bytes())
142
-
143
- if self.agent_instructions:
144
- files["LM_INSTRUCTIONS.md"] = self.agent_instructions.encode()
145
-
146
- if self.preflight_script:
147
- files["overlay_hidden_pre/preflight.sh"] = self.preflight_script.encode()
148
- files["pre_flight.sh"] = self.preflight_script.encode()
149
- if self.postflight_script:
150
- files["overlay_hidden_post/postflight.sh"] = self.postflight_script.encode()
151
-
152
- for relative_path, content in self.files.items():
153
- data = content.encode() if isinstance(content, str) else content
154
- files[str(relative_path)] = data
155
-
156
- files.setdefault("box_bootstrap.sh", self.bootstrap_content.encode())
157
-
158
- return files
159
-
160
- def build_context(self) -> bytes:
161
- """Create a tar build context for Docker builds."""
162
- import tarfile
163
-
164
- buf = io.BytesIO()
165
- with tarfile.open(fileobj=buf, mode="w") as tar:
166
- dockerfile_bytes = self.to_dockerfile().encode()
167
- dockerfile_info = tarfile.TarInfo("Dockerfile")
168
- dockerfile_info.size = len(dockerfile_bytes)
169
- dockerfile_info.mtime = int(time.time())
170
- tar.addfile(dockerfile_info, io.BytesIO(dockerfile_bytes))
171
-
172
- for rel_path, content in self.rendered_overlay_files().items():
173
- overlay_path = Path("overlay_files") / rel_path
174
- executable = overlay_path.name.endswith(".sh")
175
- info = tarfile.TarInfo(str(overlay_path))
176
- info.size = len(content)
177
- info.mtime = int(time.time())
178
- if executable:
179
- info.mode = 0o755
180
- tar.addfile(info, io.BytesIO(content))
181
-
182
- for rel_path, content in self.files.items():
183
- if str(rel_path).startswith("/"):
184
- data = content.encode() if isinstance(content, str) else content
185
- target = Path(str(rel_path).lstrip("/"))
186
- info = tarfile.TarInfo(str(target))
187
- info.size = len(data)
188
- info.mtime = int(time.time())
189
- if target.name.endswith(".sh"):
190
- info.mode = 0o755
191
- tar.addfile(info, io.BytesIO(data))
192
-
193
- buf.seek(0)
194
- return buf.read()
195
-
196
- def result_matchers(self) -> Iterable[str]:
197
- """Expose patterns for callers that only need read-only access."""
198
- return tuple(self.result_patterns)
@@ -1,34 +0,0 @@
1
- """Defaults and shared constants for the research agent (library side)."""
2
-
3
- from __future__ import annotations
4
-
5
- DEFAULT_INSTRUCTIONS = "Run baseline, then optimize prompt with configured optimizer."
6
- DEFAULT_MODEL = "gpt-4o-mini"
7
- DEFAULT_REASONING_EFFORT = "medium"
8
-
9
- DEFAULT_BACKEND = "docker"
10
- DEFAULT_BASE_IMAGE = "ubuntu:24.04"
11
- DEFAULT_PYTHON_VERSION = "3.11"
12
- DEFAULT_PACKAGES = (
13
- "git",
14
- "curl",
15
- "build-essential",
16
- "cmake",
17
- "ninja-build",
18
- "pkg-config",
19
- "python3",
20
- "python3-venv",
21
- "python3-pip",
22
- "ca-certificates",
23
- "jq",
24
- )
25
-
26
- DEFAULT_RESULT_PATTERNS = (
27
- "*.json",
28
- "*.log",
29
- "*.md",
30
- "*.toml",
31
- "diff.patch",
32
- )
33
-
34
- DEFAULT_SYNTH_PIP_SPEC = "synth-ai==0.2.26.dev1"
@@ -1,69 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from datetime import UTC, datetime
5
- from pathlib import Path
6
- from typing import Dict, Iterable
7
-
8
- from synth_ai.sdk.research_agent.container_builder import ContainerBackend
9
-
10
-
11
- class ResultsCollector:
12
- """Collect and persist artifacts from a container run."""
13
-
14
- def __init__(self, output_dir: Path):
15
- self.output_dir = output_dir
16
- self.output_dir.mkdir(parents=True, exist_ok=True)
17
-
18
- async def collect_from_container(
19
- self,
20
- backend: ContainerBackend,
21
- container_id: str,
22
- patterns: Iterable[str],
23
- ) -> Dict[str, Path]:
24
- """Fetch artifacts via the backend and write them to disk."""
25
- artifacts = await backend.collect_artifacts(container_id, patterns)
26
- saved: Dict[str, Path] = {}
27
- for name, content in artifacts.items():
28
- path = self.output_dir / Path(name).name
29
- path.write_bytes(content)
30
- saved[name] = path
31
- return saved
32
-
33
- def create_manifest(self, saved_files: Dict[str, Path]) -> Path:
34
- """Create a manifest describing collected artifacts."""
35
- manifest = {
36
- "files": [
37
- {
38
- "name": name,
39
- "path": str(path.relative_to(self.output_dir)),
40
- "size_bytes": path.stat().st_size,
41
- "type": self._classify_file(name),
42
- }
43
- for name, path in saved_files.items()
44
- ],
45
- "collected_at": datetime.now(UTC).isoformat(),
46
- "total_size_bytes": sum(path.stat().st_size for path in saved_files.values()),
47
- }
48
- manifest_path = self.output_dir / "manifest.json"
49
- manifest_path.write_text(json.dumps(manifest, indent=2))
50
- return manifest_path
51
-
52
- def _classify_file(self, filename: str) -> str:
53
- """Coarse file typing to make downstream filtering simpler."""
54
- lowered = filename.lower()
55
- if lowered.endswith(".json"):
56
- if "result" in lowered or "metric" in lowered:
57
- return "metrics"
58
- if "config" in lowered:
59
- return "config"
60
- return "data"
61
- if lowered.endswith(".log"):
62
- return "logs"
63
- if lowered.endswith(".md"):
64
- return "documentation"
65
- if lowered.endswith((".toml", ".yaml", ".yml")):
66
- return "config"
67
- if lowered == "diff.patch":
68
- return "code_changes"
69
- return "other"
@@ -1,46 +0,0 @@
1
- """System specification abstractions for synth-ai.
2
-
3
- Provides hierarchical specification format inspired by Sean Grove's "spec as code" pattern.
4
- Specs encode intent, policies, and rules as versioned, testable artifacts.
5
- """
6
-
7
- from synth_ai.sdk.specs.dataclasses import (
8
- Constraints,
9
- Example,
10
- GlossaryItem,
11
- Interfaces,
12
- Metadata,
13
- Principle,
14
- Rule,
15
- Spec,
16
- TestCase,
17
- )
18
- from synth_ai.sdk.specs.loader import load_spec_from_dict, load_spec_from_file
19
- from synth_ai.sdk.specs.serializer import spec_to_compact_context, spec_to_prompt_context
20
- from synth_ai.sdk.specs.validation import (
21
- SpecValidationError,
22
- SpecValidator,
23
- validate_spec_dict,
24
- validate_spec_file,
25
- )
26
-
27
- __all__ = [
28
- "Spec",
29
- "Metadata",
30
- "Principle",
31
- "Rule",
32
- "Constraints",
33
- "Example",
34
- "TestCase",
35
- "Interfaces",
36
- "GlossaryItem",
37
- "load_spec_from_file",
38
- "load_spec_from_dict",
39
- "spec_to_prompt_context",
40
- "spec_to_compact_context",
41
- "SpecValidator",
42
- "SpecValidationError",
43
- "validate_spec_dict",
44
- "validate_spec_file",
45
- ]
46
-
@@ -1,149 +0,0 @@
1
- """Dataclasses for hierarchical system specifications.
2
-
3
- Based on Sean Grove's "spec as code" pattern from AI Engineer World's Fair.
4
- Specs are the source of truth that encode intent, policies, and rules.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from dataclasses import dataclass, field
10
- from typing import Any, Dict, List, Optional
11
-
12
-
13
- @dataclass
14
- class Principle:
15
- """A high-level principle or value that guides behavior."""
16
-
17
- id: str
18
- text: str
19
- rationale: Optional[str] = None
20
-
21
-
22
- @dataclass
23
- class Example:
24
- """An example demonstrating good or bad behavior."""
25
-
26
- kind: str # "good" | "bad"
27
- prompt: str
28
- response: str
29
- description: Optional[str] = None
30
-
31
-
32
- @dataclass
33
- class TestCase:
34
- """A test case to verify adherence to a rule."""
35
-
36
- id: str
37
- challenge: str
38
- asserts: List[str] = field(default_factory=list)
39
- expected_behavior: Optional[str] = None
40
-
41
-
42
- @dataclass
43
- class Constraints:
44
- """Positive and negative constraints for a rule."""
45
-
46
- must: List[str] = field(default_factory=list)
47
- must_not: List[str] = field(default_factory=list)
48
- should: List[str] = field(default_factory=list)
49
- should_not: List[str] = field(default_factory=list)
50
-
51
-
52
- @dataclass
53
- class Rule:
54
- """A specific policy or rule with constraints, examples, and tests."""
55
-
56
- id: str
57
- title: str
58
- rationale: Optional[str] = None
59
- constraints: Constraints = field(default_factory=Constraints)
60
- examples: List[Example] = field(default_factory=list)
61
- tests: List[TestCase] = field(default_factory=list)
62
- priority: Optional[int] = None # Higher = more important
63
-
64
-
65
- @dataclass
66
- class Metadata:
67
- """Metadata about the specification."""
68
-
69
- id: str
70
- title: str
71
- version: str
72
- owner: Optional[str] = None
73
- created_at: Optional[str] = None
74
- updated_at: Optional[str] = None
75
- imports: List[str] = field(default_factory=list)
76
- scope: Optional[str] = None
77
- description: Optional[str] = None
78
-
79
-
80
- @dataclass
81
- class Interfaces:
82
- """Interface definitions for the system."""
83
-
84
- io_modes: List[str] = field(default_factory=list)
85
- capabilities: List[str] = field(default_factory=list)
86
- constraints: Optional[Dict[str, Any]] = None
87
-
88
-
89
- @dataclass
90
- class GlossaryItem:
91
- """A term definition in the glossary."""
92
-
93
- term: str
94
- definition: str
95
- aliases: List[str] = field(default_factory=list)
96
-
97
-
98
- @dataclass
99
- class Spec:
100
- """A complete system specification.
101
-
102
- Hierarchical structure:
103
- - Metadata (versioning, ownership, imports)
104
- - Principles (high-level values)
105
- - Rules (specific policies with constraints, examples, tests)
106
- - Interfaces (capabilities, modes)
107
- - Glossary (domain terminology)
108
- - Changelog (version history)
109
- """
110
-
111
- metadata: Metadata
112
- principles: List[Principle] = field(default_factory=list)
113
- rules: List[Rule] = field(default_factory=list)
114
- interfaces: Interfaces = field(default_factory=Interfaces)
115
- glossary: List[GlossaryItem] = field(default_factory=list)
116
- changelog: List[Dict[str, Any]] = field(default_factory=list)
117
-
118
- def get_rule(self, rule_id: str) -> Optional[Rule]:
119
- """Get a rule by ID."""
120
- for rule in self.rules:
121
- if rule.id == rule_id:
122
- return rule
123
- return None
124
-
125
- def get_principle(self, principle_id: str) -> Optional[Principle]:
126
- """Get a principle by ID."""
127
- for principle in self.principles:
128
- if principle.id == principle_id:
129
- return principle
130
- return None
131
-
132
- def get_glossary_term(self, term: str) -> Optional[GlossaryItem]:
133
- """Get a glossary item by term or alias."""
134
- term_lower = term.lower()
135
- for item in self.glossary:
136
- if item.term.lower() == term_lower:
137
- return item
138
- if any(alias.lower() == term_lower for alias in item.aliases):
139
- return item
140
- return None
141
-
142
- def get_high_priority_rules(self, min_priority: int = 8) -> List[Rule]:
143
- """Get rules with priority >= min_priority."""
144
- return [
145
- rule for rule in self.rules
146
- if rule.priority is not None and rule.priority >= min_priority
147
- ]
148
-
149
-
@@ -1,144 +0,0 @@
1
- """Loaders for system specifications from JSON files."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- from pathlib import Path
7
- from typing import Any, Dict
8
-
9
- from synth_ai.sdk.specs.dataclasses import (
10
- Constraints,
11
- Example,
12
- GlossaryItem,
13
- Interfaces,
14
- Metadata,
15
- Principle,
16
- Rule,
17
- Spec,
18
- TestCase,
19
- )
20
-
21
-
22
- def load_spec_from_dict(data: Dict[str, Any]) -> Spec:
23
- """Load a Spec from a dictionary.
24
-
25
- Args:
26
- data: Dictionary representation of a spec
27
-
28
- Returns:
29
- Spec instance
30
- """
31
- # Load metadata
32
- metadata_dict = data["metadata"]
33
- md = Metadata(
34
- id=metadata_dict["id"],
35
- title=metadata_dict["title"],
36
- version=metadata_dict["version"],
37
- owner=metadata_dict.get("owner"),
38
- created_at=metadata_dict.get("created_at"),
39
- updated_at=metadata_dict.get("updated_at"),
40
- imports=metadata_dict.get("imports", []),
41
- scope=metadata_dict.get("scope"),
42
- description=metadata_dict.get("description"),
43
- )
44
-
45
- # Load principles
46
- principles = [
47
- Principle(
48
- id=p["id"],
49
- text=p["text"],
50
- rationale=p.get("rationale"),
51
- )
52
- for p in data.get("principles", [])
53
- ]
54
-
55
- # Load rules
56
- def load_rule(r: Dict[str, Any]) -> Rule:
57
- constraints_data = r.get("constraints", {})
58
- constraints = Constraints(**constraints_data)
59
-
60
- examples_data = r.get("examples", [])
61
- examples = [
62
- Example(
63
- kind=e["kind"],
64
- prompt=e["prompt"],
65
- response=e["response"],
66
- description=e.get("description"),
67
- )
68
- for e in examples_data
69
- ]
70
-
71
- tests_data = r.get("tests", [])
72
- tests = [
73
- TestCase(
74
- id=t["id"],
75
- challenge=t["challenge"],
76
- asserts=t.get("asserts", []),
77
- expected_behavior=t.get("expected_behavior"),
78
- )
79
- for t in tests_data
80
- ]
81
-
82
- return Rule(
83
- id=r["id"],
84
- title=r["title"],
85
- rationale=r.get("rationale"),
86
- constraints=constraints,
87
- examples=examples,
88
- tests=tests,
89
- priority=r.get("priority"),
90
- )
91
-
92
- rules = [load_rule(r) for r in data.get("rules", [])]
93
-
94
- # Load interfaces
95
- interfaces_data = data.get("interfaces", {})
96
- interfaces = Interfaces(**interfaces_data)
97
-
98
- # Load glossary
99
- glossary = [
100
- GlossaryItem(
101
- term=g["term"],
102
- definition=g["definition"],
103
- aliases=g.get("aliases", []),
104
- )
105
- for g in data.get("glossary", [])
106
- ]
107
-
108
- # Load changelog
109
- changelog = data.get("changelog", [])
110
-
111
- return Spec(
112
- metadata=md,
113
- principles=principles,
114
- rules=rules,
115
- interfaces=interfaces,
116
- glossary=glossary,
117
- changelog=changelog,
118
- )
119
-
120
-
121
- def load_spec_from_file(path: str | Path) -> Spec:
122
- """Load a Spec from a JSON file.
123
-
124
- Args:
125
- path: Path to JSON file
126
-
127
- Returns:
128
- Spec instance
129
-
130
- Raises:
131
- FileNotFoundError: If file doesn't exist
132
- json.JSONDecodeError: If file is not valid JSON
133
- """
134
- path = Path(path)
135
-
136
- if not path.exists():
137
- raise FileNotFoundError(f"Spec file not found: {path}")
138
-
139
- with open(path, encoding="utf-8") as f:
140
- data = json.load(f)
141
-
142
- return load_spec_from_dict(data)
143
-
144
-