feat(ccip): refresh task + beat + retrain hook for character prototypes (#1317, m138 step 3)
- refresh_character_prototypes celery task wraps the incremental builder (sync ml worker); returns skipped / rebuilt=N removed=N. - Beat: every ~15 min (cheap global-gate no-op when idle) + a nightly full=True reconcile as belt-and-suspenders. - train_heads enqueues it on success, so the Retrain button AND the nightly head retrain refresh CCIP on the SAME trigger — unified lifecycle, as asked. The initial (cold) full build loads the whole reference set once in the background, never on a /suggestions request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user