Skip to content
Open

Demos #187

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ pre-commit:
run: uv run nbstripout {files}
stage_fixed: true

# Demos: Auto-generate .ipynb from .py files with cell markers
demos-notebook-gen:
tags:
- jupyter
- generate
glob:
- "demos/**/*.py"
run: uv run python scripts/py_to_ipynb.py --force {files}
stage_fixed: true

# TUI: TypeScript typecheck
tui-typecheck:
tags:
Expand Down
97 changes: 97 additions & 0 deletions scripts/py_to_ipynb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Convert demo .py files with percent markers to .ipynb notebooks.

Only processes .py files under the repo's demos/ directory that include
Jupytext-style cell markers (lines starting with "# %%").

Usage:
uv run python scripts/py_to_ipynb.py demos/gepa_banking77/run_banking77.py
uv run python scripts/py_to_ipynb.py demos/ # all demo notebooks
uv run python scripts/py_to_ipynb.py --force demos/ # overwrite existing
"""

import argparse
import subprocess
from pathlib import Path


def _repo_root() -> Path:
return Path(__file__).resolve().parents[1]


def _demos_dir() -> Path:
return _repo_root() / "demos"


def _is_demo_py(path: Path) -> bool:
if path.suffix != ".py":
return False
try:
path.resolve().relative_to(_demos_dir().resolve())
except ValueError:
return False
return True


def _has_ipynb_markers(py_path: Path) -> bool:
try:
for line in py_path.read_text(encoding="utf-8").splitlines():
if line.lstrip().startswith("# %%"):
return True
except Exception:
return False
return False


def convert_file(py_path: Path, force: bool = False) -> bool:
"""Convert a single .py file to .ipynb."""
if not _is_demo_py(py_path):
return False
if not _has_ipynb_markers(py_path):
return False # Not a notebook-style .py file

ipynb_path = py_path.with_suffix(".ipynb")

if ipynb_path.exists() and not force:
print(f"Skipping {py_path} (.ipynb exists, use --force)")
return False

# Delete existing file first (jupytext overwrite is unreliable)
if ipynb_path.exists():
ipynb_path.unlink()

result = subprocess.run(
["jupytext", "--to", "notebook", str(py_path)],
capture_output=True,
text=True,
)

if result.returncode != 0:
print(f"Error: {py_path}: {result.stderr}")
return False

print(f"{py_path} -> {ipynb_path}")
return True


def main():
parser = argparse.ArgumentParser()
parser.add_argument("paths", nargs="+")
parser.add_argument("--force", "-f", action="store_true")
args = parser.parse_args()

converted = 0
for path in map(Path, args.paths):
if path.is_dir():
files = path.rglob("*.py")
else:
files = [path]
for f in files:
if convert_file(f, args.force):
converted += 1

print(f"Converted {converted} file(s)")


if __name__ == "__main__":
main()
9 changes: 9 additions & 0 deletions synth_ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
create_task_app,
)
from synth_ai.sdk.optimization import GraphOptimizationJob, PolicyOptimizationJob
from synth_ai.sdk.utils import confidence_band, split_seed_slices, stratified_seed_sample

# Legacy aliases
PromptLearningJob = PolicyOptimizationJob
Expand Down Expand Up @@ -118,6 +119,10 @@
"VerifierTracePayload",
"ReviewPayload",
"CriterionScorePayload",
# Utils
"confidence_band",
"split_seed_slices",
"stratified_seed_sample",
]

# Lazy loading map: name -> (module, attribute)
Expand Down Expand Up @@ -149,6 +154,10 @@
"VerifierTracePayload": ("synth_ai.sdk.graphs.verifier_schemas", "VerifierTracePayload"),
"ReviewPayload": ("synth_ai.sdk.graphs.verifier_schemas", "ReviewPayload"),
"CriterionScorePayload": ("synth_ai.sdk.graphs.verifier_schemas", "CriterionScorePayload"),
# Utils
"confidence_band": ("synth_ai.sdk.utils", "confidence_band"),
"split_seed_slices": ("synth_ai.sdk.utils", "split_seed_slices"),
"stratified_seed_sample": ("synth_ai.sdk.utils", "stratified_seed_sample"),
}


Expand Down
1 change: 1 addition & 0 deletions synth_ai/core/streaming/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class StreamConfig:
sample_rate: float = 1.0
max_events_per_poll: int | None = None
deduplicate: bool = True
dedupe_events: bool = True

@classmethod
def default(cls) -> StreamConfig:
Expand Down
4 changes: 4 additions & 0 deletions synth_ai/core/streaming/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ def should_handle(self, message: StreamMessage) -> bool: # pragma: no cover - t
"""Predicate allowing handlers to filter messages before processing."""
return True

def wants_event_backfill(self) -> bool: # pragma: no cover - optional
"""Whether the streamer should backfill events via polling."""
return False

def flush(self) -> None: # pragma: no cover - optional
"""Flush buffered output."""
return None
Expand Down
Loading
Loading