new changes in the api
This commit is contained in:
@@ -3,8 +3,8 @@ Analytics & ML Admin API
|
||||
=========================
|
||||
Exposes historical assignment quality data, the empirical-ETA pipeline, and the
|
||||
autonomous road-sequencing / rider-affinity agents. (The XGBoost/Optuna hypertuner,
|
||||
ID3 risk tree, and profit predictor have all been retired — they never affected
|
||||
assignment.)
|
||||
ID3 risk tree, profit predictor, and Thompson-Sampling strategy bandit have all
|
||||
been retired — none of them ever affected assignment or routing behavior.)
|
||||
|
||||
Endpoints:
|
||||
GET /api/v1/ml/status – quality trend, analytics DB + history stats
|
||||
@@ -12,7 +12,6 @@ Endpoints:
|
||||
GET /api/v1/ml/config – active config values
|
||||
PATCH /api/v1/ml/config – manual config override
|
||||
POST /api/v1/ml/reset – reset config to defaults
|
||||
POST /api/v1/ml/strategy – change optimization strategy
|
||||
POST /api/v1/ml/refresh-eta – sync nearledb + rebuild empirical ETA stats
|
||||
GET /api/v1/ml/eta-accuracy – formula vs empirical ETA backtest
|
||||
GET/POST /api/v1/ml/road-eval – autonomous road-sequencing decision
|
||||
@@ -103,7 +102,6 @@ def ml_analytics():
|
||||
"hourly_stats": collector.get_hourly_stats(),
|
||||
"zone_stats": collector.get_zone_stats(),
|
||||
"quality_histogram": collector.get_quality_histogram(),
|
||||
"strategy_comparison": collector.get_strategy_comparison(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[ML API] analytics: {e}", exc_info=True)
|
||||
@@ -163,142 +161,6 @@ def ml_reset():
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /strategy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.post("/strategy", summary="Change the optimization strategy")
|
||||
def ml_strategy(strategy: str = Body(default="balanced", embed=True)):
|
||||
"""
|
||||
Choices: balanced | fuel_saver | aggressive_speed | zone_strict
|
||||
Affects how the quality score is computed in analytics only.
|
||||
"""
|
||||
valid = ["balanced", "fuel_saver", "aggressive_speed", "zone_strict"]
|
||||
if strategy not in valid:
|
||||
raise HTTPException(400, f"Invalid strategy. Choose from {valid}")
|
||||
from app.config.dynamic_config import get_config
|
||||
try:
|
||||
get_config().set("ml_strategy", strategy)
|
||||
return {"status": "ok", "strategy": strategy}
|
||||
except Exception as e:
|
||||
logger.error(f"[ML API] strategy: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /auto-tune
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.post("/auto-tune", summary="Run SQL-based strategy auto-tuner")
|
||||
def ml_auto_tune():
|
||||
"""
|
||||
Analyses the assignment log and picks the best-performing ml_strategy
|
||||
based on average quality score across all recorded calls.
|
||||
|
||||
Rules:
|
||||
- A strategy needs ≥ 10 calls to be considered.
|
||||
- At least 2 strategies must have enough data to compare.
|
||||
- If a better strategy is found it is written to DynamicConfig
|
||||
immediately and takes effect on the next /riderassign call.
|
||||
- Also returns per-hour breakdown so you can see peak-hour patterns.
|
||||
|
||||
Safe to call any time. Runs in < 100ms.
|
||||
"""
|
||||
from app.services.ml.ml_data_collector import get_collector
|
||||
from app.config.dynamic_config import get_config, DEFAULTS
|
||||
try:
|
||||
collector = get_collector()
|
||||
cfg = get_config()
|
||||
comparison = collector.get_strategy_comparison()
|
||||
hourly = collector.get_hourly_stats()
|
||||
total_records = collector.count_records()
|
||||
|
||||
if total_records == 0:
|
||||
return {
|
||||
"status": "no_data",
|
||||
"message": "No assignment events logged yet. "
|
||||
"Call /riderassign a few times first.",
|
||||
"total_records": 0,
|
||||
}
|
||||
|
||||
qualified = [s for s in comparison if s["call_count"] >= 10]
|
||||
current_strategy = cfg.get("ml_strategy", "balanced")
|
||||
action = "no_change"
|
||||
recommendation = None
|
||||
|
||||
if len(qualified) >= 2:
|
||||
best = max(qualified, key=lambda x: x["avg_quality"])
|
||||
recommendation = best["strategy"]
|
||||
if best["strategy"] != current_strategy:
|
||||
cfg.set("ml_strategy", best["strategy"], source="auto_tuner")
|
||||
action = "updated"
|
||||
logger.info(
|
||||
f"[AutoTune API] Strategy: '{current_strategy}' → "
|
||||
f"'{best['strategy']}' (quality={best['avg_quality']:.1f})"
|
||||
)
|
||||
elif len(comparison) > 0:
|
||||
action = "insufficient_data"
|
||||
recommendation = comparison[0]["strategy"] # best so far even if < 10 calls
|
||||
|
||||
# Per-hour best strategy (informational — not auto-applied)
|
||||
# Shows which strategy logged the highest quality at each hour
|
||||
hour_best: list = []
|
||||
if hourly:
|
||||
for h in hourly:
|
||||
# Find which strategy performed best in this hour block
|
||||
# (simple: use the dominant strategy for that hour from comparison)
|
||||
hour_best.append({
|
||||
"hour": h["hour"],
|
||||
"avg_quality": h["avg_quality"],
|
||||
"call_count": h["call_count"],
|
||||
"sla_breaches": h["sla_breaches"],
|
||||
})
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"action": action,
|
||||
"current_strategy": cfg.get("ml_strategy", "balanced"),
|
||||
"recommendation": recommendation,
|
||||
"total_records": total_records,
|
||||
"strategy_comparison": comparison,
|
||||
"hourly_quality": hour_best,
|
||||
"message": (
|
||||
f"Strategy updated to '{recommendation}'."
|
||||
if action == "updated"
|
||||
else "Current strategy is already optimal."
|
||||
if action == "no_change"
|
||||
else "More data needed (≥ 10 calls per strategy to compare)."
|
||||
),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[ML API] auto-tune: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /bandit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/bandit", summary="Thompson Sampling bandit — posterior stats per context")
|
||||
def ml_bandit():
|
||||
"""
|
||||
Shows the current state of the RL strategy bandit.
|
||||
|
||||
Each context (time_band|load_band) has 4 arms (strategies).
|
||||
For each arm:
|
||||
mean_reward — expected quality / 100 based on posterior mean
|
||||
observations — number of observed calls (excluding prior)
|
||||
alpha / beta — Beta distribution parameters
|
||||
|
||||
The bandit uses Thompson Sampling to select strategies automatically
|
||||
on every /riderassign call, balancing exploration vs exploitation.
|
||||
"""
|
||||
try:
|
||||
from app.services.ml.strategy_bandit import get_bandit
|
||||
return {"status": "ok", **get_bandit().get_stats()}
|
||||
except Exception as e:
|
||||
logger.error(f"[ML API] bandit: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -346,21 +208,24 @@ def ml_history(patterns: bool = False):
|
||||
(all clear dominant-rider zones, sorted by pattern score).
|
||||
"""
|
||||
from app.services.vector.delivery_history_store import (
|
||||
get_delivery_history_store, _INDEX_PATH, _META_PATH, CSV_PATH
|
||||
get_delivery_history_store, _paths_for, _pattern_source, CSV_PATH
|
||||
)
|
||||
try:
|
||||
store = get_delivery_history_store()
|
||||
meta = {}
|
||||
if os.path.isfile(_META_PATH):
|
||||
with open(_META_PATH, "r", encoding="utf-8") as f:
|
||||
store = get_delivery_history_store()
|
||||
source = _pattern_source()
|
||||
vectors_path, _records_path, meta_path = _paths_for(source)
|
||||
meta = {}
|
||||
if os.path.isfile(meta_path):
|
||||
with open(meta_path, "r", encoding="utf-8") as f:
|
||||
meta = json.load(f)
|
||||
|
||||
resp = {
|
||||
"status": "ok",
|
||||
"pattern_source": source,
|
||||
"record_count": store.record_count(),
|
||||
"pattern_count": store.pattern_count(),
|
||||
"index_ready": store.record_count() > 0,
|
||||
"disk_index": os.path.isfile(_INDEX_PATH),
|
||||
"disk_index": os.path.isfile(vectors_path),
|
||||
"csv_path": CSV_PATH,
|
||||
"csv_exists": os.path.isfile(CSV_PATH),
|
||||
"saved_meta": meta,
|
||||
|
||||
Reference in New Issue
Block a user