mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-07-14 07:57:21 +02:00
04b8ec60cb
New core modules: - auto_obliterate.py: Automated multi-iteration obliteration pipeline - watchtower.py: HF Hub model discovery and tracking - ui_watchtower.py: Gradio tabs for Watchtower (ready for app.py wiring) - hard_negative.py: Residue mining from refusal audits - model_profile.py: Parameter profiling from safetensors/config - bestiary_sync.py: Sync models from PlinyOS BESTIARY registry - models_client.py: Lightweight HF model list client Framework enhancements: - abliterate.py: ASPA source-tethering, step gradient blending, hard-negative residue support - cli.py: self-improve command, model profiling, hard-negative flags - prompts.py: Expanded 842-prompt refusal eval corpus across 10 categories - __init__.py: New exports (Watchtower, AutoObliterator) Reference implementations (14 scripts): - ASPA sweep, gradient search, coherence eval, MMLU benchmarks - Pareto controller, refusal sniper, stock comparisons Documentation: - README: Research framing, responsible use section, comprehensive disclaimer - docs/beyond_sota_roadmap.md, docs/recursive_self_improvement.md Tests: 4 new test files (354 lines) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
529 lines
20 KiB
Python
529 lines
20 KiB
Python
"""Watchtower — Scan HuggingFace for new popular open-weight models.
|
|
|
|
Continuously monitors the HuggingFace Hub for instruction-tuned,
|
|
open-license text-generation models that are gaining traction. Tracks
|
|
which models have been seen, queued, and obliterated in a persistent
|
|
JSON state file (~/.obliteratus/watchtower_state.json).
|
|
|
|
Usage:
|
|
from obliteratus.watchtower import Watchtower
|
|
|
|
wt = Watchtower()
|
|
new_models = wt.scan() # returns list of newly discovered models
|
|
trending = wt.get_trending() # returns current hot models sorted by downloads
|
|
wt.start_scheduler(interval=3600) # background hourly scan
|
|
wt.stop_scheduler()
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import threading
|
|
import time
|
|
from dataclasses import dataclass, field, asdict
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── Constants ─────────────────────────────────────────────────────────
|
|
|
|
STATE_DIR = Path.home() / ".obliteratus"
|
|
STATE_FILE = STATE_DIR / "watchtower_state.json"
|
|
|
|
# Minimum downloads in last 7 days to qualify as "popular"
|
|
MIN_RECENT_DOWNLOADS = 1000
|
|
|
|
# Open-weight licenses we care about (lowercase, prefix-matched)
|
|
OPEN_LICENSES = {
|
|
"apache-2.0", "mit", "bsd-2-clause", "bsd-3-clause",
|
|
"llama2", "llama3", "llama3.1", "llama3.2", "llama3.3", "llama4",
|
|
"gemma", "qwen", "deepseek",
|
|
"cc-by-4.0", "cc-by-sa-4.0", "cc-by-nc-4.0",
|
|
"openrail", "openrail++", "bigscience-openrail-m",
|
|
"artistic-2.0", "wtfpl", "unlicense", "zlib",
|
|
"other", # many open models use "other" + a permissive custom license
|
|
}
|
|
|
|
# Keywords that indicate instruction/chat tuning
|
|
INSTRUCT_KEYWORDS = {
|
|
"instruct", "chat", "it", "rlhf", "dpo", "sft",
|
|
"aligned", "conversational", "assistant", "dialogue",
|
|
}
|
|
|
|
# ── Organizations to watch ────────────────────────────────────────────
|
|
|
|
MAJOR_LABS = {
|
|
"google", "meta-llama", "mistralai", "Qwen", "deepseek-ai",
|
|
"microsoft", "nvidia", "01-ai", "internlm", "THUDM",
|
|
"tiiuae", "CohereForAI", "allenai", "openai", "openai-community",
|
|
"moonshotai", "openbmb", "stabilityai", "stepfun-ai", "zai-org",
|
|
"MiniMaxAI",
|
|
}
|
|
|
|
MEDIUM_LABS = {
|
|
"teknium", "NousResearch", "OpenBuddy", "lmsys",
|
|
"HuggingFaceH4", "mosaicml", "bigcode",
|
|
"cognitivecomputations", "mlabonne", "huihui-ai",
|
|
"Orenguteng", "WhiteRabbitNeo",
|
|
}
|
|
|
|
ALL_WATCHED_ORGS = MAJOR_LABS | MEDIUM_LABS
|
|
|
|
|
|
# ── Data classes ──────────────────────────────────────────────────────
|
|
|
|
@dataclass
|
|
class DiscoveredModel:
|
|
"""A model discovered by the Watchtower."""
|
|
model_id: str # e.g. "meta-llama/Llama-3.1-8B-Instruct"
|
|
name: str # short display name
|
|
org: str # organization
|
|
downloads_7d: int = 0 # downloads in last 7 days
|
|
total_downloads: int = 0 # all-time downloads
|
|
likes: int = 0 # HF likes
|
|
size_category: str = "" # e.g. "7B", "13B", "70B"
|
|
license: str = "" # license identifier
|
|
pipeline_tag: str = "" # should be "text-generation"
|
|
discovered_at: str = "" # ISO timestamp
|
|
status: str = "new" # new | queued | obliterating | obliterated | failed
|
|
obliteration_metrics: dict = field(default_factory=dict)
|
|
last_updated: str = "" # ISO timestamp of last status change
|
|
|
|
def to_dict(self) -> dict:
|
|
return asdict(self)
|
|
|
|
@classmethod
|
|
def from_dict(cls, d: dict) -> "DiscoveredModel":
|
|
# Handle extra/missing keys gracefully
|
|
known = {f.name for f in cls.__dataclass_fields__.values()}
|
|
return cls(**{k: v for k, v in d.items() if k in known})
|
|
|
|
|
|
# ── Watchtower class ──────────────────────────────────────────────────
|
|
|
|
class Watchtower:
|
|
"""Scans HuggingFace Hub for new popular open-weight instruction models."""
|
|
|
|
def __init__(self, state_file: Path | str | None = None):
|
|
self.state_file = Path(state_file) if state_file else STATE_FILE
|
|
self._models: dict[str, DiscoveredModel] = {}
|
|
self._last_scan: str | None = None
|
|
self._scan_count: int = 0
|
|
self._lock = threading.Lock()
|
|
self._scheduler_thread: threading.Thread | None = None
|
|
self._scheduler_stop = threading.Event()
|
|
self._on_new_model_callbacks: list = []
|
|
|
|
# Load persisted state
|
|
self._load_state()
|
|
|
|
# ── Persistence ───────────────────────────────────────────────────
|
|
|
|
def _load_state(self):
|
|
"""Load watchtower state from disk."""
|
|
try:
|
|
if self.state_file.exists():
|
|
data = json.loads(self.state_file.read_text(encoding="utf-8"))
|
|
self._last_scan = data.get("last_scan")
|
|
self._scan_count = data.get("scan_count", 0)
|
|
for mid, mdata in data.get("models", {}).items():
|
|
self._models[mid] = DiscoveredModel.from_dict(mdata)
|
|
logger.info(
|
|
"Watchtower: loaded %d models from %s",
|
|
len(self._models), self.state_file,
|
|
)
|
|
except Exception as e:
|
|
logger.warning("Watchtower: failed to load state: %s", e)
|
|
|
|
def _save_state(self):
|
|
"""Persist watchtower state to disk."""
|
|
try:
|
|
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
data = {
|
|
"last_scan": self._last_scan,
|
|
"scan_count": self._scan_count,
|
|
"models": {mid: m.to_dict() for mid, m in self._models.items()},
|
|
}
|
|
# Atomic write via temp file
|
|
tmp = self.state_file.with_suffix(".tmp")
|
|
tmp.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
|
|
tmp.replace(self.state_file)
|
|
except Exception as e:
|
|
logger.warning("Watchtower: failed to save state: %s", e)
|
|
|
|
# ── HuggingFace API helpers ───────────────────────────────────────
|
|
|
|
@staticmethod
|
|
def _fetch_models_from_hf(
|
|
*,
|
|
orgs: set[str] | None = None,
|
|
limit_per_org: int = 50,
|
|
min_downloads: int = MIN_RECENT_DOWNLOADS,
|
|
) -> list[dict[str, Any]]:
|
|
"""Fetch model metadata from HuggingFace Hub API.
|
|
|
|
Returns a list of raw model info dicts. Gracefully returns []
|
|
if the API is unreachable.
|
|
"""
|
|
try:
|
|
from huggingface_hub import HfApi, ModelFilter
|
|
except ImportError:
|
|
logger.error("huggingface_hub not installed — cannot scan HF")
|
|
return []
|
|
|
|
api = HfApi()
|
|
results = []
|
|
search_orgs = orgs or ALL_WATCHED_ORGS
|
|
|
|
for org in search_orgs:
|
|
try:
|
|
models = api.list_models(
|
|
author=org,
|
|
pipeline_tag="text-generation",
|
|
sort="downloads",
|
|
direction=-1,
|
|
limit=limit_per_org,
|
|
)
|
|
for m in models:
|
|
results.append(m)
|
|
except Exception as e:
|
|
logger.debug("Watchtower: error scanning org '%s': %s", org, e)
|
|
continue
|
|
|
|
return results
|
|
|
|
@staticmethod
|
|
def _is_instruction_tuned(model_id: str, tags: list[str] | None = None) -> bool:
|
|
"""Heuristic check if a model is instruction/chat tuned."""
|
|
name_lower = model_id.lower()
|
|
# Check model name
|
|
for kw in INSTRUCT_KEYWORDS:
|
|
if kw in name_lower:
|
|
return True
|
|
# Check tags
|
|
if tags:
|
|
tags_lower = {t.lower() for t in tags}
|
|
for kw in INSTRUCT_KEYWORDS:
|
|
if kw in tags_lower:
|
|
return True
|
|
# Explicit tag checks
|
|
if "conversational" in tags_lower:
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def _has_open_license(license_id: str | None) -> bool:
|
|
"""Check if the license is considered open-weight."""
|
|
if not license_id:
|
|
return False
|
|
lid = license_id.lower().strip()
|
|
for allowed in OPEN_LICENSES:
|
|
if lid == allowed or lid.startswith(allowed):
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def _estimate_size(model_id: str, config: dict | None = None) -> str:
|
|
"""Estimate model size from the name or config."""
|
|
name = model_id.lower()
|
|
# Try to extract a size like "7b", "70b", "1.5b", "397b"
|
|
import re
|
|
match = re.search(r'(\d+\.?\d*)\s*[bB]', model_id)
|
|
if match:
|
|
size = float(match.group(1))
|
|
if size >= 1:
|
|
return f"{match.group(1)}B"
|
|
else:
|
|
return f"{size * 1000:.0f}M"
|
|
return "unknown"
|
|
|
|
# ── Core scan logic ───────────────────────────────────────────────
|
|
|
|
def scan(self, on_log=None) -> list[DiscoveredModel]:
|
|
"""Scan HuggingFace for new popular open-weight instruction models.
|
|
|
|
Returns a list of *newly discovered* models (not previously seen).
|
|
Thread-safe.
|
|
"""
|
|
def _log(msg):
|
|
logger.info(msg)
|
|
if on_log:
|
|
on_log(msg)
|
|
|
|
_log("Watchtower: starting scan...")
|
|
new_models = []
|
|
|
|
try:
|
|
raw_models = self._fetch_models_from_hf()
|
|
except Exception as e:
|
|
_log(f"Watchtower: HF API error — {e}")
|
|
return []
|
|
|
|
_log(f"Watchtower: fetched {len(raw_models)} candidate models from HF Hub")
|
|
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
processed = 0
|
|
|
|
for m in raw_models:
|
|
model_id = getattr(m, "id", None) or getattr(m, "modelId", "")
|
|
if not model_id:
|
|
continue
|
|
|
|
# Extract metadata safely
|
|
downloads = getattr(m, "downloads", 0) or 0
|
|
likes = getattr(m, "likes", 0) or 0
|
|
tags = getattr(m, "tags", []) or []
|
|
license_id = getattr(m, "license", None) or ""
|
|
# Some models store license in tags
|
|
if not license_id:
|
|
for t in tags:
|
|
if t.startswith("license:"):
|
|
license_id = t.split(":", 1)[1]
|
|
break
|
|
|
|
pipeline_tag = getattr(m, "pipeline_tag", "") or ""
|
|
|
|
# Filter: must be text-generation
|
|
if pipeline_tag and pipeline_tag != "text-generation":
|
|
continue
|
|
|
|
# Filter: minimum popularity
|
|
if downloads < MIN_RECENT_DOWNLOADS:
|
|
continue
|
|
|
|
# Filter: open license
|
|
if not self._has_open_license(license_id):
|
|
continue
|
|
|
|
# Filter: instruction-tuned (or from a major lab — they usually are)
|
|
org = model_id.split("/")[0] if "/" in model_id else ""
|
|
is_major = org in MAJOR_LABS
|
|
if not is_major and not self._is_instruction_tuned(model_id, tags):
|
|
continue
|
|
|
|
processed += 1
|
|
|
|
# Check if we've already seen this model
|
|
with self._lock:
|
|
if model_id in self._models:
|
|
# Update download counts
|
|
existing = self._models[model_id]
|
|
existing.downloads_7d = downloads # HF "downloads" is ~recent
|
|
existing.total_downloads = downloads
|
|
existing.likes = likes
|
|
existing.last_updated = now
|
|
continue
|
|
|
|
# New model!
|
|
dm = DiscoveredModel(
|
|
model_id=model_id,
|
|
name=model_id.split("/")[-1] if "/" in model_id else model_id,
|
|
org=org,
|
|
downloads_7d=downloads,
|
|
total_downloads=downloads,
|
|
likes=likes,
|
|
size_category=self._estimate_size(model_id),
|
|
license=license_id,
|
|
pipeline_tag=pipeline_tag or "text-generation",
|
|
discovered_at=now,
|
|
status="new",
|
|
last_updated=now,
|
|
)
|
|
self._models[model_id] = dm
|
|
new_models.append(dm)
|
|
|
|
with self._lock:
|
|
self._last_scan = now
|
|
self._scan_count += 1
|
|
|
|
self._save_state()
|
|
|
|
_log(
|
|
f"Watchtower: scan complete — {processed} qualifying models, "
|
|
f"{len(new_models)} new discoveries"
|
|
)
|
|
|
|
# Fire callbacks for new models
|
|
for dm in new_models:
|
|
for cb in self._on_new_model_callbacks:
|
|
try:
|
|
cb(dm)
|
|
except Exception as e:
|
|
logger.warning("Watchtower callback error: %s", e)
|
|
|
|
return new_models
|
|
|
|
# ── Query methods ─────────────────────────────────────────────────
|
|
|
|
def get_trending(self, limit: int = 50) -> list[DiscoveredModel]:
|
|
"""Return current hot models sorted by recent downloads (descending)."""
|
|
with self._lock:
|
|
models = list(self._models.values())
|
|
models.sort(key=lambda m: m.downloads_7d, reverse=True)
|
|
return models[:limit]
|
|
|
|
def get_new_models(self) -> list[DiscoveredModel]:
|
|
"""Return models with status 'new' (not yet queued or obliterated)."""
|
|
with self._lock:
|
|
return [m for m in self._models.values() if m.status == "new"]
|
|
|
|
def get_obliterated(self) -> list[DiscoveredModel]:
|
|
"""Return models that have been obliterated."""
|
|
with self._lock:
|
|
return [m for m in self._models.values() if m.status == "obliterated"]
|
|
|
|
def get_all_models(self) -> list[DiscoveredModel]:
|
|
"""Return all tracked models."""
|
|
with self._lock:
|
|
return list(self._models.values())
|
|
|
|
def get_model(self, model_id: str) -> DiscoveredModel | None:
|
|
"""Get a specific model by ID."""
|
|
with self._lock:
|
|
return self._models.get(model_id)
|
|
|
|
def set_status(self, model_id: str, status: str, metrics: dict | None = None):
|
|
"""Update a model's status (new/queued/obliterating/obliterated/failed)."""
|
|
with self._lock:
|
|
if model_id in self._models:
|
|
m = self._models[model_id]
|
|
m.status = status
|
|
m.last_updated = datetime.now(timezone.utc).isoformat()
|
|
if metrics:
|
|
m.obliteration_metrics = metrics
|
|
self._save_state()
|
|
|
|
def get_stats(self) -> dict[str, Any]:
|
|
"""Return summary statistics."""
|
|
with self._lock:
|
|
total = len(self._models)
|
|
by_status = {}
|
|
for m in self._models.values():
|
|
by_status[m.status] = by_status.get(m.status, 0) + 1
|
|
return {
|
|
"total_tracked": total,
|
|
"last_scan": self._last_scan,
|
|
"scan_count": self._scan_count,
|
|
"by_status": by_status,
|
|
}
|
|
|
|
def get_model_choices(self) -> list[str]:
|
|
"""Return model IDs suitable for a dropdown, trending first."""
|
|
trending = self.get_trending(limit=100)
|
|
return [m.model_id for m in trending]
|
|
|
|
# ── Callbacks ─────────────────────────────────────────────────────
|
|
|
|
def on_new_model(self, callback):
|
|
"""Register a callback for when a new model is discovered.
|
|
|
|
Signature: callback(model: DiscoveredModel)
|
|
"""
|
|
self._on_new_model_callbacks.append(callback)
|
|
|
|
# ── Scheduler ─────────────────────────────────────────────────────
|
|
|
|
def start_scheduler(self, interval: int = 3600, on_log=None):
|
|
"""Start background scanning at the given interval (seconds).
|
|
|
|
Safe to call multiple times — restarts with new interval.
|
|
"""
|
|
self.stop_scheduler()
|
|
self._scheduler_stop.clear()
|
|
|
|
def _run():
|
|
while not self._scheduler_stop.is_set():
|
|
try:
|
|
self.scan(on_log=on_log)
|
|
except Exception as e:
|
|
logger.error("Watchtower scheduler error: %s", e)
|
|
self._scheduler_stop.wait(timeout=interval)
|
|
|
|
self._scheduler_thread = threading.Thread(
|
|
target=_run, daemon=True, name="watchtower-scheduler"
|
|
)
|
|
self._scheduler_thread.start()
|
|
logger.info("Watchtower: scheduler started (interval=%ds)", interval)
|
|
|
|
def stop_scheduler(self):
|
|
"""Stop the background scheduler if running."""
|
|
if self._scheduler_thread and self._scheduler_thread.is_alive():
|
|
self._scheduler_stop.set()
|
|
self._scheduler_thread.join(timeout=5)
|
|
self._scheduler_thread = None
|
|
logger.info("Watchtower: scheduler stopped")
|
|
|
|
@property
|
|
def is_scanning(self) -> bool:
|
|
"""True if the scheduler is actively running."""
|
|
return (
|
|
self._scheduler_thread is not None
|
|
and self._scheduler_thread.is_alive()
|
|
)
|
|
|
|
# ── Table formatting ──────────────────────────────────────────────
|
|
|
|
def format_table(self, models: list[DiscoveredModel] | None = None) -> list[list[str]]:
|
|
"""Format models as a list of rows for a Gradio Dataframe.
|
|
|
|
Columns: [Name, Org, Size, Downloads, Likes, License, Discovered, Status]
|
|
"""
|
|
if models is None:
|
|
models = self.get_trending()
|
|
|
|
rows = []
|
|
for m in models:
|
|
discovered = ""
|
|
if m.discovered_at:
|
|
try:
|
|
dt = datetime.fromisoformat(m.discovered_at)
|
|
discovered = dt.strftime("%Y-%m-%d %H:%M")
|
|
except Exception:
|
|
discovered = m.discovered_at[:16]
|
|
|
|
status_emoji = {
|
|
"new": "🆕",
|
|
"queued": "⏳",
|
|
"obliterating": "⚡",
|
|
"obliterated": "✅",
|
|
"failed": "❌",
|
|
}.get(m.status, "❓")
|
|
|
|
rows.append([
|
|
m.model_id,
|
|
m.org,
|
|
m.size_category,
|
|
f"{m.downloads_7d:,}",
|
|
str(m.likes),
|
|
m.license,
|
|
discovered,
|
|
f"{status_emoji} {m.status}",
|
|
])
|
|
return rows
|
|
|
|
TABLE_HEADERS = [
|
|
"Model ID", "Org", "Size", "Downloads (7d)",
|
|
"Likes", "License", "Discovered", "Status",
|
|
]
|
|
|
|
|
|
# ── Module-level singleton ────────────────────────────────────────────
|
|
# Lazy-initialized so importing the module doesn't trigger disk I/O.
|
|
|
|
_watchtower_instance: Watchtower | None = None
|
|
_watchtower_lock = threading.Lock()
|
|
|
|
|
|
def get_watchtower() -> Watchtower:
|
|
"""Get or create the global Watchtower singleton."""
|
|
global _watchtower_instance
|
|
if _watchtower_instance is None:
|
|
with _watchtower_lock:
|
|
if _watchtower_instance is None:
|
|
_watchtower_instance = Watchtower()
|
|
return _watchtower_instance
|