Initial commit
This commit is contained in:
182
app/services/vector/faiss_customer_store.py
Normal file
182
app/services/vector/faiss_customer_store.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user