new changes in the api
This commit is contained in:
@@ -11,7 +11,6 @@ from fastapi import APIRouter, Body, Request, Depends, status, HTTPException, Qu
|
||||
|
||||
from app.controllers.route_controller import RouteController
|
||||
from app.core.exceptions import APIException
|
||||
from app.core.arrow_utils import save_optimized_route_parquet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -85,68 +84,19 @@ def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
return float("inf")
|
||||
|
||||
|
||||
def _auto_tune_strategy(collector) -> None:
|
||||
"""
|
||||
SQL-based strategy auto-tuner.
|
||||
|
||||
Reads quality_score statistics from the assignment log grouped by
|
||||
ml_strategy. If one strategy clearly outperforms the rest (minimum
|
||||
10 calls, ≥ 2 strategies compared) it is written to DynamicConfig so
|
||||
all future requests benefit automatically.
|
||||
|
||||
Safe to call at any time — DynamicConfig write is idempotent.
|
||||
"""
|
||||
try:
|
||||
comparison = collector.get_strategy_comparison()
|
||||
if not comparison or len(comparison) < 2:
|
||||
return # not enough variety to compare
|
||||
|
||||
# Only consider strategies with at least 10 real observations
|
||||
qualified = [s for s in comparison if s["call_count"] >= 10]
|
||||
if not qualified:
|
||||
return
|
||||
|
||||
best = max(qualified, key=lambda x: x["avg_quality"])
|
||||
from app.config.dynamic_config import get_config as _gc
|
||||
cfg = _gc()
|
||||
current = cfg.get("ml_strategy", "balanced")
|
||||
if best["strategy"] != current:
|
||||
cfg.set("ml_strategy", best["strategy"], source="auto_tuner")
|
||||
logger.info(
|
||||
f"[AutoTune] Strategy updated: '{current}' → '{best['strategy']}' "
|
||||
f"(avg_quality={best['avg_quality']:.1f}, n={best['call_count']})"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"[AutoTune] Strategy '{current}' already optimal "
|
||||
f"(avg_quality={best['avg_quality']:.1f})"
|
||||
)
|
||||
except Exception as _ate:
|
||||
logger.warning(f"[AutoTune] Failed (non-fatal): {_ate}")
|
||||
|
||||
|
||||
def _bg_log_and_maybe_retrain(
|
||||
def _bg_log_assignment(
|
||||
num_orders: int,
|
||||
num_riders: int,
|
||||
hyperparams: dict,
|
||||
assignments: dict,
|
||||
unassigned_count: int,
|
||||
elapsed_ms: float,
|
||||
bandit_context: "tuple | None" = None, # (time_band, load_band)
|
||||
) -> None:
|
||||
"""
|
||||
Background worker: log event → update RL bandit → auto-retrain ID3
|
||||
→ auto-tune strategy. Runs in _ml_executor (never blocks the response).
|
||||
|
||||
Thresholds:
|
||||
every call → bandit posterior update (Thompson Sampling RL)
|
||||
every 100 evts → greedy SQL auto-tuner (backup check, rarely fires now)
|
||||
"""
|
||||
"""Background worker: log the assignment event. Runs in _ml_executor so
|
||||
it never blocks the API response."""
|
||||
try:
|
||||
from app.services.ml.ml_data_collector import get_collector as _gc
|
||||
collector = _gc()
|
||||
|
||||
# ── 1. Log event, get back quality_score ──────────────────────────
|
||||
quality_score = collector.log_assignment_event(
|
||||
num_orders=num_orders,
|
||||
num_riders=num_riders,
|
||||
@@ -155,29 +105,8 @@ def _bg_log_and_maybe_retrain(
|
||||
unassigned_count=unassigned_count,
|
||||
elapsed_ms=elapsed_ms,
|
||||
) or 50.0
|
||||
|
||||
count = collector.count_records()
|
||||
logger.debug(f"[ML BG] Logged event #{count}, quality={quality_score:.1f}")
|
||||
|
||||
# ── 2. Thompson Sampling bandit update (RL feedback) ──────────────
|
||||
if bandit_context:
|
||||
try:
|
||||
from app.services.ml.strategy_bandit import get_bandit as _gb
|
||||
_t_band, _l_band = bandit_context
|
||||
_strategy = hyperparams.get("ml_strategy", "balanced")
|
||||
get_bandit_fn = _gb
|
||||
get_bandit_fn().update(_t_band, _l_band, _strategy, quality_score)
|
||||
logger.debug(
|
||||
f"[Bandit] Updated ctx={_t_band}|{_l_band} "
|
||||
f"arm={_strategy} reward={quality_score/100:.3f}"
|
||||
)
|
||||
except Exception as _be:
|
||||
logger.debug(f"[Bandit] Update failed (non-fatal): {_be}")
|
||||
|
||||
# ── 3. Greedy SQL auto-tune every 100 events (backup) ────────────
|
||||
if count >= 20 and count % 100 == 0:
|
||||
_auto_tune_strategy(collector)
|
||||
|
||||
except Exception as _e:
|
||||
logger.warning(f"[ML BG] Background task failed (non-fatal): {_e}")
|
||||
|
||||
@@ -242,18 +171,6 @@ async def provider_optimize_forward(
|
||||
try:
|
||||
url = "https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries"
|
||||
result = await controller.optimize_and_forward_provider_payload(body, url)
|
||||
|
||||
# Parquet snapshot — offloaded so it never blocks the response
|
||||
def _snap():
|
||||
try:
|
||||
os.makedirs("data/snapshots", exist_ok=True)
|
||||
snapshot_path = f"data/snapshots/route_{int(time.time())}.parquet"
|
||||
save_optimized_route_parquet(body, snapshot_path)
|
||||
logger.info(f"Apache Arrow: Snapshot saved to {snapshot_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not save Arrow snapshot: {e}")
|
||||
_ml_executor.submit(_snap)
|
||||
|
||||
return result
|
||||
except APIException:
|
||||
raise
|
||||
@@ -554,7 +471,6 @@ async def assign_orders_to_riders(
|
||||
request: Request,
|
||||
body: Any = Body(default=None),
|
||||
reshuffle: bool = Query(False, alias="reshuffle"),
|
||||
hypertuning_params: str = None,
|
||||
):
|
||||
"""
|
||||
Smart assignment of orders to riders.
|
||||
@@ -653,11 +569,10 @@ async def assign_orders_to_riders(
|
||||
|
||||
# 3. Log summary after all data is in hand
|
||||
mode_str = "reshuffle" if do_reshuffle else "normal"
|
||||
tuning_str = hypertuning_params if hypertuning_params else "null"
|
||||
logger.info(
|
||||
f"\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
f"[API HIT] POST /api/v1/optimization/riderassign\n"
|
||||
f"[CONFIG] Mode: {mode_str.upper()} | Hypertuning: {tuning_str} | Active Riders: {len(riders)}\n"
|
||||
f"[CONFIG] Mode: {mode_str.upper()} | Active Riders: {len(riders)}\n"
|
||||
f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
)
|
||||
logger.info(f"[API] riderassign ▶ orders={len(orders)} riders={len(riders)} mode={mode_str}")
|
||||
@@ -811,44 +726,10 @@ async def assign_orders_to_riders(
|
||||
vrp_orders = [o for o in orders if id(o) not in history_pre_assigned_ids]
|
||||
|
||||
# 3. Run Assignment (AssignmentService)
|
||||
# -- Per-request strategy override (thread-safe via contextvars) --
|
||||
# Old approach mutated _cfg._cache directly — a race condition when two
|
||||
# concurrent requests set different strategies simultaneously.
|
||||
# New approach: each async task gets its OWN copy of the ContextVar,
|
||||
# so overrides never leak between concurrent requests.
|
||||
from app.config.dynamic_config import get_config, set_request_strategy
|
||||
from app.services.ml.behavior_analyzer import time_band as _tb, load_band as _lb
|
||||
from datetime import datetime as _DT
|
||||
from app.config.dynamic_config import get_config
|
||||
|
||||
_cfg = get_config()
|
||||
|
||||
# ── Bandit context (used both for selection + background update) ──
|
||||
_bandit_t_band = _tb(_DT.now().isoformat())
|
||||
_bandit_l_band = _lb(len(vrp_orders) / max(1, len(riders)))
|
||||
_bandit_context = (_bandit_t_band, _bandit_l_band)
|
||||
|
||||
valid_strategies = ["balanced", "fuel_saver", "aggressive_speed", "zone_strict"]
|
||||
if hypertuning_params and hypertuning_params in valid_strategies:
|
||||
# Explicit caller override always wins
|
||||
set_request_strategy(hypertuning_params)
|
||||
logger.info(f"[HYPERTUNE] Per-request strategy override: {hypertuning_params}")
|
||||
else:
|
||||
# ── Thompson Sampling: let the bandit choose ──────────────────
|
||||
# The bandit samples from Beta posteriors for this (time, load)
|
||||
# context and picks the arm with highest sample. It naturally
|
||||
# explores when uncertain and exploits known-good strategies as
|
||||
# data accumulates. Replaces the one-size-fits-all greedy picker.
|
||||
try:
|
||||
from app.services.ml.strategy_bandit import get_bandit as _gb
|
||||
_bandit_strategy = _gb().select(_bandit_t_band, _bandit_l_band)
|
||||
set_request_strategy(_bandit_strategy)
|
||||
logger.info(
|
||||
f"[Bandit] Strategy='{_bandit_strategy}' "
|
||||
f"ctx={_bandit_t_band}|{_bandit_l_band}"
|
||||
)
|
||||
except Exception as _bse:
|
||||
logger.debug(f"[Bandit] Selection failed (non-fatal): {_bse}")
|
||||
|
||||
optimizer = RouteOptimizer()
|
||||
|
||||
# ── PHASE 3a: TRUE VRP (primary solver) ──────────────────────────────
|
||||
@@ -863,7 +744,7 @@ async def assign_orders_to_riders(
|
||||
if not do_reshuffle: # VRP not meaningful during reshuffle (intentional exploration)
|
||||
try:
|
||||
# Build minimal rider info for VRP
|
||||
from app.services.routing.kalman_filter import smooth_rider_locations
|
||||
from app.services.routing.gps_smoother import smooth_rider_locations
|
||||
_smooth_riders = smooth_rider_locations(list(riders))
|
||||
|
||||
from app.services.core.assignment_service import AssignmentService as _AS
|
||||
@@ -1272,21 +1153,19 @@ async def assign_orders_to_riders(
|
||||
risk_meta: dict = {"label": "n/a", "model_trained": False, "deprecated": True}
|
||||
|
||||
# ── BACKGROUND ML LOGGING ────────────────────────────────────────────
|
||||
# Fire-and-forget: log this assignment event to SQLite, auto-retrain
|
||||
# ID3 every 50 events, auto-tune strategy every 100 events.
|
||||
# Fire-and-forget: log this assignment event to SQLite.
|
||||
# Uses _ml_executor so the API response is never delayed.
|
||||
try:
|
||||
_elapsed_ms = (time.time() - _t0) * 1000
|
||||
_hyp_snapshot = get_config().get_all() # frozen copy for this call
|
||||
_ml_executor.submit(
|
||||
_bg_log_and_maybe_retrain,
|
||||
_bg_log_assignment,
|
||||
len(orders),
|
||||
len(riders),
|
||||
_hyp_snapshot,
|
||||
{rid: list(ords) for rid, ords in assignments.items()},
|
||||
len(unassigned_orders),
|
||||
round(_elapsed_ms, 1),
|
||||
_bandit_context, # (time_band, load_band) for RL update
|
||||
)
|
||||
except Exception as _mle:
|
||||
logger.debug(f"[ML BG] Submit failed (non-fatal): {_mle}")
|
||||
@@ -1354,7 +1233,6 @@ async def assign_orders_to_riders(
|
||||
},
|
||||
"reshuffle_mode": do_reshuffle,
|
||||
"solver_mode": "vrp_optimal" if used_vrp else "2phase_heuristic",
|
||||
"hypertuning_params": hypertuning_params or "default",
|
||||
"faiss_coord_corrections": _faiss_corrected,
|
||||
"faiss_customer_records": _faiss_store.record_count() if _faiss_store else 0,
|
||||
"faiss_history_preassigned": _history_hits,
|
||||
|
||||
Reference in New Issue
Block a user