feat(heads): nightly auto-retrain + inline Retrain button in Explore
Two cadences for keeping heads in sync with your tagging: - PASSIVE: a nightly `scheduled_train_heads` beat (skips if a run is already in flight; creates+commits the run row before dispatching train_heads so the ml worker always finds it). Folds the day's accepts/rejects + newly-eligible concepts into the heads without anyone clicking. - ACTIVE: a "Retrain heads" button in the Explore trail bar — bank the +/- feedback you just gave while walking content, without a trip to Settings. Shared logic in a new useHeadTraining composable (trigger + poll + start/finish toasts), used by the Explore button; reflects an already-running run (incl. the nightly one) on mount. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -109,6 +109,10 @@ def make_celery() -> Celery:
|
||||
"task": "backend.app.tasks.ml.apply_allowlist_tags",
|
||||
"schedule": 86400.0,
|
||||
},
|
||||
"train-heads-nightly": {
|
||||
"task": "backend.app.tasks.ml.scheduled_train_heads",
|
||||
"schedule": 86400.0, # passive cadence; manual retrain stays available
|
||||
},
|
||||
"integrity-verify-weekly": {
|
||||
"task": "backend.app.tasks.maintenance.verify_integrity",
|
||||
"schedule": 604800.0, # weekly
|
||||
|
||||
@@ -629,3 +629,33 @@ def train_heads(self, run_id: int) -> str:
|
||||
run.finished_at = datetime.now(UTC)
|
||||
session.commit()
|
||||
return "ready"
|
||||
|
||||
|
||||
@celery.task(name="backend.app.tasks.ml.scheduled_train_heads")
|
||||
def scheduled_train_heads() -> str:
|
||||
"""Nightly passive retrain (#114): fold the day's accepts/rejects + any
|
||||
newly-eligible concepts into the heads without the operator clicking. Skips
|
||||
if a run is already in flight (one at a time). Creates + COMMITS the run row
|
||||
before dispatching so the ml-queue worker can always find it."""
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
from ..models import HeadTrainingRun
|
||||
|
||||
SessionLocal = _sync_session_factory()
|
||||
with SessionLocal() as session:
|
||||
running = session.execute(
|
||||
sa_select(HeadTrainingRun.id).where(HeadTrainingRun.status == "running")
|
||||
).scalar_one_or_none()
|
||||
if running is not None:
|
||||
return "already running"
|
||||
run = HeadTrainingRun(
|
||||
params={"source": "scheduled"}, status="running",
|
||||
last_progress_at=datetime.now(UTC),
|
||||
)
|
||||
session.add(run)
|
||||
session.commit()
|
||||
run_id = run.id
|
||||
train_heads.delay(run_id)
|
||||
return "dispatched"
|
||||
|
||||
Reference in New Issue
Block a user