📄 router.py 3,725 bytes Apr 19, 2026 📋 Raw

"""Core route optimizer — sorts classified items into warehouse traversal order."""

from costco_route.config import ZONES, ZONE_ORDER

def generate_route(classified: dict[str, list[str]], learned_overrides: dict[str, dict] | None = None) -> list[dict]:
"""Generate a route-optimized shopping list.

Args:
    classified: LLM-classified items {zone_id: [items]}
    learned_overrides: Override from ChromaDB {item_name: {zone, notes, ...}}

Returns:
    List of zone dicts in traversal order, each with zone info and items.
"""
if learned_overrides:
    classified = _apply_overrides(classified, learned_overrides)

route = []
for zone_id in ZONE_ORDER:
    items = classified.get(zone_id, [])
    if not items:
        continue

    zone_info = ZONES.get(zone_id, {"name": f"Zone {zone_id}"})
    route.append({
        "zone_id": zone_id,
        "zone_name": zone_info["name"],
        "items": items,
    })

return route

def _apply_overrides(classified: dict[str, list[str]], overrides: dict[str, dict]) -> dict[str, list[str]]:
"""Move items to their learned zones, overriding LLM classification.

If an item was classified to zone X by the LLM but the user previously
calibrated it to zone Y, move it to zone Y.
"""
# Build a lookup: item_lower → override zone
override_map = {}
for item_name, override_info in overrides.items():
    override_map[item_name.lower().strip()] = override_info["zone"]

# Rebuild classified dict with overrides applied
result = {}
for zone_id, items in classified.items():
    remaining = []
    for item in items:
        key = item.lower().strip()
        if key in override_map:
            # Move to the learned zone instead
            target_zone = override_map[key]
            result.setdefault(target_zone, [])
            result[target_zone].append(item)
        else:
            remaining.append(item)
    if remaining:
        result[zone_id] = remaining

return result

def format_route(route: list[dict], store_name: str = "Green Bay Bellevue") -> str:
"""Format a route into a Telegram-friendly shopping list.

Args:
    route: Output from generate_route()
    store_name: Display name for the warehouse

Returns:
    Formatted string ready for Telegram.
"""
lines = [f"🛒 Costco Route  {store_name}"]

total_items = 0
zone_count = len(route)

for zone in route:
    lines.append(f"\n📦 Zone {zone['zone_id']}  {zone['zone_name']}")
    for item in zone["items"]:
        lines.append(f"   {item}")
        total_items += 1

# Estimate time: ~2 min per zone + 1 min per item, minimum 15 min
est_minutes = max(15, zone_count * 2 + total_items)
lines.append(f"\n {total_items} items across {zone_count} zones  ~{est_minutes} min")

return "\n".join(lines)

def format_route_markdown(route: list[dict], store_name: str = "Green Bay Bellevue") -> str:
"""Format a route with markdown (for CLI / non-Telegram output).

Same as format_route but with markdown checkboxes for copy-paste.
"""
lines = [f"## 🛒 Costco Route — {store_name}"]

total_items = 0
zone_count = len(route)

for zone in route:
    lines.append(f"\n### Zone {zone['zone_id']} — {zone['zone_name']}")
    for item in zone["items"]:
        lines.append(f"- [ ] {item}")
        total_items += 1

est_minutes = max(15, zone_count * 2 + total_items)
lines.append(f"\n**{total_items} items across {zone_count} zones — ~{est_minutes} min**")

return "\n".join(lines)