tao-shen's picture
feat: add HF Dataset persistence + auto-register HuggingClaw agents
123e7c1
Raw
History Blame Contribute Delete
7.17 kB
#!/usr/bin/env python3
"""HuggingFace Dataset persistence for Mission Control .data/ directory.
On startup: restore .data/ from HF Dataset
Periodically: upload .data/ to HF Dataset (every SYNC_INTERVAL seconds)
On shutdown: final upload
Env vars:
HF_TOKEN - HuggingFace API token (required)
HF_DATASET_REPO - Dataset repo ID (default: auto from SPACE_ID)
SYNC_INTERVAL - Seconds between syncs (default: 120)
"""
import os, sys, signal, time, subprocess, threading, logging
logging.basicConfig(level=logging.INFO, format="[hf-persist] %(message)s")
log = logging.getLogger(__name__)
HF_TOKEN = os.environ.get("HF_TOKEN", "")
SPACE_ID = os.environ.get("SPACE_ID", "")
DATA_DIR = os.environ.get("MISSION_CONTROL_DATA_DIR", "/app/.data")
DATASET_PATH = "mission-control"
SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "120"))
# Derive dataset repo from SPACE_ID (e.g. tao-shen/HuggingClaw-MissionControl -> tao-shen/HuggingClaw-MissionControl-data)
if os.environ.get("HF_DATASET_REPO"):
HF_REPO_ID = os.environ["HF_DATASET_REPO"]
elif SPACE_ID:
HF_REPO_ID = f"{SPACE_ID}-data"
else:
HF_REPO_ID = ""
IGNORE_PATTERNS = ["*.lock", "*.tmp", "*.pid", "__pycache__", "*.wal", "*.shm"]
def ensure_dataset():
"""Create dataset repo if it doesn't exist."""
from huggingface_hub import HfApi
api = HfApi(token=HF_TOKEN)
try:
api.repo_info(repo_id=HF_REPO_ID, repo_type="dataset")
log.info(f"Dataset {HF_REPO_ID} exists")
except Exception:
log.info(f"Creating dataset {HF_REPO_ID}")
api.create_repo(repo_id=HF_REPO_ID, repo_type="dataset", private=True)
log.info(f"Created dataset {HF_REPO_ID}")
def restore():
"""Download .data/ from HF Dataset on startup."""
from huggingface_hub import snapshot_download
import tempfile, shutil
log.info(f"Restoring from {HF_REPO_ID}/{DATASET_PATH}/ ...")
try:
with tempfile.TemporaryDirectory() as tmpdir:
snapshot_download(
repo_id=HF_REPO_ID,
repo_type="dataset",
allow_patterns=f"{DATASET_PATH}/**",
local_dir=tmpdir,
token=HF_TOKEN,
)
src = os.path.join(tmpdir, DATASET_PATH)
if os.path.isdir(src):
count = 0
for root, dirs, files in os.walk(src):
for f in files:
src_file = os.path.join(root, f)
rel = os.path.relpath(src_file, src)
dst_file = os.path.join(DATA_DIR, rel)
os.makedirs(os.path.dirname(dst_file), exist_ok=True)
shutil.copy2(src_file, dst_file)
count += 1
log.info(f"Restored {count} files to {DATA_DIR}")
else:
log.info("No data found in dataset (fresh start)")
except Exception as e:
log.warning(f"Restore failed (starting fresh): {e}")
def save():
"""Upload .data/ to HF Dataset."""
from huggingface_hub import HfApi
from datetime import datetime
if not os.path.isdir(DATA_DIR) or not os.listdir(DATA_DIR):
log.info("Nothing to save (empty .data/)")
return
api = HfApi(token=HF_TOKEN)
try:
api.upload_folder(
folder_path=DATA_DIR,
path_in_repo=DATASET_PATH,
repo_id=HF_REPO_ID,
repo_type="dataset",
commit_message=f"Sync mission-control data — {datetime.now().isoformat()}",
ignore_patterns=IGNORE_PATTERNS,
)
log.info("Saved to dataset")
except Exception as e:
log.error(f"Save failed: {e}")
def register_agents(port):
"""Register HuggingClaw agents after server is ready."""
import urllib.request, urllib.error, json
agents = [
{"name": "Adam", "role": "agent", "capabilities": ["parenting", "coding", "A2A"], "framework": "openclaw"},
{"name": "Eve", "role": "agent", "capabilities": ["parenting", "coding", "A2A"], "framework": "openclaw"},
{"name": "God", "role": "agent", "capabilities": ["supervision", "architecture", "A2A"], "framework": "openclaw"},
{"name": "Cain", "role": "agent", "capabilities": ["learning", "coding"], "framework": "openclaw"},
]
# Wait for server to be ready
for i in range(30):
try:
req = urllib.request.Request(f"http://localhost:{port}/api/status?action=health")
urllib.request.urlopen(req, timeout=2)
break
except Exception:
time.sleep(2)
else:
log.warning("Server not ready after 60s, skipping agent registration")
return
api_key = os.environ.get("API_KEY", "")
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
for agent in agents:
try:
data = json.dumps(agent).encode()
req = urllib.request.Request(
f"http://localhost:{port}/api/agents/register",
data=data,
headers=headers,
method="POST",
)
resp = urllib.request.urlopen(req, timeout=10)
log.info(f"Registered agent: {agent['name']} ({resp.status})")
except urllib.error.HTTPError as e:
body = e.read().decode()
log.warning(f"Failed to register {agent['name']}: {e.code} {body}")
except Exception as e:
log.warning(f"Failed to register {agent['name']}: {e}")
def main():
if not HF_TOKEN or not HF_REPO_ID:
log.info("HF_TOKEN or HF_DATASET_REPO not set — persistence disabled, starting server directly")
os.execvp("node", ["node", "server.js"])
return
port = os.environ.get("PORT", "7860")
# Step 1: Ensure dataset exists
ensure_dataset()
# Step 2: Restore from dataset
restore()
# Step 3: Start server as subprocess
proc = subprocess.Popen(["node", "server.js"], cwd="/app")
log.info(f"Server started (PID {proc.pid})")
# Step 4: Register agents in background
reg_thread = threading.Thread(target=register_agents, args=(port,), daemon=True)
reg_thread.start()
# Step 5: Periodic sync
stop_event = threading.Event()
def sync_loop():
while not stop_event.is_set():
stop_event.wait(SYNC_INTERVAL)
if not stop_event.is_set():
save()
sync_thread = threading.Thread(target=sync_loop, daemon=True)
sync_thread.start()
# Step 6: Handle shutdown
def shutdown(signum, frame):
log.info(f"Received signal {signum}, shutting down...")
stop_event.set()
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
save() # Final save
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
# Wait for server process
ret = proc.wait()
stop_event.set()
save() # Final save
sys.exit(ret)
if __name__ == "__main__":
main()