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.
- synth_ai/__init__.py +13 -13
- synth_ai/cli/__init__.py +6 -15
- synth_ai/cli/commands/eval/__init__.py +6 -15
- synth_ai/cli/commands/eval/config.py +338 -0
- synth_ai/cli/commands/eval/core.py +236 -1091
- synth_ai/cli/commands/eval/runner.py +704 -0
- synth_ai/cli/commands/eval/validation.py +44 -117
- synth_ai/cli/commands/filter/core.py +7 -7
- synth_ai/cli/commands/filter/validation.py +2 -2
- synth_ai/cli/commands/smoke/core.py +7 -17
- synth_ai/cli/commands/status/__init__.py +1 -64
- synth_ai/cli/commands/status/client.py +50 -151
- synth_ai/cli/commands/status/config.py +3 -83
- synth_ai/cli/commands/status/errors.py +4 -13
- synth_ai/cli/commands/status/subcommands/__init__.py +2 -8
- synth_ai/cli/commands/status/subcommands/config.py +13 -0
- synth_ai/cli/commands/status/subcommands/files.py +18 -63
- synth_ai/cli/commands/status/subcommands/jobs.py +28 -311
- synth_ai/cli/commands/status/subcommands/models.py +18 -62
- synth_ai/cli/commands/status/subcommands/runs.py +16 -63
- synth_ai/cli/commands/status/subcommands/session.py +67 -172
- synth_ai/cli/commands/status/subcommands/summary.py +24 -32
- synth_ai/cli/commands/status/subcommands/utils.py +41 -0
- synth_ai/cli/commands/status/utils.py +16 -107
- synth_ai/cli/commands/train/__init__.py +18 -20
- synth_ai/cli/commands/train/errors.py +3 -3
- synth_ai/cli/commands/train/prompt_learning_validation.py +15 -16
- synth_ai/cli/commands/train/validation.py +7 -7
- synth_ai/cli/commands/train/{judge_schemas.py → verifier_schemas.py} +33 -34
- synth_ai/cli/commands/train/verifier_validation.py +235 -0
- synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +0 -1
- synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +2 -6
- synth_ai/cli/demo_apps/math/config.toml +0 -1
- synth_ai/cli/demo_apps/math/modal_task_app.py +2 -6
- synth_ai/cli/demo_apps/mipro/task_app.py +25 -47
- synth_ai/cli/lib/apps/task_app.py +12 -13
- synth_ai/cli/lib/task_app_discovery.py +6 -6
- synth_ai/cli/lib/train_cfgs.py +10 -10
- synth_ai/cli/task_apps/__init__.py +11 -0
- synth_ai/cli/task_apps/commands.py +7 -15
- synth_ai/core/env.py +12 -1
- synth_ai/core/errors.py +1 -2
- synth_ai/core/integrations/cloudflare.py +209 -33
- synth_ai/core/tracing_v3/abstractions.py +46 -0
- synth_ai/data/__init__.py +3 -30
- synth_ai/data/enums.py +1 -20
- synth_ai/data/rewards.py +100 -3
- synth_ai/products/graph_evolve/__init__.py +1 -2
- synth_ai/products/graph_evolve/config.py +16 -16
- synth_ai/products/graph_evolve/converters/__init__.py +3 -3
- synth_ai/products/graph_evolve/converters/openai_sft.py +7 -7
- synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +1 -1
- synth_ai/products/graph_gepa/__init__.py +23 -0
- synth_ai/products/graph_gepa/converters/__init__.py +19 -0
- synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
- synth_ai/sdk/__init__.py +45 -35
- synth_ai/sdk/api/eval/__init__.py +33 -0
- synth_ai/sdk/api/eval/job.py +732 -0
- synth_ai/sdk/api/research_agent/__init__.py +276 -66
- synth_ai/sdk/api/train/builders.py +181 -0
- synth_ai/sdk/api/train/cli.py +41 -33
- synth_ai/sdk/api/train/configs/__init__.py +6 -4
- synth_ai/sdk/api/train/configs/prompt_learning.py +127 -33
- synth_ai/sdk/api/train/configs/rl.py +264 -16
- synth_ai/sdk/api/train/configs/sft.py +165 -1
- synth_ai/sdk/api/train/graph_validators.py +12 -12
- synth_ai/sdk/api/train/graphgen.py +169 -51
- synth_ai/sdk/api/train/graphgen_models.py +95 -45
- synth_ai/sdk/api/train/local_api.py +10 -0
- synth_ai/sdk/api/train/pollers.py +36 -0
- synth_ai/sdk/api/train/prompt_learning.py +390 -60
- synth_ai/sdk/api/train/rl.py +41 -5
- synth_ai/sdk/api/train/sft.py +2 -0
- synth_ai/sdk/api/train/task_app.py +20 -0
- synth_ai/sdk/api/train/validators.py +17 -17
- synth_ai/sdk/graphs/completions.py +239 -33
- synth_ai/sdk/{judging/schemas.py → graphs/verifier_schemas.py} +23 -23
- synth_ai/sdk/learning/__init__.py +35 -5
- synth_ai/sdk/learning/context_learning_client.py +531 -0
- synth_ai/sdk/learning/context_learning_types.py +294 -0
- synth_ai/sdk/learning/prompt_learning_client.py +1 -1
- synth_ai/sdk/learning/prompt_learning_types.py +2 -1
- synth_ai/sdk/learning/rl/__init__.py +0 -4
- synth_ai/sdk/learning/rl/contracts.py +0 -4
- synth_ai/sdk/localapi/__init__.py +40 -0
- synth_ai/sdk/localapi/apps/__init__.py +28 -0
- synth_ai/sdk/localapi/client.py +10 -0
- synth_ai/sdk/localapi/contracts.py +10 -0
- synth_ai/sdk/localapi/helpers.py +519 -0
- synth_ai/sdk/localapi/rollouts.py +93 -0
- synth_ai/sdk/localapi/server.py +29 -0
- synth_ai/sdk/localapi/template.py +49 -0
- synth_ai/sdk/streaming/handlers.py +6 -6
- synth_ai/sdk/streaming/streamer.py +10 -6
- synth_ai/sdk/task/__init__.py +18 -5
- synth_ai/sdk/task/apps/__init__.py +37 -1
- synth_ai/sdk/task/client.py +9 -1
- synth_ai/sdk/task/config.py +6 -11
- synth_ai/sdk/task/contracts.py +137 -95
- synth_ai/sdk/task/in_process.py +32 -22
- synth_ai/sdk/task/in_process_runner.py +9 -4
- synth_ai/sdk/task/rubrics/__init__.py +2 -3
- synth_ai/sdk/task/rubrics/loaders.py +4 -4
- synth_ai/sdk/task/rubrics/strict.py +3 -4
- synth_ai/sdk/task/server.py +76 -16
- synth_ai/sdk/task/trace_correlation_helpers.py +190 -139
- synth_ai/sdk/task/validators.py +34 -49
- synth_ai/sdk/training/__init__.py +7 -16
- synth_ai/sdk/tunnels/__init__.py +118 -0
- synth_ai/sdk/tunnels/cleanup.py +83 -0
- synth_ai/sdk/tunnels/ports.py +120 -0
- synth_ai/sdk/tunnels/tunneled_api.py +363 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/METADATA +71 -4
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/RECORD +118 -128
- synth_ai/cli/commands/baseline/__init__.py +0 -12
- synth_ai/cli/commands/baseline/core.py +0 -636
- synth_ai/cli/commands/baseline/list.py +0 -94
- synth_ai/cli/commands/eval/errors.py +0 -81
- synth_ai/cli/commands/status/formatters.py +0 -164
- synth_ai/cli/commands/status/subcommands/pricing.py +0 -23
- synth_ai/cli/commands/status/subcommands/usage.py +0 -203
- synth_ai/cli/commands/train/judge_validation.py +0 -305
- synth_ai/cli/usage.py +0 -159
- synth_ai/data/specs.py +0 -36
- synth_ai/sdk/api/research_agent/cli.py +0 -428
- synth_ai/sdk/api/research_agent/config.py +0 -357
- synth_ai/sdk/api/research_agent/job.py +0 -717
- synth_ai/sdk/baseline/__init__.py +0 -25
- synth_ai/sdk/baseline/config.py +0 -209
- synth_ai/sdk/baseline/discovery.py +0 -216
- synth_ai/sdk/baseline/execution.py +0 -154
- synth_ai/sdk/judging/__init__.py +0 -15
- synth_ai/sdk/judging/base.py +0 -24
- synth_ai/sdk/judging/client.py +0 -191
- synth_ai/sdk/judging/types.py +0 -42
- synth_ai/sdk/research_agent/__init__.py +0 -34
- synth_ai/sdk/research_agent/container_builder.py +0 -328
- synth_ai/sdk/research_agent/container_spec.py +0 -198
- synth_ai/sdk/research_agent/defaults.py +0 -34
- synth_ai/sdk/research_agent/results_collector.py +0 -69
- synth_ai/sdk/specs/__init__.py +0 -46
- synth_ai/sdk/specs/dataclasses.py +0 -149
- synth_ai/sdk/specs/loader.py +0 -144
- synth_ai/sdk/specs/serializer.py +0 -199
- synth_ai/sdk/specs/validation.py +0 -250
- synth_ai/sdk/tracing/__init__.py +0 -39
- synth_ai/sdk/usage/__init__.py +0 -37
- synth_ai/sdk/usage/client.py +0 -171
- synth_ai/sdk/usage/models.py +0 -261
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/WHEEL +0 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/licenses/LICENSE +0 -0
- {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"
|
synth_ai/sdk/specs/__init__.py
DELETED
|
@@ -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
|
-
|
synth_ai/sdk/specs/loader.py
DELETED
|
@@ -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
|
-
|