|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""run_sleep.py — OpenClaw entry point for SkillOpt-Sleep. |
| 3 | +
|
| 4 | +Runs one nightly sleep cycle: |
| 5 | + 1. harvest recent session transcripts |
| 6 | + 2. mine recurring task patterns |
| 7 | + 3. replay tasks with current skill (baseline) + candidate skill (with proposed edit) |
| 8 | + 4. gate candidate vs baseline on held-out accuracy |
| 9 | + 5. stage the proposal in ~/.skillopt-sleep/staging/<night>/ |
| 10 | + 6. leave adoption to Ethan (auto_adopt=false) |
| 11 | +
|
| 12 | +Usage: |
| 13 | + python3 run_sleep.py # one cycle, default config |
| 14 | + python3 run_sleep.py --dry-run # compute report only, no staging |
| 15 | + python3 run_sleep.py --tasks path.json # use a pre-built task file |
| 16 | +""" |
| 17 | +from __future__ import annotations |
| 18 | + |
| 19 | +import argparse |
| 20 | +import json |
| 21 | +import os |
| 22 | +import sys |
| 23 | +from pathlib import Path |
| 24 | + |
| 25 | +# Ensure the skillopt_sleep package is importable (it lives in the cloned repo) |
| 26 | +REPO = Path("/home/ethanclaw/.openclaw/workspace/SkillOpt") |
| 27 | +sys.path.insert(0, str(REPO)) |
| 28 | + |
| 29 | +# Register our backend before importing cycle |
| 30 | +from skillopt_sleep_openclaw import OpenClawDeepSeekBackend |
| 31 | +import skillopt_sleep.backend as _b |
| 32 | +_b._BACKENDS = getattr(_b, "_BACKENDS", {}) |
| 33 | +_b._BACKENDS["openclaw-deepseek"] = OpenClawDeepSeekBackend |
| 34 | + |
| 35 | +# Patch get_backend to know about our backend |
| 36 | +_orig_get_backend = _b.get_backend |
| 37 | + |
| 38 | +def get_backend(name, model="", codex_path=""): |
| 39 | + if name == "openclaw-deepseek": |
| 40 | + return OpenClawDeepSeekBackend(model=model or "deepseek-v4-pro") |
| 41 | + return _orig_get_backend(name, model=model, codex_path=codex_path) |
| 42 | + |
| 43 | +_b.get_backend = get_backend |
| 44 | + |
| 45 | +from skillopt_sleep.cycle import run_sleep_cycle |
| 46 | +from skillopt_sleep.config import load_config |
| 47 | + |
| 48 | + |
| 49 | +def main() -> int: |
| 50 | + ap = argparse.ArgumentParser(description="OpenClaw SkillOpt-Sleep nightly cycle") |
| 51 | + ap.add_argument("--dry-run", action="store_true", help="Compute but don't stage") |
| 52 | + ap.add_argument("--config", default="/home/ethanclaw/.openclaw/workspace/skills/skillopt-sleep/config.json") |
| 53 | + ap.add_argument("--tasks", default=None, help="Path to pre-built tasks JSON") |
| 54 | + ap.add_argument("--verbose", action="store_true") |
| 55 | + args = ap.parse_args() |
| 56 | + |
| 57 | + # Load config from file then override with our defaults |
| 58 | + overrides = {} |
| 59 | + if os.path.exists(args.config): |
| 60 | + with open(args.config) as f: |
| 61 | + overrides.update(json.load(f)) |
| 62 | + overrides.pop("_comment", None) |
| 63 | + |
| 64 | + cfg = load_config(**overrides) |
| 65 | + |
| 66 | + seed_tasks = None |
| 67 | + if args.tasks: |
| 68 | + from skillopt_sleep.types import TaskRecord |
| 69 | + with open(args.tasks) as f: |
| 70 | + raw = json.load(f) |
| 71 | + # Translate our test-set fields → TaskRecord fields |
| 72 | + seed_tasks = [] |
| 73 | + for t in raw: |
| 74 | + seed_tasks.append(TaskRecord( |
| 75 | + id=t['id'], |
| 76 | + project=t.get('project', 'openclaw'), |
| 77 | + intent=t.get('intent') or t.get('prompt', ''), |
| 78 | + context_excerpt=t.get('context_excerpt', ''), |
| 79 | + attempted_solution=t.get('attempted_solution', ''), |
| 80 | + outcome=t.get('outcome', 'unknown'), |
| 81 | + reference_kind=t.get('reference_kind', 'rubric'), |
| 82 | + reference=t.get('reference', ''), |
| 83 | + judge=t.get('judge', {}), |
| 84 | + tags=t.get('tags', []), |
| 85 | + source_sessions=t.get('source_sessions', []), |
| 86 | + split=t.get('split', 'train'), |
| 87 | + )) |
| 88 | + |
| 89 | + print(f"[skillopt-sleep] starting cycle...") |
| 90 | + print(f" backend: {cfg.get('backend')}") |
| 91 | + print(f" project: {cfg.get('invoked_project')}") |
| 92 | + print(f" max tasks: {cfg.get('max_tasks_per_night')}") |
| 93 | + print(f" edit budget: {cfg.get('edit_budget')}") |
| 94 | + print(f" dry_run: {args.dry_run}") |
| 95 | + |
| 96 | + outcome = run_sleep_cycle(cfg, seed_tasks=seed_tasks, dry_run=args.dry_run) |
| 97 | + |
| 98 | + r = outcome.report |
| 99 | + print(f"\n=== Report — night {r.night} ===") |
| 100 | + print(f" sessions harvested: {r.n_sessions}") |
| 101 | + print(f" tasks mined: {r.n_tasks} (replayed: {r.n_replayed})") |
| 102 | + print(f" baseline: {r.baseline_score:.3f} -> candidate: {r.candidate_score:.3f}") |
| 103 | + print(f" gate: {r.gate_action} accepted={r.accepted}") |
| 104 | + print(f" tokens: {r.tokens_used}") |
| 105 | + if r.edits: |
| 106 | + print(f" applied edits ({len(r.edits)}):") |
| 107 | + for e in r.edits: |
| 108 | + print(f" [{e.target}/{e.op}] {e.content[:80]}...") |
| 109 | + if r.rejected_edits: |
| 110 | + print(f" rejected edits ({len(r.rejected_edits)}) — kept as negative feedback") |
| 111 | + if r.notes: |
| 112 | + for n in r.notes: |
| 113 | + print(f" note: {n}") |
| 114 | + if outcome.staging_dir: |
| 115 | + print(f"\n STAGED at: {outcome.staging_dir}") |
| 116 | + print(f" Review with: ls {outcome.staging_dir}") |
| 117 | + |
| 118 | + return 0 if r.accepted or r.candidate_score >= r.baseline_score else 1 |
| 119 | + |
| 120 | + |
| 121 | +if __name__ == "__main__": |
| 122 | + sys.exit(main()) |
0 commit comments