TradeFlowAI / src /tasks /learning_tasks.py
muhammadghiffari's picture
Deploy TradeFlow API to HF
db4ba8d
Raw
History Blame Contribute Delete
4.06 kB
"""
TradeFlow AI — Learning & Maintenance Tasks
"""
import structlog
from ..config import settings
from ..services.predictor_svc import FEATURE_NAMES, rejection_predictor
from .celery_app import celery_app
log = structlog.get_logger()
def _run_async(coro):
import asyncio
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
@celery_app.task(bind=True, queue="low")
def retrain_predictor(self):
"""Retrain XGBoost from labeled CEISA outcomes and promote only if quality holds."""
async def _retrain():
import asyncpg
import numpy as np
conn = await asyncpg.connect(settings.DATABASE_URL)
try:
rows = await conn.fetch(
"""
SELECT id, outcome, feature_snapshot, predicted_rejection_prob
FROM submission_outcomes
WHERE feature_snapshot IS NOT NULL
ORDER BY created_at ASC
"""
)
if len(rows) < settings.RETRAIN_MIN_TOTAL_SAMPLES:
return {"trained": False, "reason": "not_enough_total_samples", "samples": len(rows)}
X = np.array(
[[float((row["feature_snapshot"] or {}).get(feature, 0.0)) for feature in FEATURE_NAMES] for row in rows],
dtype=np.float32,
)
y = np.array([1 if row["outcome"] == "rejected" else 0 for row in rows], dtype=np.int32)
version = str(int(__import__("time").time()))
result = rejection_predictor.train_and_upload(X, y, version)
if result.get("promoted"):
await conn.execute(
"UPDATE submission_outcomes SET used_in_training = TRUE WHERE feature_snapshot IS NOT NULL"
)
return {"trained": bool(result.get("promoted")), **result, "samples": len(rows)}
finally:
await conn.close()
log.info("Retraining rejection predictor")
return _run_async(_retrain())
@celery_app.task(bind=True, queue="low")
def refresh_btki_embeddings(self):
log.info("Task stub: refresh_btki_embeddings")
@celery_app.task(bind=True, queue="low")
def check_retrain_trigger(self):
"""Schedule retraining and flag extraction drift from recent corrections."""
async def _check():
import asyncpg
conn = await asyncpg.connect(settings.DATABASE_URL)
try:
new_samples = await conn.fetchval(
"SELECT COUNT(*) FROM submission_outcomes WHERE used_in_training = FALSE"
)
drift_rows = await conn.fetch(
"""
SELECT field_name, COUNT(*) AS correction_count
FROM learning_samples
WHERE created_at >= NOW() - ($1::int * INTERVAL '1 day')
GROUP BY field_name
HAVING COUNT(*) >= $2
ORDER BY correction_count DESC
""",
settings.DRIFT_LOOKBACK_DAYS,
settings.DRIFT_CORRECTION_THRESHOLD,
)
drift_alerts = [
{"field_name": row["field_name"], "correction_count": row["correction_count"]}
for row in drift_rows
]
should_retrain = (
settings.ENABLE_ADAPTIVE_LEARNING
and int(new_samples or 0) >= settings.RETRAIN_MIN_NEW_SAMPLES
)
if should_retrain:
retrain_predictor.apply_async(queue="low")
if drift_alerts:
log.warning("Field-level extraction drift detected", drift_alerts=drift_alerts)
return {
"new_samples": int(new_samples or 0),
"scheduled_retrain": should_retrain,
"drift_alerts": drift_alerts,
}
finally:
await conn.close()
log.info("Checking adaptive learning retrain trigger")
return _run_async(_check())