trycua--cua
91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
241 行
7.8 KiB
Python
241 行
7.8 KiB
Python
"""
|
|
Package a DaaS KiCad submission JSON into a self-contained cb task folder.
|
|
|
|
Usage:
|
|
python package_kicad_submission.py <submission.json> --output <output_dir>
|
|
|
|
The submission JSON must contain:
|
|
- submission_id
|
|
- circuit_pcb_file.s3Uri
|
|
- netlist.s3Uri
|
|
- circuit_prompt (used as task description)
|
|
- difficulty
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import io
|
|
import json
|
|
import re
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import boto3
|
|
|
|
# Patterns to skip when extracting KiCad zips
|
|
_SKIP_PREFIXES = ("__MACOSX", "._", "_autosave-", "#auto_saved_files#", "~")
|
|
_SKIP_SUFFIXES = ("-backups", ".DS_Store")
|
|
|
|
_REMOTE_PROJECT_DIR = "/home/cua/kicad_project"
|
|
|
|
_MAIN_PY_TEMPLATE = '''\
|
|
"""Auto-generated cb task for KiCad submission {submission_id}."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
import cua_bench as cb
|
|
|
|
_SUBMISSION_ID = "{submission_id}"
|
|
_REMOTE_PROJECT_DIR = "{remote_project_dir}"
|
|
_HARNESS_DIR = Path(__file__).parent
|
|
|
|
|
|
@cb.tasks_config
|
|
def tasks() -> list[cb.Task]:
|
|
return [
|
|
cb.Task(
|
|
description={description!r},
|
|
metadata={{"difficulty": {difficulty!r}, "submission_id": _SUBMISSION_ID}},
|
|
)
|
|
]
|
|
|
|
|
|
@cb.setup_task(split="train")
|
|
async def start(task_cfg: cb.Task, session: cb.DesktopSession) -> None:
|
|
await session.apps.kicad.install(with_shortcut=True)
|
|
|
|
initial_dir = _HARNESS_DIR / "initial"
|
|
for local_path in sorted(initial_dir.rglob("*")):
|
|
if not local_path.is_file():
|
|
continue
|
|
rel = local_path.relative_to(initial_dir)
|
|
remote_path = f"{{_REMOTE_PROJECT_DIR}}/{{rel.as_posix()}}"
|
|
remote_dir = f"{{_REMOTE_PROJECT_DIR}}/{{rel.parent.as_posix()}}"
|
|
await session.run_command(f"mkdir -p '{{remote_dir}}'", check=False)
|
|
await session.write_bytes(remote_path, local_path.read_bytes())
|
|
|
|
try:
|
|
await session.apps.kicad.launch(project_path={project_path!r})
|
|
except Exception:
|
|
pass
|
|
await asyncio.sleep(5)
|
|
|
|
|
|
@cb.solve_task(split="train")
|
|
async def solve(task_cfg: cb.Task, session: cb.DesktopSession) -> None:
|
|
"""Oracle solver: upload reference.net directly to the expected output path."""
|
|
await session.run_command(f"mkdir -p '{{_REMOTE_PROJECT_DIR}}'", check=False)
|
|
await session.write_bytes(
|
|
f"{{_REMOTE_PROJECT_DIR}}/output.net",
|
|
(_HARNESS_DIR / "reference.net").read_bytes(),
|
|
)
|
|
|
|
|
|
@cb.evaluate_task(split="train")
|
|
async def evaluate(task_cfg: cb.Task, session: cb.DesktopSession) -> float:
|
|
from cua_bench.netlist_compare import (
|
|
compare_kicad_netlists,
|
|
load_reference_netlist,
|
|
)
|
|
|
|
reference = load_reference_netlist(_HARNESS_DIR / "reference.net", _HARNESS_DIR)
|
|
|
|
# Try common netlist output locations first
|
|
candidate_paths = [
|
|
f"{{_REMOTE_PROJECT_DIR}}/{{_SUBMISSION_ID}}.net",
|
|
f"{{_REMOTE_PROJECT_DIR}}/output.net",
|
|
]
|
|
for path in candidate_paths:
|
|
result = await session.run_command(f"cat '{{path}}'", check=False)
|
|
candidate = result.get("stdout", "")
|
|
if candidate.strip():
|
|
return compare_kicad_netlists(candidate, reference)
|
|
|
|
# Search for any .net file in the project dir
|
|
result = await session.run_command(
|
|
f"find {{_REMOTE_PROJECT_DIR}} -name '*.net' | head -1", check=False
|
|
)
|
|
net_path = result.get("stdout", "").strip()
|
|
if net_path:
|
|
result = await session.run_command(f"cat '{{net_path}}'", check=False)
|
|
candidate = result.get("stdout", "")
|
|
if candidate.strip():
|
|
return compare_kicad_netlists(candidate, reference)
|
|
|
|
# Fall back: agent edited schematic without exporting — use kicad-cli to generate netlist
|
|
result = await session.run_command(
|
|
f"find {{_REMOTE_PROJECT_DIR}} -name '*.kicad_sch' | head -1", check=False
|
|
)
|
|
sch_path = result.get("stdout", "").strip()
|
|
if sch_path:
|
|
out_net = "/tmp/kicad_eval_output.net"
|
|
await session.run_command(
|
|
f"kicad-cli sch export netlist '{{sch_path}}' -o '{{out_net}}'",
|
|
check=False,
|
|
)
|
|
result = await session.run_command(f"cat '{{out_net}}'", check=False)
|
|
candidate = result.get("stdout", "")
|
|
if candidate.strip():
|
|
return compare_kicad_netlists(candidate, reference)
|
|
|
|
return 0.0
|
|
'''
|
|
|
|
|
|
def _should_skip(name: str) -> bool:
|
|
parts = name.replace("\\", "/").split("/")
|
|
for part in parts:
|
|
if any(part.startswith(p) for p in _SKIP_PREFIXES):
|
|
return True
|
|
if any(part.endswith(s) for s in _SKIP_SUFFIXES):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _download_zip(s3_uri: str) -> zipfile.ZipFile:
|
|
s3 = boto3.client("s3", region_name="us-west-2")
|
|
bucket, key = s3_uri.replace("s3://", "").split("/", 1)
|
|
obj = s3.get_object(Bucket=bucket, Key=key)
|
|
return zipfile.ZipFile(io.BytesIO(obj["Body"].read()))
|
|
|
|
|
|
def _first_net_file(zf: zipfile.ZipFile) -> bytes:
|
|
for name in zf.namelist():
|
|
if _should_skip(name):
|
|
continue
|
|
if name.endswith(".net") and not name.endswith("/"):
|
|
return zf.read(name)
|
|
raise ValueError("No .net file found in netlist zip")
|
|
|
|
|
|
def _extract_kicad_zip(zf: zipfile.ZipFile, dest: Path) -> None:
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
for name in zf.namelist():
|
|
if _should_skip(name):
|
|
continue
|
|
if name.endswith("/"):
|
|
continue
|
|
out = dest / name
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_bytes(zf.read(name))
|
|
|
|
|
|
def _slugify(text: str) -> str:
|
|
return re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")[:40]
|
|
|
|
|
|
def package(submission: dict, output_dir: Path) -> None:
|
|
sub_id = submission["submission_id"]
|
|
initial_circuit = submission.get("initial_circuit")
|
|
pcb_uri = initial_circuit["s3Uri"] if initial_circuit else None
|
|
netlist_uri = submission["netlist"]["s3Uri"]
|
|
description = submission.get("circuit_prompt", "").strip()
|
|
difficulty = submission.get("difficulty", "unknown")
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Download and extract netlist → reference.net
|
|
print(f" Downloading netlist: {netlist_uri}")
|
|
netlist_zip = _download_zip(netlist_uri)
|
|
reference_net = _first_net_file(netlist_zip)
|
|
(output_dir / "reference.net").write_bytes(reference_net)
|
|
|
|
# Download and extract PCB files → initial/ (empty for start-from-scratch tasks)
|
|
if pcb_uri:
|
|
print(f" Downloading PCB: {pcb_uri}")
|
|
pcb_zip = _download_zip(pcb_uri)
|
|
_extract_kicad_zip(pcb_zip, output_dir / "initial")
|
|
else:
|
|
print(" No initial circuit (start from scratch)")
|
|
(output_dir / "initial").mkdir(parents=True, exist_ok=True)
|
|
|
|
# Derive remote .kicad_pro path from the zip filename (stem = folder and project name)
|
|
if initial_circuit:
|
|
stem = Path(initial_circuit["filename"]).stem # e.g. "kicad_555timer_circuit"
|
|
project_path = f"{_REMOTE_PROJECT_DIR}/{stem}/{stem}.kicad_pro"
|
|
else:
|
|
project_path = None
|
|
|
|
# Generate main.py
|
|
main_py = _MAIN_PY_TEMPLATE.format(
|
|
submission_id=sub_id,
|
|
remote_project_dir=_REMOTE_PROJECT_DIR,
|
|
description=description,
|
|
difficulty=difficulty,
|
|
project_path=project_path,
|
|
)
|
|
(output_dir / "main.py").write_text(main_py, encoding="utf-8")
|
|
|
|
# Copy submission.json
|
|
(output_dir / "submission.json").write_text(
|
|
json.dumps(submission, indent=2), encoding="utf-8"
|
|
)
|
|
|
|
print(f" → {output_dir}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("submission_json", help="Path to submission JSON file")
|
|
parser.add_argument("--output", required=True, help="Output directory")
|
|
args = parser.parse_args()
|
|
|
|
submission = json.loads(Path(args.submission_json).read_text())
|
|
package(submission, Path(args.output))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|