refactor(sleep): decouple engine to top-level skillopt_sleep/ (zero research dep)
Open-source-tool / research-code separation:
- git mv skillopt/sleep/ -> skillopt_sleep/ (top-level, sibling to the research
skillopt/ package). History preserved as renames.
- All imports skillopt.sleep.* -> skillopt_sleep.*.
- Vendor the validation gate into skillopt_sleep/gate.py (a self-contained copy
of skillopt.evaluation.gate). The engine now has ZERO dependency on the
research package — verified: grep finds no `from skillopt.` in skillopt_sleep/,
and consolidate's gate resolves to skillopt_sleep.gate.
- Plugin scripts/commands/skill call `-m skillopt_sleep`.
29 tests pass; `python -m skillopt_sleep` runs standalone.
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ cd SkillOpt
|
|||||||
```
|
```
|
||||||
|
|
||||||
The plugin's bundled runner (`scripts/sleep.sh`) auto-selects a Python ≥ 3.10
|
The plugin's bundled runner (`scripts/sleep.sh`) auto-selects a Python ≥ 3.10
|
||||||
interpreter and calls the `skillopt.sleep` engine in the repo. No `pip install`
|
interpreter and calls the `skillopt_sleep` engine in the repo. No `pip install`
|
||||||
is required for the default `mock` backend or for `claude`/`codex` backends —
|
is required for the default `mock` backend or for `claude`/`codex` backends —
|
||||||
they shell out to the CLIs you already have.
|
they shell out to the CLIs you already have.
|
||||||
|
|
||||||
@@ -65,9 +65,9 @@ they shell out to the CLIs you already have.
|
|||||||
Or call the engine directly (Python ≥ 3.10):
|
Or call the engine directly (Python ≥ 3.10):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m skillopt.sleep run --project "$(pwd)" --scope invoked --backend mock
|
python -m skillopt_sleep run --project "$(pwd)" --scope invoked --backend mock
|
||||||
python -m skillopt.sleep run --project "$(pwd)" --backend claude # real lift via Claude
|
python -m skillopt_sleep run --project "$(pwd)" --backend claude # real lift via Claude
|
||||||
python -m skillopt.sleep run --project "$(pwd)" --backend codex # real lift via Codex
|
python -m skillopt_sleep run --project "$(pwd)" --backend codex # real lift via Codex
|
||||||
```
|
```
|
||||||
|
|
||||||
Default backend is **`mock`** — deterministic, no API spend — so you can try the
|
Default backend is **`mock`** — deterministic, no API spend — so you can try the
|
||||||
@@ -98,10 +98,10 @@ Reproduce:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/garrytan/gbrain-evals /tmp/gbrain-evals
|
git clone https://github.com/garrytan/gbrain-evals /tmp/gbrain-evals
|
||||||
python -m skillopt.sleep.experiments.run_gbrain --backend claude --model haiku \
|
python -m skillopt_sleep.experiments.run_gbrain --backend claude --model haiku \
|
||||||
--seeds brief-writer --data-root /tmp/gbrain-evals/eval/data/skillopt-v1 \
|
--seeds brief-writer --data-root /tmp/gbrain-evals/eval/data/skillopt-v1 \
|
||||||
--nights 1 --limit-replay 3 --limit-holdout 3
|
--nights 1 --limit-replay 3 --limit-holdout 3
|
||||||
python -m skillopt.sleep.experiments.run_gbrain --backend codex \
|
python -m skillopt_sleep.experiments.run_gbrain --backend codex \
|
||||||
--seeds brief-writer --data-root /tmp/gbrain-evals/eval/data/skillopt-v1 \
|
--seeds brief-writer --data-root /tmp/gbrain-evals/eval/data/skillopt-v1 \
|
||||||
--nights 1 --limit-replay 3 --limit-holdout 3
|
--nights 1 --limit-replay 3 --limit-holdout 3
|
||||||
```
|
```
|
||||||
@@ -109,8 +109,8 @@ python -m skillopt.sleep.experiments.run_gbrain --backend codex \
|
|||||||
## Deterministic proof (no API, no keys)
|
## Deterministic proof (no API, no keys)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m skillopt.sleep.experiments.run_experiment --persona researcher --assert-improves
|
python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves
|
||||||
python -m skillopt.sleep.experiments.run_experiment --persona programmer --assert-improves
|
python -m skillopt_sleep.experiments.run_experiment --persona programmer --assert-improves
|
||||||
```
|
```
|
||||||
|
|
||||||
Each prints the held-out score rising from baseline toward 1.0 as the gate
|
Each prints the held-out score rising from baseline toward 1.0 as the gate
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ held-out replay score, and nothing live is modified until the user adopts it.
|
|||||||
|
|
||||||
## How to run it
|
## How to run it
|
||||||
|
|
||||||
The engine is the `skillopt.sleep` Python package in this repo. Use the
|
The engine is the `skillopt_sleep` Python package in this repo. Use the
|
||||||
**plugin's bundled runner** so the right interpreter and repo are on the path:
|
**plugin's bundled runner** so the right interpreter and repo are on the path:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# SkillOpt-Sleep runner — invokes the skillopt.sleep engine with a suitable
|
# SkillOpt-Sleep runner — invokes the skillopt_sleep engine with a suitable
|
||||||
# Python interpreter, from the repo that contains this plugin.
|
# Python interpreter, from the repo that contains this plugin.
|
||||||
#
|
#
|
||||||
# Usage: sleep.sh <run|dry-run|status|adopt|harvest> [extra args...]
|
# Usage: sleep.sh <run|dry-run|status|adopt|harvest> [extra args...]
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Resolve the repo root: the plugin lives at <repo>/skillopt-sleep-plugin,
|
# Resolve the repo root: the plugin lives at <repo>/skillopt-sleep-plugin,
|
||||||
# so the engine package is at <repo>/skillopt/sleep. CLAUDE_PLUGIN_ROOT points
|
# so the engine package is at <repo>/skillopt_sleep. CLAUDE_PLUGIN_ROOT points
|
||||||
# at the plugin dir when run by Claude Code; fall back to this script's dir.
|
# at the plugin dir when run by Claude Code; fall back to this script's dir.
|
||||||
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||||
REPO_ROOT="$(cd "$PLUGIN_ROOT/.." && pwd)"
|
REPO_ROOT="$(cd "$PLUGIN_ROOT/.." && pwd)"
|
||||||
@@ -27,4 +27,4 @@ fi
|
|||||||
if [ "$#" -eq 0 ]; then set -- status; fi
|
if [ "$#" -eq 0 ]; then set -- status; fi
|
||||||
|
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
exec "$PY" -m skillopt.sleep "$@"
|
exec "$PY" -m skillopt_sleep "$@"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: skillopt-sleep
|
name: skillopt-sleep
|
||||||
description: "Use when the user wants their Claude agent to self-improve from past usage, asks about a nightly/offline 'sleep' or 'dream' cycle, memory/skill consolidation, or says things like '让 agent 越用越好用', 'review my past sessions', 'learn my preferences', 'consolidate what you learned', 'run the sleep cycle', or wants to schedule offline self-optimization. Drives the skillopt.sleep engine: harvest past sessions → mine recurring tasks → replay offline → consolidate validated CLAUDE.md/SKILL.md behind a held-out gate."
|
description: "Use when the user wants their Claude agent to self-improve from past usage, asks about a nightly/offline 'sleep' or 'dream' cycle, memory/skill consolidation, or says things like '让 agent 越用越好用', 'review my past sessions', 'learn my preferences', 'consolidate what you learned', 'run the sleep cycle', or wants to schedule offline self-optimization. Drives the skillopt_sleep engine: harvest past sessions → mine recurring tasks → replay offline → consolidate validated CLAUDE.md/SKILL.md behind a held-out gate."
|
||||||
---
|
---
|
||||||
|
|
||||||
# SkillOpt-Sleep: offline self-evolution for a local Claude agent
|
# SkillOpt-Sleep: offline self-evolution for a local Claude agent
|
||||||
@@ -62,7 +62,7 @@ Prefer the `/sleep` command. Under the hood it calls the bundled runner:
|
|||||||
- Always show the user the **held-out baseline → candidate** score and the
|
- Always show the user the **held-out baseline → candidate** score and the
|
||||||
exact proposed edits before suggesting adoption. Evidence before adoption.
|
exact proposed edits before suggesting adoption. Evidence before adoption.
|
||||||
- If asked whether it really helps, run
|
- If asked whether it really helps, run
|
||||||
`python -m skillopt.sleep.experiments.run_experiment --persona researcher --json`
|
`python -m skillopt_sleep.experiments.run_experiment --persona researcher --json`
|
||||||
— a deterministic demo that proves held-out lift and that the gate blocks
|
— a deterministic demo that proves held-out lift and that the gate blocks
|
||||||
harmful edits.
|
harmful edits.
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@ Prefer the `/sleep` command. Under the hood it calls the bundled runner:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# deterministic proof (no API): held-out score rises, gate blocks regressions
|
# deterministic proof (no API): held-out score rises, gate blocks regressions
|
||||||
python -m skillopt.sleep.experiments.run_experiment --persona researcher --assert-improves
|
python -m skillopt_sleep.experiments.run_experiment --persona researcher --assert-improves
|
||||||
python -m skillopt.sleep.experiments.run_experiment --persona programmer --assert-improves
|
python -m skillopt_sleep.experiments.run_experiment --persona programmer --assert-improves
|
||||||
```
|
```
|
||||||
|
|
||||||
See `docs/sleep/experiment_results.md` for recorded output and
|
See `docs/sleep/experiment_results.md` for recorded output and
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ Synthesizes three ideas:
|
|||||||
* Sleep — short-term experience -> long-term competence, offline
|
* Sleep — short-term experience -> long-term competence, offline
|
||||||
|
|
||||||
Public entry points:
|
Public entry points:
|
||||||
* skillopt.sleep.cli — `python -m skillopt.sleep ...`
|
* skillopt_sleep.cli — `python -m skillopt_sleep ...`
|
||||||
* skillopt.sleep.cycle.run_sleep_cycle(...)
|
* skillopt_sleep.cycle.run_sleep_cycle(...)
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
"""SkillOpt-Sleep — command-line interface.
|
"""SkillOpt-Sleep — command-line interface.
|
||||||
|
|
||||||
python -m skillopt.sleep run # full cycle: harvest->mine->replay->gate->stage
|
python -m skillopt_sleep run # full cycle: harvest->mine->replay->gate->stage
|
||||||
python -m skillopt.sleep dry-run # same but report only, no staging/adopt
|
python -m skillopt_sleep dry-run # same but report only, no staging/adopt
|
||||||
python -m skillopt.sleep status # show state + latest staged proposal
|
python -m skillopt_sleep status # show state + latest staged proposal
|
||||||
python -m skillopt.sleep adopt # apply the latest staged proposal (with backup)
|
python -m skillopt_sleep adopt # apply the latest staged proposal (with backup)
|
||||||
python -m skillopt.sleep harvest # just print what would be mined (debug)
|
python -m skillopt_sleep harvest # just print what would be mined (debug)
|
||||||
|
|
||||||
Common flags:
|
Common flags:
|
||||||
--project PATH project to evolve (default: cwd)
|
--project PATH project to evolve (default: cwd)
|
||||||
@@ -23,12 +23,12 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from skillopt.sleep.config import load_config
|
from skillopt_sleep.config import load_config
|
||||||
from skillopt.sleep.cycle import run_sleep_cycle
|
from skillopt_sleep.cycle import run_sleep_cycle
|
||||||
from skillopt.sleep.harvest import harvest
|
from skillopt_sleep.harvest import harvest
|
||||||
from skillopt.sleep.mine import mine
|
from skillopt_sleep.mine import mine
|
||||||
from skillopt.sleep.state import SleepState
|
from skillopt_sleep.state import SleepState
|
||||||
from skillopt.sleep.staging import latest_staging, adopt as adopt_staging
|
from skillopt_sleep.staging import latest_staging, adopt as adopt_staging
|
||||||
|
|
||||||
|
|
||||||
def _add_common(p: argparse.ArgumentParser) -> None:
|
def _add_common(p: argparse.ArgumentParser) -> None:
|
||||||
@@ -90,7 +90,7 @@ def cmd_run(args, dry: bool = False) -> int:
|
|||||||
if outcome.staging_dir:
|
if outcome.staging_dir:
|
||||||
print(f"[sleep] staged: {outcome.staging_dir}")
|
print(f"[sleep] staged: {outcome.staging_dir}")
|
||||||
if not outcome.adopted:
|
if not outcome.adopted:
|
||||||
print("[sleep] review it, then: python -m skillopt.sleep adopt")
|
print("[sleep] review it, then: python -m skillopt_sleep adopt")
|
||||||
if outcome.adopted:
|
if outcome.adopted:
|
||||||
print(f"[sleep] auto-adopted: {', '.join(outcome.adopted_paths)}")
|
print(f"[sleep] auto-adopted: {', '.join(outcome.adopted_paths)}")
|
||||||
return 0
|
return 0
|
||||||
@@ -164,7 +164,7 @@ def cmd_harvest(args) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def main(argv=None) -> int:
|
def main(argv=None) -> int:
|
||||||
parser = argparse.ArgumentParser(prog="skillopt.sleep", description="SkillOpt-Sleep nightly self-evolution")
|
parser = argparse.ArgumentParser(prog="skillopt_sleep", description="SkillOpt-Sleep nightly self-evolution")
|
||||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
p_run = sub.add_parser("run", help="run a full sleep cycle")
|
p_run = sub.add_parser("run", help="run a full sleep cycle")
|
||||||
@@ -26,7 +26,7 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from skillopt.sleep.types import EditRecord, ReplayResult, TaskRecord
|
from skillopt_sleep.types import EditRecord, ReplayResult, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
def skill_hash(content: str) -> str:
|
def skill_hash(content: str) -> str:
|
||||||
@@ -192,7 +192,7 @@ class MockBackend(Backend):
|
|||||||
|
|
||||||
def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
|
def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
|
||||||
if task.reference_kind == "rule" and task.judge:
|
if task.reference_kind == "rule" and task.judge:
|
||||||
from skillopt.sleep.judges import score_rule_judge
|
from skillopt_sleep.judges import score_rule_judge
|
||||||
return score_rule_judge(task.judge, response)
|
return score_rule_judge(task.judge, response)
|
||||||
if task.reference_kind == "exact" and task.reference:
|
if task.reference_kind == "exact" and task.reference:
|
||||||
hard = exact_score(task.reference, response)
|
hard = exact_score(task.reference, response)
|
||||||
@@ -303,7 +303,7 @@ class CliBackend(Backend):
|
|||||||
def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
|
def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
|
||||||
# gbrain-style rule judge: scored locally, no API spend
|
# gbrain-style rule judge: scored locally, no API spend
|
||||||
if task.reference_kind == "rule" and task.judge:
|
if task.reference_kind == "rule" and task.judge:
|
||||||
from skillopt.sleep.judges import score_rule_judge
|
from skillopt_sleep.judges import score_rule_judge
|
||||||
return score_rule_judge(task.judge, response)
|
return score_rule_judge(task.judge, response)
|
||||||
# exact references are scored locally — no API spend
|
# exact references are scored locally — no API spend
|
||||||
if task.reference_kind == "exact" and task.reference:
|
if task.reference_kind == "exact" and task.reference:
|
||||||
@@ -3,11 +3,8 @@
|
|||||||
This is the core that makes nightly evolution *safe*: it proposes bounded
|
This is the core that makes nightly evolution *safe*: it proposes bounded
|
||||||
edits from replayed failures, applies them to a candidate skill/memory, then
|
edits from replayed failures, applies them to a candidate skill/memory, then
|
||||||
**gates** the candidate on a held-out slice of the user's own tasks. Only a
|
**gates** the candidate on a held-out slice of the user's own tasks. Only a
|
||||||
candidate that strictly improves the held-out score is accepted — exactly the
|
candidate that strictly improves the held-out score is accepted — the SkillOpt
|
||||||
SkillOpt validation gate, reused verbatim from ``skillopt.evaluation.gate``.
|
validation gate, vendored self-contained in ``skillopt_sleep.gate``.
|
||||||
|
|
||||||
Reused from the main SkillOpt package (import-light, no `openai` needed):
|
|
||||||
* skillopt.evaluation.gate.evaluate_gate / select_gate_score
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -15,26 +12,16 @@ import os
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from skillopt.sleep.backend import Backend
|
from skillopt_sleep.backend import Backend
|
||||||
from skillopt.sleep.memory import apply_edits
|
from skillopt_sleep.memory import apply_edits
|
||||||
from skillopt.sleep.replay import aggregate_scores, replay_batch
|
from skillopt_sleep.replay import aggregate_scores, replay_batch
|
||||||
from skillopt.sleep.types import EditRecord, ReplayResult, TaskRecord
|
from skillopt_sleep.types import EditRecord, ReplayResult, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
# Reuse the real SkillOpt gate. This module imports cleanly without `openai`.
|
# Self-contained validation gate (vendored from SkillOpt; zero dependency on the
|
||||||
try:
|
# research package, so this open-source tool stays decoupled from the paper code).
|
||||||
from skillopt.evaluation.gate import evaluate_gate, select_gate_score
|
from skillopt_sleep.gate import evaluate_gate, select_gate_score
|
||||||
_HAVE_REPO_GATE = True
|
_HAVE_REPO_GATE = True
|
||||||
except Exception: # pragma: no cover - fallback keeps engine standalone
|
|
||||||
_HAVE_REPO_GATE = False
|
|
||||||
|
|
||||||
def select_gate_score(hard, soft, metric="hard", mixed_weight=0.5): # type: ignore
|
|
||||||
if metric == "hard":
|
|
||||||
return float(hard)
|
|
||||||
if metric == "soft":
|
|
||||||
return float(soft)
|
|
||||||
w = max(0.0, min(1.0, float(mixed_weight)))
|
|
||||||
return (1 - w) * float(hard) + w * float(soft)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -140,7 +127,7 @@ def consolidate(
|
|||||||
if rollouts_k > 1:
|
if rollouts_k > 1:
|
||||||
# multi-rollout contrastive reflection: run each train task K times
|
# multi-rollout contrastive reflection: run each train task K times
|
||||||
# and distill a rule from the good-vs-bad contrast (the "脑补" signal).
|
# and distill a rule from the good-vs-bad contrast (the "脑补" signal).
|
||||||
from skillopt.sleep.rollout import multi_rollout, contrastive_reflect
|
from skillopt_sleep.rollout import multi_rollout, contrastive_reflect
|
||||||
sets = [multi_rollout(backend, t, cand_skill, cand_memory, k=rollouts_k)
|
sets = [multi_rollout(backend, t, cand_skill, cand_memory, k=rollouts_k)
|
||||||
for t in train_tasks]
|
for t in train_tasks]
|
||||||
edits = contrastive_reflect(
|
edits = contrastive_reflect(
|
||||||
@@ -14,15 +14,15 @@ import time
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from skillopt.sleep.backend import get_backend
|
from skillopt_sleep.backend import get_backend
|
||||||
from skillopt.sleep.config import SleepConfig, load_config
|
from skillopt_sleep.config import SleepConfig, load_config
|
||||||
from skillopt.sleep.consolidate import consolidate
|
from skillopt_sleep.consolidate import consolidate
|
||||||
from skillopt.sleep.harvest import harvest
|
from skillopt_sleep.harvest import harvest
|
||||||
from skillopt.sleep.memory import ensure_skill_scaffold
|
from skillopt_sleep.memory import ensure_skill_scaffold
|
||||||
from skillopt.sleep.mine import mine
|
from skillopt_sleep.mine import mine
|
||||||
from skillopt.sleep.state import SleepState, _now_iso
|
from skillopt_sleep.state import SleepState, _now_iso
|
||||||
from skillopt.sleep.staging import write_staging, adopt as adopt_staging
|
from skillopt_sleep.staging import write_staging, adopt as adopt_staging
|
||||||
from skillopt.sleep.types import SessionDigest, SleepReport, TaskRecord
|
from skillopt_sleep.types import SessionDigest, SleepReport, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -131,7 +131,7 @@ def run_sleep_cycle(
|
|||||||
llm_miner = None
|
llm_miner = None
|
||||||
if cfg.get("backend", "mock") != "mock" and cfg.get("llm_mine", True):
|
if cfg.get("backend", "mock") != "mock" and cfg.get("llm_mine", True):
|
||||||
try:
|
try:
|
||||||
from skillopt.sleep.llm_miner import make_llm_miner
|
from skillopt_sleep.llm_miner import make_llm_miner
|
||||||
llm_miner = make_llm_miner(backend, max_tasks=cfg.get("max_tasks_per_night", 40))
|
llm_miner = make_llm_miner(backend, max_tasks=cfg.get("max_tasks_per_night", 40))
|
||||||
except Exception:
|
except Exception:
|
||||||
llm_miner = None
|
llm_miner = None
|
||||||
+2
-2
@@ -17,7 +17,7 @@ We map:
|
|||||||
judge -> TaskRecord.judge (+ reference_kind="rule")
|
judge -> TaskRecord.judge (+ reference_kind="rule")
|
||||||
|
|
||||||
This lets us reproduce gbrain's headline result with our engine and either the
|
This lets us reproduce gbrain's headline result with our engine and either the
|
||||||
claude or codex backend, scoring locally via skillopt.sleep.judges (no judge API).
|
claude or codex backend, scoring locally via skillopt_sleep.judges (no judge API).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from skillopt.sleep.types import TaskRecord
|
from skillopt_sleep.types import TaskRecord
|
||||||
|
|
||||||
|
|
||||||
SEED_DIRS = {
|
SEED_DIRS = {
|
||||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from skillopt.sleep.types import TaskRecord
|
from skillopt_sleep.types import TaskRecord
|
||||||
|
|
||||||
|
|
||||||
def _t(i, intent, ref, rule, project="/personas/demo", outcome="fail") -> TaskRecord:
|
def _t(i, intent, ref, rule, project="/personas/demo", outcome="fail") -> TaskRecord:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""SkillOpt-Sleep — turn a sweep JSONL into a presented Markdown scorecard.
|
"""SkillOpt-Sleep — turn a sweep JSONL into a presented Markdown scorecard.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python -m skillopt.sleep.experiments.report --in docs/sleep/sweep.jsonl \
|
python -m skillopt_sleep.experiments.report --in docs/sleep/sweep.jsonl \
|
||||||
--out docs/sleep/benchmark_report.md
|
--out docs/sleep/benchmark_report.md
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -101,9 +101,9 @@ def render(rows: List[Dict[str, Any]]) -> str:
|
|||||||
out.append("")
|
out.append("")
|
||||||
out.append("```bash")
|
out.append("```bash")
|
||||||
out.append("git clone https://github.com/garrytan/gbrain-evals /tmp/gbrain-evals")
|
out.append("git clone https://github.com/garrytan/gbrain-evals /tmp/gbrain-evals")
|
||||||
out.append("python -m skillopt.sleep.experiments.sweep --plan full \\")
|
out.append("python -m skillopt_sleep.experiments.sweep --plan full \\")
|
||||||
out.append(" --data-root /tmp/gbrain-evals/eval/data/skillopt-v1 --out docs/sleep/sweep.jsonl")
|
out.append(" --data-root /tmp/gbrain-evals/eval/data/skillopt-v1 --out docs/sleep/sweep.jsonl")
|
||||||
out.append("python -m skillopt.sleep.experiments.report \\")
|
out.append("python -m skillopt_sleep.experiments.report \\")
|
||||||
out.append(" --in docs/sleep/sweep.jsonl --out docs/sleep/benchmark_report.md")
|
out.append(" --in docs/sleep/sweep.jsonl --out docs/sleep/benchmark_report.md")
|
||||||
out.append("```")
|
out.append("```")
|
||||||
out.append("")
|
out.append("")
|
||||||
+11
-11
@@ -14,9 +14,9 @@ What it proves:
|
|||||||
the adopted artifact, re-scored, retains the lift.
|
the adopted artifact, re-scored, retains the lift.
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
python -m skillopt.sleep.experiments.run_experiment
|
python -m skillopt_sleep.experiments.run_experiment
|
||||||
python -m skillopt.sleep.experiments.run_experiment --persona programmer --nights 3
|
python -m skillopt_sleep.experiments.run_experiment --persona programmer --nights 3
|
||||||
python -m skillopt.sleep.experiments.run_experiment --backend anthropic # real lift
|
python -m skillopt_sleep.experiments.run_experiment --backend anthropic # real lift
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -27,21 +27,21 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from skillopt.sleep.backend import get_backend
|
from skillopt_sleep.backend import get_backend
|
||||||
from skillopt.sleep.consolidate import consolidate
|
from skillopt_sleep.consolidate import consolidate
|
||||||
from skillopt.sleep.experiments.personas import (
|
from skillopt_sleep.experiments.personas import (
|
||||||
PERSONAS,
|
PERSONAS,
|
||||||
harmful_edit_task,
|
harmful_edit_task,
|
||||||
researcher_persona,
|
researcher_persona,
|
||||||
)
|
)
|
||||||
from skillopt.sleep.memory import ensure_skill_scaffold
|
from skillopt_sleep.memory import ensure_skill_scaffold
|
||||||
from skillopt.sleep.replay import aggregate_scores, replay_batch
|
from skillopt_sleep.replay import aggregate_scores, replay_batch
|
||||||
from skillopt.sleep.types import TaskRecord
|
from skillopt_sleep.types import TaskRecord
|
||||||
|
|
||||||
|
|
||||||
def _score_holdout(backend, tasks: List[TaskRecord], skill: str, memory: str,
|
def _score_holdout(backend, tasks: List[TaskRecord], skill: str, memory: str,
|
||||||
metric: str = "mixed", w: float = 0.5) -> float:
|
metric: str = "mixed", w: float = 0.5) -> float:
|
||||||
from skillopt.sleep.consolidate import select_gate_score
|
from skillopt_sleep.consolidate import select_gate_score
|
||||||
# the persona experiment uses a 2-way split (train/val, no test); score on val
|
# the persona experiment uses a 2-way split (train/val, no test); score on val
|
||||||
holdout = [t for t in tasks if t.split in ("val", "holdout")] or tasks
|
holdout = [t for t in tasks if t.split in ("val", "holdout")] or tasks
|
||||||
pairs = replay_batch(backend, holdout, skill, memory)
|
pairs = replay_batch(backend, holdout, skill, memory)
|
||||||
@@ -52,7 +52,7 @@ def _score_holdout(backend, tasks: List[TaskRecord], skill: str, memory: str,
|
|||||||
def run(persona: str = "researcher", nights: int = 4, backend_name: str = "mock",
|
def run(persona: str = "researcher", nights: int = 4, backend_name: str = "mock",
|
||||||
edit_budget: int = 4, seed: int = 42, model: str = "", codex_path: str = "",
|
edit_budget: int = 4, seed: int = 42, model: str = "", codex_path: str = "",
|
||||||
limit_tasks: int = 0) -> dict:
|
limit_tasks: int = 0) -> dict:
|
||||||
from skillopt.sleep.mine import assign_splits
|
from skillopt_sleep.mine import assign_splits
|
||||||
|
|
||||||
make = PERSONAS.get(persona, researcher_persona)
|
make = PERSONAS.get(persona, researcher_persona)
|
||||||
items = make()
|
items = make()
|
||||||
@@ -13,9 +13,9 @@ Held-out scoring is done locally by the rule judge (no judge API). Only the
|
|||||||
agent's `attempt` (and the optimizer's `reflect`) spend tokens.
|
agent's `attempt` (and the optimizer's `reflect`) spend tokens.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python -m skillopt.sleep.experiments.run_gbrain --backend mock
|
python -m skillopt_sleep.experiments.run_gbrain --backend mock
|
||||||
python -m skillopt.sleep.experiments.run_gbrain --backend claude --seeds brief-writer --nights 2
|
python -m skillopt_sleep.experiments.run_gbrain --backend claude --seeds brief-writer --nights 2
|
||||||
python -m skillopt.sleep.experiments.run_gbrain --backend codex --data-root /tmp/gbrain-evals/eval/data/skillopt-v1
|
python -m skillopt_sleep.experiments.run_gbrain --backend codex --data-root /tmp/gbrain-evals/eval/data/skillopt-v1
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -24,14 +24,14 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from skillopt.sleep.backend import build_backend, get_backend
|
from skillopt_sleep.backend import build_backend, get_backend
|
||||||
from skillopt.sleep.consolidate import consolidate, select_gate_score
|
from skillopt_sleep.consolidate import consolidate, select_gate_score
|
||||||
from skillopt.sleep.experiments.gbrain_bench import (
|
from skillopt_sleep.experiments.gbrain_bench import (
|
||||||
available_seeds,
|
available_seeds,
|
||||||
find_data_root,
|
find_data_root,
|
||||||
load_seed,
|
load_seed,
|
||||||
)
|
)
|
||||||
from skillopt.sleep.replay import aggregate_scores, replay_batch
|
from skillopt_sleep.replay import aggregate_scores, replay_batch
|
||||||
|
|
||||||
|
|
||||||
def _score(backend, tasks, skill, memory, split="test", metric="mixed", w=0.5):
|
def _score(backend, tasks, skill, memory, split="test", metric="mixed", w=0.5):
|
||||||
@@ -95,7 +95,7 @@ def run_seed(backend, seed: str, skill: str, tasks: List, *,
|
|||||||
slow_text = None
|
slow_text = None
|
||||||
if nights >= 2 and slow_update:
|
if nights >= 2 and slow_update:
|
||||||
try:
|
try:
|
||||||
from skillopt.sleep.slow_update import run_slow_update, replace_slow_field
|
from skillopt_sleep.slow_update import run_slow_update, replace_slow_field
|
||||||
val_tasks = [t for t in tasks if t.split == "val"] or tasks
|
val_tasks = [t for t in tasks if t.split == "val"] or tasks
|
||||||
prev_pairs = replay_batch(backend, val_tasks, first_night_skill, memory)
|
prev_pairs = replay_batch(backend, val_tasks, first_night_skill, memory)
|
||||||
curr_pairs = replay_batch(backend, val_tasks, cur, memory)
|
curr_pairs = replay_batch(backend, val_tasks, cur, memory)
|
||||||
@@ -170,7 +170,7 @@ def main(argv=None) -> int:
|
|||||||
# budget auto-planning: derive nights x rollouts_k from a token budget
|
# budget auto-planning: derive nights x rollouts_k from a token budget
|
||||||
nights, rollouts_k = args.nights, args.rollouts_k
|
nights, rollouts_k = args.nights, args.rollouts_k
|
||||||
if args.budget_tokens:
|
if args.budget_tokens:
|
||||||
from skillopt.sleep.budget import Budget, plan_depth
|
from skillopt_sleep.budget import Budget, plan_depth
|
||||||
n_train = len([t for t in tasks if t.split == "train"]) or len(tasks)
|
n_train = len([t for t in tasks if t.split == "train"]) or len(tasks)
|
||||||
nights, rollouts_k = plan_depth(
|
nights, rollouts_k = plan_depth(
|
||||||
Budget(max_tokens=args.budget_tokens), n_tasks=n_train,
|
Budget(max_tokens=args.budget_tokens), n_tasks=n_train,
|
||||||
+5
-5
@@ -16,7 +16,7 @@ Protocol, per gbrain seed:
|
|||||||
Report baseline / direct / transferred, mirroring SkillOpt Table "transfer".
|
Report baseline / direct / transferred, mirroring SkillOpt Table "transfer".
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python -m skillopt.sleep.experiments.run_transfer \
|
python -m skillopt_sleep.experiments.run_transfer \
|
||||||
--source-backend claude --source-model haiku \
|
--source-backend claude --source-model haiku \
|
||||||
--target-backend claude --target-model sonnet \
|
--target-backend claude --target-model sonnet \
|
||||||
--seeds brief-writer --nights 2
|
--seeds brief-writer --nights 2
|
||||||
@@ -28,12 +28,12 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from skillopt.sleep.backend import get_backend
|
from skillopt_sleep.backend import get_backend
|
||||||
from skillopt.sleep.consolidate import consolidate, select_gate_score
|
from skillopt_sleep.consolidate import consolidate, select_gate_score
|
||||||
from skillopt.sleep.experiments.gbrain_bench import (
|
from skillopt_sleep.experiments.gbrain_bench import (
|
||||||
available_seeds, find_data_root, load_seed,
|
available_seeds, find_data_root, load_seed,
|
||||||
)
|
)
|
||||||
from skillopt.sleep.replay import aggregate_scores, replay_batch
|
from skillopt_sleep.replay import aggregate_scores, replay_batch
|
||||||
|
|
||||||
|
|
||||||
def _holdout_hard(backend, tasks, skill, memory="") -> float:
|
def _holdout_hard(backend, tasks, skill, memory="") -> float:
|
||||||
@@ -8,8 +8,8 @@ survive) and resume (skip configs whose row already exists).
|
|||||||
Then `report.py` turns the JSONL into a presented Markdown scorecard.
|
Then `report.py` turns the JSONL into a presented Markdown scorecard.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python -m skillopt.sleep.experiments.sweep --plan quick --out docs/sleep/sweep.jsonl
|
python -m skillopt_sleep.experiments.sweep --plan quick --out docs/sleep/sweep.jsonl
|
||||||
python -m skillopt.sleep.experiments.sweep --plan full --out docs/sleep/sweep.jsonl
|
python -m skillopt_sleep.experiments.sweep --plan full --out docs/sleep/sweep.jsonl
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -20,10 +20,10 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from skillopt.sleep.backend import build_backend, get_backend
|
from skillopt_sleep.backend import build_backend, get_backend
|
||||||
from skillopt.sleep.experiments.gbrain_bench import find_data_root, load_seed
|
from skillopt_sleep.experiments.gbrain_bench import find_data_root, load_seed
|
||||||
from skillopt.sleep.experiments.run_gbrain import run_seed as bench_seed
|
from skillopt_sleep.experiments.run_gbrain import run_seed as bench_seed
|
||||||
from skillopt.sleep.experiments.run_transfer import run_seed as transfer_seed
|
from skillopt_sleep.experiments.run_transfer import run_seed as transfer_seed
|
||||||
|
|
||||||
|
|
||||||
# Plans: lists of config dicts. Kept small per-run to bound cost/latency.
|
# Plans: lists of config dicts. Kept small per-run to bound cost/latency.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""SkillOpt-Sleep — vendored validation gate.
|
||||||
|
|
||||||
|
This is a self-contained copy of the SkillOpt validation gate so the sleep
|
||||||
|
engine has ZERO dependency on the research package (skillopt/*). The research
|
||||||
|
repo's ``skillopt.evaluation.gate`` is the reference implementation and the two
|
||||||
|
are kept behaviourally identical; vendoring keeps this open-source tool
|
||||||
|
decoupled from the paper's experiment code.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class GateResult:
|
||||||
|
action: str # "accept_new_best" | "accept" | "reject"
|
||||||
|
current_skill: str
|
||||||
|
current_score: float
|
||||||
|
best_skill: str
|
||||||
|
best_score: float
|
||||||
|
best_step: int
|
||||||
|
|
||||||
|
|
||||||
|
def select_gate_score(hard: float, soft: float, metric: str = "hard",
|
||||||
|
mixed_weight: float = 0.5) -> float:
|
||||||
|
"""Project (hard, soft) onto a single comparison metric."""
|
||||||
|
if metric == "hard":
|
||||||
|
return float(hard)
|
||||||
|
if metric == "soft":
|
||||||
|
return float(soft)
|
||||||
|
if metric == "mixed":
|
||||||
|
w = max(0.0, min(1.0, float(mixed_weight)))
|
||||||
|
return (1.0 - w) * float(hard) + w * float(soft)
|
||||||
|
raise ValueError(f"unknown gate metric {metric!r}; expected hard/soft/mixed")
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_gate(candidate_skill: str, cand_hard: float, current_skill: str,
|
||||||
|
current_score: float, best_skill: str, best_score: float,
|
||||||
|
best_step: int, global_step: int, *, cand_soft: float = 0.0,
|
||||||
|
metric: str = "hard", mixed_weight: float = 0.5) -> GateResult:
|
||||||
|
"""Pure gate decision: compare candidate score to current/best."""
|
||||||
|
cand_score = select_gate_score(cand_hard, cand_soft, metric, mixed_weight)
|
||||||
|
if cand_score > current_score:
|
||||||
|
if cand_score > best_score:
|
||||||
|
return GateResult("accept_new_best", candidate_skill, cand_score,
|
||||||
|
candidate_skill, cand_score, global_step)
|
||||||
|
return GateResult("accept", candidate_skill, cand_score,
|
||||||
|
best_skill, best_score, best_step)
|
||||||
|
return GateResult("reject", current_skill, current_score,
|
||||||
|
best_skill, best_score, best_step)
|
||||||
@@ -19,7 +19,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from typing import Any, Dict, Iterable, List, Optional
|
from typing import Any, Dict, Iterable, List, Optional
|
||||||
|
|
||||||
from skillopt.sleep.types import SessionDigest
|
from skillopt_sleep.types import SessionDigest
|
||||||
|
|
||||||
|
|
||||||
# Heuristic phrases that signal the user (dis)approving of prior output.
|
# Heuristic phrases that signal the user (dis)approving of prior output.
|
||||||
@@ -22,8 +22,8 @@ import json
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Any, Callable, Dict, List
|
||||||
|
|
||||||
from skillopt.sleep.backend import Backend, _extract_json
|
from skillopt_sleep.backend import Backend, _extract_json
|
||||||
from skillopt.sleep.types import SessionDigest, TaskRecord
|
from skillopt_sleep.types import SessionDigest, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
_MINER_PROMPT = """You are mining a user's past AI-assistant sessions to find RECURRING tasks
|
_MINER_PROMPT = """You are mining a user's past AI-assistant sessions to find RECURRING tasks
|
||||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from skillopt.sleep.types import EditRecord
|
from skillopt_sleep.types import EditRecord
|
||||||
|
|
||||||
|
|
||||||
LEARNED_START = "<!-- SKILLOPT-SLEEP:LEARNED START -->"
|
LEARNED_START = "<!-- SKILLOPT-SLEEP:LEARNED START -->"
|
||||||
@@ -18,7 +18,7 @@ import hashlib
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Callable, List, Optional
|
from typing import Any, Callable, List, Optional
|
||||||
|
|
||||||
from skillopt.sleep.types import SessionDigest, TaskRecord
|
from skillopt_sleep.types import SessionDigest, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
def _tid(project: str, intent: str) -> str:
|
def _tid(project: str, intent: str) -> str:
|
||||||
@@ -11,8 +11,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from skillopt.sleep.backend import Backend
|
from skillopt_sleep.backend import Backend
|
||||||
from skillopt.sleep.types import ReplayResult, TaskRecord
|
from skillopt_sleep.types import ReplayResult, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
def _required_tools(task: TaskRecord) -> List[str]:
|
def _required_tools(task: TaskRecord) -> List[str]:
|
||||||
@@ -44,7 +44,7 @@ def replay_one(backend: Backend, task: TaskRecord, skill: str, memory: str) -> R
|
|||||||
|
|
||||||
# rule judges may need the detected tool calls; score locally when possible
|
# rule judges may need the detected tool calls; score locally when possible
|
||||||
if task.reference_kind == "rule" and task.judge:
|
if task.reference_kind == "rule" and task.judge:
|
||||||
from skillopt.sleep.judges import score_rule_judge
|
from skillopt_sleep.judges import score_rule_judge
|
||||||
hard, soft, rationale = score_rule_judge(task.judge, response, tools_called)
|
hard, soft, rationale = score_rule_judge(task.judge, response, tools_called)
|
||||||
else:
|
else:
|
||||||
hard, soft, rationale = backend.judge(task, response)
|
hard, soft, rationale = backend.judge(task, response)
|
||||||
@@ -18,9 +18,9 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from skillopt.sleep.backend import Backend, _extract_json
|
from skillopt_sleep.backend import Backend, _extract_json
|
||||||
from skillopt.sleep.replay import replay_one
|
from skillopt_sleep.replay import replay_one
|
||||||
from skillopt.sleep.types import EditRecord, ReplayResult, TaskRecord
|
from skillopt_sleep.types import EditRecord, ReplayResult, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -23,8 +23,8 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from skillopt.sleep.backend import Backend, _extract_json
|
from skillopt_sleep.backend import Backend, _extract_json
|
||||||
from skillopt.sleep.types import ReplayResult, TaskRecord
|
from skillopt_sleep.types import ReplayResult, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
SLOW_UPDATE_START = "<!-- SLOW_UPDATE_START -->"
|
SLOW_UPDATE_START = "<!-- SLOW_UPDATE_START -->"
|
||||||
@@ -13,7 +13,7 @@ import shutil
|
|||||||
import time
|
import time
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from skillopt.sleep.types import SleepReport
|
from skillopt_sleep.types import SleepReport
|
||||||
|
|
||||||
|
|
||||||
def _ts_dir() -> str:
|
def _ts_dir() -> str:
|
||||||
@@ -18,7 +18,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
class SessionDigest:
|
class SessionDigest:
|
||||||
"""A normalized summary of one Claude Code session transcript.
|
"""A normalized summary of one Claude Code session transcript.
|
||||||
|
|
||||||
Produced by :mod:`skillopt.sleep.harvest` from a ``<sessionId>.jsonl``
|
Produced by :mod:`skillopt_sleep.harvest` from a ``<sessionId>.jsonl``
|
||||||
transcript plus ``history.jsonl`` entries.
|
transcript plus ``history.jsonl`` entries.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
+42
-42
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Pure-stdlib (unittest), deterministic, no API key, no third-party deps.
|
Pure-stdlib (unittest), deterministic, no API key, no third-party deps.
|
||||||
Run: python3.12 -m pytest tests/test_sleep_engine.py
|
Run: python3.12 -m pytest tests/test_sleep_engine.py
|
||||||
or: python3.12 -m unittest skillopt.sleep ... (see bottom)
|
or: python3.12 -m unittest skillopt_sleep ... (see bottom)
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -11,16 +11,16 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from skillopt.sleep.backend import MockBackend, exact_score, keyword_soft_score
|
from skillopt_sleep.backend import MockBackend, exact_score, keyword_soft_score
|
||||||
from skillopt.sleep.config import load_config
|
from skillopt_sleep.config import load_config
|
||||||
from skillopt.sleep.consolidate import consolidate
|
from skillopt_sleep.consolidate import consolidate
|
||||||
from skillopt.sleep.cycle import run_sleep_cycle
|
from skillopt_sleep.cycle import run_sleep_cycle
|
||||||
from skillopt.sleep.experiments.personas import researcher_persona, programmer_persona
|
from skillopt_sleep.experiments.personas import researcher_persona, programmer_persona
|
||||||
from skillopt.sleep.harvest import digest_transcript, _detect_feedback, _is_meta_prompt
|
from skillopt_sleep.harvest import digest_transcript, _detect_feedback, _is_meta_prompt
|
||||||
from skillopt.sleep.memory import apply_edits, current_learned_lines, extract_learned, set_learned
|
from skillopt_sleep.memory import apply_edits, current_learned_lines, extract_learned, set_learned
|
||||||
from skillopt.sleep.mine import assign_splits, heuristic_mine, dedup_tasks
|
from skillopt_sleep.mine import assign_splits, heuristic_mine, dedup_tasks
|
||||||
from skillopt.sleep.staging import adopt, latest_staging
|
from skillopt_sleep.staging import adopt, latest_staging
|
||||||
from skillopt.sleep.types import EditRecord, SessionDigest, TaskRecord
|
from skillopt_sleep.types import EditRecord, SessionDigest, TaskRecord
|
||||||
|
|
||||||
|
|
||||||
class TestScoring(unittest.TestCase):
|
class TestScoring(unittest.TestCase):
|
||||||
@@ -115,7 +115,7 @@ class TestMine(unittest.TestCase):
|
|||||||
|
|
||||||
def test_dream_never_in_val_or_test(self):
|
def test_dream_never_in_val_or_test(self):
|
||||||
# the anti-overfitting guarantee: origin='dream' tasks only ever land in train
|
# the anti-overfitting guarantee: origin='dream' tasks only ever land in train
|
||||||
from skillopt.sleep.types import TaskRecord
|
from skillopt_sleep.types import TaskRecord
|
||||||
real = researcher_persona()
|
real = researcher_persona()
|
||||||
dream = [TaskRecord(id=f"d{i}", project="/p", intent=f"dream {i}",
|
dream = [TaskRecord(id=f"d{i}", project="/p", intent=f"dream {i}",
|
||||||
origin="dream", derived_from="r0") for i in range(5)]
|
origin="dream", derived_from="r0") for i in range(5)]
|
||||||
@@ -152,7 +152,7 @@ class TestConsolidateGate(unittest.TestCase):
|
|||||||
|
|
||||||
class TestRuleJudge(unittest.TestCase):
|
class TestRuleJudge(unittest.TestCase):
|
||||||
def test_section_and_regex(self):
|
def test_section_and_regex(self):
|
||||||
from skillopt.sleep.judges import score_rule_judge
|
from skillopt_sleep.judges import score_rule_judge
|
||||||
j = {"kind": "rule", "checks": [
|
j = {"kind": "rule", "checks": [
|
||||||
{"op": "section_present", "arg": "Key Risks"},
|
{"op": "section_present", "arg": "Key Risks"},
|
||||||
{"op": "regex", "arg": r"[Cc]onfidence\s*[:=]"},
|
{"op": "regex", "arg": r"[Cc]onfidence\s*[:=]"},
|
||||||
@@ -162,13 +162,13 @@ class TestRuleJudge(unittest.TestCase):
|
|||||||
self.assertEqual(score_rule_judge(j, "just an answer")[0], 0.0)
|
self.assertEqual(score_rule_judge(j, "just an answer")[0], 0.0)
|
||||||
|
|
||||||
def test_max_chars(self):
|
def test_max_chars(self):
|
||||||
from skillopt.sleep.judges import score_rule_judge
|
from skillopt_sleep.judges import score_rule_judge
|
||||||
j = {"checks": [{"op": "max_chars", "arg": 50}]}
|
j = {"checks": [{"op": "max_chars", "arg": 50}]}
|
||||||
self.assertEqual(score_rule_judge(j, "x" * 10)[0], 1.0)
|
self.assertEqual(score_rule_judge(j, "x" * 10)[0], 1.0)
|
||||||
self.assertEqual(score_rule_judge(j, "x" * 100)[0], 0.0)
|
self.assertEqual(score_rule_judge(j, "x" * 100)[0], 0.0)
|
||||||
|
|
||||||
def test_partial_soft_score(self):
|
def test_partial_soft_score(self):
|
||||||
from skillopt.sleep.judges import score_rule_judge
|
from skillopt_sleep.judges import score_rule_judge
|
||||||
j = {"checks": [
|
j = {"checks": [
|
||||||
{"op": "contains", "arg": "alpha"},
|
{"op": "contains", "arg": "alpha"},
|
||||||
{"op": "contains", "arg": "beta"},
|
{"op": "contains", "arg": "beta"},
|
||||||
@@ -180,7 +180,7 @@ class TestRuleJudge(unittest.TestCase):
|
|||||||
|
|
||||||
class TestGbrainLoader(unittest.TestCase):
|
class TestGbrainLoader(unittest.TestCase):
|
||||||
def test_loads_when_present(self):
|
def test_loads_when_present(self):
|
||||||
from skillopt.sleep.experiments.gbrain_bench import find_data_root, load_seed
|
from skillopt_sleep.experiments.gbrain_bench import find_data_root, load_seed
|
||||||
root = find_data_root()
|
root = find_data_root()
|
||||||
if not root:
|
if not root:
|
||||||
self.skipTest("gbrain-evals data not present")
|
self.skipTest("gbrain-evals data not present")
|
||||||
@@ -191,7 +191,7 @@ class TestGbrainLoader(unittest.TestCase):
|
|||||||
self.assertTrue(any(t.split == "val" for t in tasks))
|
self.assertTrue(any(t.split == "val" for t in tasks))
|
||||||
self.assertTrue(all(t.reference_kind == "rule" for t in tasks))
|
self.assertTrue(all(t.reference_kind == "rule" for t in tasks))
|
||||||
# the deficient skill must FAIL its own held-out (test) checks (baseline 0)
|
# the deficient skill must FAIL its own held-out (test) checks (baseline 0)
|
||||||
from skillopt.sleep.judges import score_rule_judge
|
from skillopt_sleep.judges import score_rule_judge
|
||||||
ho = [t for t in tasks if t.split == "test"][0]
|
ho = [t for t in tasks if t.split == "test"][0]
|
||||||
self.assertEqual(score_rule_judge(ho.judge, skill)[0], 0.0)
|
self.assertEqual(score_rule_judge(ho.judge, skill)[0], 0.0)
|
||||||
|
|
||||||
@@ -199,8 +199,8 @@ class TestGbrainLoader(unittest.TestCase):
|
|||||||
class TestLlmMiner(unittest.TestCase):
|
class TestLlmMiner(unittest.TestCase):
|
||||||
def test_miner_emits_checkable_tasks(self):
|
def test_miner_emits_checkable_tasks(self):
|
||||||
# a stub backend whose _call returns canned miner JSON => deterministic
|
# a stub backend whose _call returns canned miner JSON => deterministic
|
||||||
from skillopt.sleep.backend import Backend
|
from skillopt_sleep.backend import Backend
|
||||||
from skillopt.sleep.llm_miner import make_llm_miner
|
from skillopt_sleep.llm_miner import make_llm_miner
|
||||||
|
|
||||||
class StubBackend(Backend):
|
class StubBackend(Backend):
|
||||||
name = "stub"
|
name = "stub"
|
||||||
@@ -219,8 +219,8 @@ class TestLlmMiner(unittest.TestCase):
|
|||||||
self.assertEqual(tasks[0].judge["checks"][0]["op"], "section_present")
|
self.assertEqual(tasks[0].judge["checks"][0]["op"], "section_present")
|
||||||
|
|
||||||
def test_miner_drops_uncheckable(self):
|
def test_miner_drops_uncheckable(self):
|
||||||
from skillopt.sleep.backend import Backend
|
from skillopt_sleep.backend import Backend
|
||||||
from skillopt.sleep.llm_miner import make_llm_miner
|
from skillopt_sleep.llm_miner import make_llm_miner
|
||||||
|
|
||||||
class EmptyBackend(Backend):
|
class EmptyBackend(Backend):
|
||||||
name = "stub"
|
name = "stub"
|
||||||
@@ -234,8 +234,8 @@ class TestLlmMiner(unittest.TestCase):
|
|||||||
|
|
||||||
class TestMultiObjectiveAndPrefs(unittest.TestCase):
|
class TestMultiObjectiveAndPrefs(unittest.TestCase):
|
||||||
def test_multi_objective_reward(self):
|
def test_multi_objective_reward(self):
|
||||||
from skillopt.sleep.replay import multi_objective_reward
|
from skillopt_sleep.replay import multi_objective_reward
|
||||||
from skillopt.sleep.types import ReplayResult, TaskRecord
|
from skillopt_sleep.types import ReplayResult, TaskRecord
|
||||||
t = TaskRecord(id="t", project="/p", intent="x")
|
t = TaskRecord(id="t", project="/p", intent="x")
|
||||||
expensive = [(t, ReplayResult(id="t", hard=1.0, tokens=4000, latency_ms=20000))]
|
expensive = [(t, ReplayResult(id="t", hard=1.0, tokens=4000, latency_ms=20000))]
|
||||||
cheap = [(t, ReplayResult(id="t", hard=1.0, tokens=200, latency_ms=1000))]
|
cheap = [(t, ReplayResult(id="t", hard=1.0, tokens=200, latency_ms=1000))]
|
||||||
@@ -248,8 +248,8 @@ class TestMultiObjectiveAndPrefs(unittest.TestCase):
|
|||||||
self.assertGreater(rc, re)
|
self.assertGreater(rc, re)
|
||||||
|
|
||||||
def test_preferences_injected_into_reflect(self):
|
def test_preferences_injected_into_reflect(self):
|
||||||
from skillopt.sleep.backend import CliBackend
|
from skillopt_sleep.backend import CliBackend
|
||||||
from skillopt.sleep.types import TaskRecord, ReplayResult
|
from skillopt_sleep.types import TaskRecord, ReplayResult
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
class CapBackend(CliBackend):
|
class CapBackend(CliBackend):
|
||||||
@@ -267,9 +267,9 @@ class TestMultiObjectiveAndPrefs(unittest.TestCase):
|
|||||||
self.assertIn("British English", captured["prompt"])
|
self.assertIn("British English", captured["prompt"])
|
||||||
|
|
||||||
def test_replay_records_cost(self):
|
def test_replay_records_cost(self):
|
||||||
from skillopt.sleep.backend import MockBackend
|
from skillopt_sleep.backend import MockBackend
|
||||||
from skillopt.sleep.replay import replay_one
|
from skillopt_sleep.replay import replay_one
|
||||||
from skillopt.sleep.types import TaskRecord
|
from skillopt_sleep.types import TaskRecord
|
||||||
t = TaskRecord(id="t", project="/p", intent="hello world",
|
t = TaskRecord(id="t", project="/p", intent="hello world",
|
||||||
reference_kind="exact", reference="hi")
|
reference_kind="exact", reference="hi")
|
||||||
r = replay_one(MockBackend(), t, "some skill text", "")
|
r = replay_one(MockBackend(), t, "some skill text", "")
|
||||||
@@ -279,8 +279,8 @@ class TestMultiObjectiveAndPrefs(unittest.TestCase):
|
|||||||
|
|
||||||
class TestMultiRolloutAndBudget(unittest.TestCase):
|
class TestMultiRolloutAndBudget(unittest.TestCase):
|
||||||
def test_rolloutset_stats(self):
|
def test_rolloutset_stats(self):
|
||||||
from skillopt.sleep.rollout import RolloutSet
|
from skillopt_sleep.rollout import RolloutSet
|
||||||
from skillopt.sleep.types import ReplayResult, TaskRecord
|
from skillopt_sleep.types import ReplayResult, TaskRecord
|
||||||
rs = RolloutSet(task=TaskRecord(id="t", project="/p", intent="x"),
|
rs = RolloutSet(task=TaskRecord(id="t", project="/p", intent="x"),
|
||||||
attempts=[ReplayResult(id="t", hard=1.0),
|
attempts=[ReplayResult(id="t", hard=1.0),
|
||||||
ReplayResult(id="t", hard=0.0),
|
ReplayResult(id="t", hard=0.0),
|
||||||
@@ -291,7 +291,7 @@ class TestMultiRolloutAndBudget(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(rs.pass_rate, 2 / 3)
|
self.assertAlmostEqual(rs.pass_rate, 2 / 3)
|
||||||
|
|
||||||
def test_budget_exhaustion_and_plan(self):
|
def test_budget_exhaustion_and_plan(self):
|
||||||
from skillopt.sleep.budget import Budget, plan_depth
|
from skillopt_sleep.budget import Budget, plan_depth
|
||||||
clock = [0.0]
|
clock = [0.0]
|
||||||
b = Budget(max_tokens=1000)
|
b = Budget(max_tokens=1000)
|
||||||
b.start(lambda: clock[0], tokens_now=0)
|
b.start(lambda: clock[0], tokens_now=0)
|
||||||
@@ -303,9 +303,9 @@ class TestMultiRolloutAndBudget(unittest.TestCase):
|
|||||||
self.assertGreaterEqual(k, 1)
|
self.assertGreaterEqual(k, 1)
|
||||||
|
|
||||||
def test_contrastive_reflect_with_stub(self):
|
def test_contrastive_reflect_with_stub(self):
|
||||||
from skillopt.sleep.backend import Backend
|
from skillopt_sleep.backend import Backend
|
||||||
from skillopt.sleep.rollout import RolloutSet, contrastive_reflect
|
from skillopt_sleep.rollout import RolloutSet, contrastive_reflect
|
||||||
from skillopt.sleep.types import ReplayResult, TaskRecord
|
from skillopt_sleep.types import ReplayResult, TaskRecord
|
||||||
|
|
||||||
class StubBackend(Backend):
|
class StubBackend(Backend):
|
||||||
name = "stub"
|
name = "stub"
|
||||||
@@ -322,7 +322,7 @@ class TestMultiRolloutAndBudget(unittest.TestCase):
|
|||||||
|
|
||||||
class TestSlowUpdate(unittest.TestCase):
|
class TestSlowUpdate(unittest.TestCase):
|
||||||
def test_protected_field_roundtrip(self):
|
def test_protected_field_roundtrip(self):
|
||||||
from skillopt.sleep.slow_update import (
|
from skillopt_sleep.slow_update import (
|
||||||
replace_slow_field, extract_slow_field, has_slow_field,
|
replace_slow_field, extract_slow_field, has_slow_field,
|
||||||
SLOW_UPDATE_START, SLOW_UPDATE_END,
|
SLOW_UPDATE_START, SLOW_UPDATE_END,
|
||||||
)
|
)
|
||||||
@@ -339,9 +339,9 @@ class TestSlowUpdate(unittest.TestCase):
|
|||||||
self.assertIn("keep me", doc2)
|
self.assertIn("keep me", doc2)
|
||||||
|
|
||||||
def test_run_slow_update_with_stub_backend(self):
|
def test_run_slow_update_with_stub_backend(self):
|
||||||
from skillopt.sleep.backend import Backend
|
from skillopt_sleep.backend import Backend
|
||||||
from skillopt.sleep.slow_update import run_slow_update
|
from skillopt_sleep.slow_update import run_slow_update
|
||||||
from skillopt.sleep.types import TaskRecord, ReplayResult
|
from skillopt_sleep.types import TaskRecord, ReplayResult
|
||||||
|
|
||||||
class StubBackend(Backend):
|
class StubBackend(Backend):
|
||||||
name = "stub"
|
name = "stub"
|
||||||
@@ -365,10 +365,10 @@ class TestSlowUpdate(unittest.TestCase):
|
|||||||
|
|
||||||
class TestToolLoop(unittest.TestCase):
|
class TestToolLoop(unittest.TestCase):
|
||||||
def test_tool_called_judge_via_replay(self):
|
def test_tool_called_judge_via_replay(self):
|
||||||
from skillopt.sleep.backend import MockBackend
|
from skillopt_sleep.backend import MockBackend
|
||||||
from skillopt.sleep.replay import replay_one, _required_tools
|
from skillopt_sleep.replay import replay_one, _required_tools
|
||||||
from skillopt.sleep.memory import set_learned
|
from skillopt_sleep.memory import set_learned
|
||||||
from skillopt.sleep.types import TaskRecord
|
from skillopt_sleep.types import TaskRecord
|
||||||
|
|
||||||
task = TaskRecord(
|
task = TaskRecord(
|
||||||
id="qa1", project="/p", intent="answer the question",
|
id="qa1", project="/p", intent="answer the question",
|
||||||
|
|||||||
Reference in New Issue
Block a user