new changes in the api

This commit is contained in:
2026-07-06 15:15:51 +05:30
parent c742ef0e53
commit 871981035a
43 changed files with 414 additions and 1975 deletions

View File

@@ -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,