diff --git a/backend/app/celery_app.py b/backend/app/celery_app.py index 74e2403..f42db3b 100644 --- a/backend/app/celery_app.py +++ b/backend/app/celery_app.py @@ -107,6 +107,15 @@ def make_celery() -> Celery: "task": "backend.app.tasks.ml.scheduled_train_heads", "schedule": 86400.0, # passive cadence; manual retrain stays available }, + "refresh-character-prototypes": { + "task": "backend.app.tasks.ml.refresh_character_prototypes", + "schedule": 900.0, # ~15 min; cheap global-gate no-op when idle (#1317) + }, + "reconcile-character-prototypes-nightly": { + "task": "backend.app.tasks.ml.refresh_character_prototypes", + "schedule": 86400.0, # nightly FULL reconcile (belt-and-suspenders) + "args": (True,), # full=True + }, "apply-head-tags-daily": { "task": "backend.app.tasks.ml.scheduled_apply_head_tags", "schedule": 86400.0, # no-op unless head_auto_apply_enabled diff --git a/backend/app/tasks/ml.py b/backend/app/tasks/ml.py index b1650e2..bfb8d0c 100644 --- a/backend/app/tasks/ml.py +++ b/backend/app/tasks/ml.py @@ -328,6 +328,11 @@ def train_heads(self, run_id: int) -> str: run.status = "ready" run.finished_at = datetime.now(UTC) session.commit() + # A retrain folds the day's accepts/rejects into the heads; refresh the + # CCIP character prototypes on the SAME trigger (#1317) so the Retrain + # button + nightly run keep character matching current too. Cheap no-op + # when references didn't change. + refresh_character_prototypes.delay() return "ready" @@ -361,6 +366,31 @@ def scheduled_train_heads() -> str: return "dispatched" +@celery.task( + name="backend.app.tasks.ml.refresh_character_prototypes", + # Global-gate no-op when idle; a rebuild touches only changed characters. The + # initial (cold) build loads the whole reference set once, in the BACKGROUND + # — never on a /suggestions request. + soft_time_limit=1800, time_limit=2100, +) +def refresh_character_prototypes(full: bool = False) -> str: + """Incrementally refresh the CCIP character-prototype store (#1317, m138): + cheap global-gate skip when nothing changed, else rebuild only the characters + whose references moved. Triggered by a ~15-min beat, after every head retrain + (train_heads), and nightly with full=True. Returns a short status.""" + from ..services.ml.character_prototypes import ( + refresh_character_prototypes as _refresh, + ) + + SessionLocal = _sync_session_factory() + with SessionLocal() as session: + result = _refresh(session, full=full) + return ( + "skipped" if result["skipped"] + else f"rebuilt={result['rebuilt']} removed={result['removed']}" + ) + + @celery.task( name="backend.app.tasks.ml.apply_head_tags", bind=True,