Files
routesapi/app/services/vector/faiss_customer_store.py
2026-06-22 17:40:08 +05:30

183 lines
6.6 KiB
Python

"""
Customer coordinate store.
Purpose:
For each incoming order, look up the customer's phone number.
If a previous verified delivery exists (rider coords on file), use those
coords instead of the (possibly noisy) input delivery coords.
If no record exists, create one so future calls benefit from it.
Storage:
Pure dict + JSON — no FAISS dependency.
phone_meta.json maps phone -> {name, rider_lat, rider_lon}.
Coord lookup key is phone number, not coordinates, so vector search
was never needed; FAISS has been removed.
"""
import os
import json
import logging
from math import radians, cos, sin, asin, sqrt
from threading import Lock
logger = logging.getLogger(__name__)
STORE_DIR = os.getenv("CUSTOMER_STORE_DIR", "ml_data/faiss_customer")
META_PATH = os.path.join(STORE_DIR, "phone_meta.json")
# Within this distance the stored rider coords are considered the real delivery point
SIMILARITY_THRESHOLD_KM = float(os.getenv("FAISS_SIMILARITY_KM", "0.5"))
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Great-circle distance in km."""
try:
lon1, lat1, lon2, lat2 = map(radians, [float(lon1), float(lat1), float(lon2), float(lat2)])
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
return 2 * asin(min(1.0, sqrt(a))) * 6371
except Exception:
return float("inf")
class CustomerCoordStore:
"""
Thread-safe customer delivery coordinate store (dict + JSON backend).
Primary flow (called per order before assignment):
verified_lat, verified_lon, corrected = store.get_verified_coords(
phone, customer_name, input_lat, input_lon
)
"""
def __init__(self):
self._lock = Lock()
os.makedirs(STORE_DIR, exist_ok=True)
self._meta: dict = {} # phone -> {name, rider_lat, rider_lon}
self._load()
# ------------------------------------------------------------------
# Persistence
# ------------------------------------------------------------------
def _load(self):
if os.path.exists(META_PATH):
try:
with open(META_PATH, "r", encoding="utf-8") as f:
self._meta = json.load(f)
logger.info(f"[CustomerStore] Loaded {len(self._meta)} customer records.")
except Exception as e:
logger.warning(f"[CustomerStore] Could not load {META_PATH}: {e} — starting fresh.")
self._meta = {}
else:
self._meta = {}
logger.info("[CustomerStore] No existing store found — starting fresh.")
def _save(self):
try:
with open(META_PATH, "w", encoding="utf-8") as f:
json.dump(self._meta, f, indent=2)
except Exception as e:
logger.warning(f"[CustomerStore] Save failed: {e}")
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def get_verified_coords(
self,
phone: str,
customer_name: str,
input_lat: float,
input_lon: float,
) -> tuple:
"""
Verify / correct delivery coordinates using stored history.
Returns:
(lat, lon, was_corrected)
was_corrected=True → coords replaced with verified stored coords
was_corrected=False → input coords used (new customer or new location)
"""
if not phone or not input_lat or not input_lon:
return input_lat, input_lon, False
phone = str(phone).strip()
with self._lock:
record = self._meta.get(phone)
if record:
stored_lat = record["rider_lat"]
stored_lon = record["rider_lon"]
dist_km = _haversine(stored_lat, stored_lon, input_lat, input_lon)
if dist_km <= SIMILARITY_THRESHOLD_KM:
# Stored rider coords match input → they represent the real door
logger.info(
f"[CustomerStore] {phone}: verified coords used "
f"(stored↔input dist={dist_km:.3f}km, within {SIMILARITY_THRESHOLD_KM}km)"
)
return stored_lat, stored_lon, True
else:
# Customer location changed significantly, trust new input
logger.info(
f"[CustomerStore] {phone}: location changed "
f"(dist={dist_km:.3f}km > {SIMILARITY_THRESHOLD_KM}km), using input coords"
)
return input_lat, input_lon, False
else:
# New customer — create record with input coords
self._create_record(phone, customer_name, input_lat, input_lon)
logger.info(f"[CustomerStore] {phone}: new record created ({input_lat}, {input_lon})")
return input_lat, input_lon, False
def update_rider_coords(self, phone: str, rider_lat: float, rider_lon: float):
"""
Called when a delivery is confirmed to update with actual rider GPS.
This is what makes stored coords progressively more accurate.
"""
phone = str(phone).strip()
with self._lock:
if phone in self._meta:
record = self._meta[phone]
record["rider_lat"] = rider_lat
record["rider_lon"] = rider_lon
self._save()
logger.info(f"[CustomerStore] {phone}: rider coords updated ({rider_lat}, {rider_lon})")
def record_count(self) -> int:
return len(self._meta)
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _create_record(self, phone: str, name: str, lat: float, lon: float):
self._meta[phone] = {
"name": name,
"rider_lat": lat,
"rider_lon": lon,
}
self._save()
# Singleton — keep old name for backward compatibility with any imports
_store_instance: "CustomerCoordStore | None" = None
_store_lock = Lock()
def get_faiss_store() -> CustomerCoordStore:
"""Returns the singleton CustomerCoordStore (backward-compat name)."""
global _store_instance
if _store_instance is None:
with _store_lock:
if _store_instance is None:
_store_instance = CustomerCoordStore()
return _store_instance
# Also expose under new name
get_customer_store = get_faiss_store