Spaces:
Running
Running
| """ShopStack demo walkthrough β stepwise narration of household data flow. | |
| Canonical run path (preferred): | |
| uv run python scripts/demo_walkthrough.py | |
| .venv/bin/python scripts/demo_walkthrough.py | |
| The ``shopstack`` package is importable directly because the project | |
| is installed in editable mode (``pip install -e .`` / ``uv sync``). | |
| No ``sys.path`` manipulation is required. | |
| Walks the user through inventory β market lens β shopping list β | |
| classification β output, exercising the same code paths the Gradio | |
| app uses in production. | |
| """ | |
| from __future__ import annotations | |
| from shopstack.config import settings | |
| from shopstack.decisions import classify_all | |
| from shopstack.persistence.database import Database | |
| from shopstack.tools.registry import ToolRegistry | |
| DEMO_ITEMS = [ | |
| {"canonical_name": "milk", "display_name": "Milk", "quantity": 2.0, "unit": "liter", "price": 74.0, "store": "local milk booth", "location": "fridge", "category": "dairy"}, | |
| {"canonical_name": "rice", "display_name": "Basmati Rice", "quantity": 5.0, "unit": "kg", "price": 410.0, "store": "Big Bazaar", "location": "pantry", "category": "grains"}, | |
| {"canonical_name": "eggs", "display_name": "Eggs", "quantity": 12.0, "unit": "pieces", "price": 96.0, "store": "Morning Eggstop", "location": "fridge_top", "category": "protein"}, | |
| {"canonical_name": "onion", "display_name": "Onion", "quantity": 1.5, "unit": "kg", "price": 32.0, "store": "Local Vendor", "location": "pantry_mid", "category": "vegetable"}, | |
| {"canonical_name": "toothpaste", "display_name": "Toothpaste", "quantity": 1.0, "unit": "unit", "price": 129.0, "store": "Apna Store", "location": "bathroom_cabinet", "category": "personal care"}, | |
| {"canonical_name": "olive oil", "display_name": "Olive Oil", "quantity": 1.0, "unit": "L", "price": 690.0, "store": "Supermart", "location": "pantry_mid", "category": "cooking"}, | |
| {"canonical_name": "curd", "display_name": "Curd", "quantity": 0.5, "unit": "kg", "price": 48.0, "store": "Fresh Dairy", "location": "fridge", "category": "dairy"}, | |
| {"canonical_name": "bread", "display_name": "Whole Wheat Bread", "quantity": 1.0, "unit": "loaf", "price": 55.0, "store": "Apna Store", "location": "fridge_top", "category": "bakery"}, | |
| {"canonical_name": "potato", "display_name": "Potato", "quantity": 2.0, "unit": "kg", "price": 30.0, "store": "Local Vendor", "location": "pantry_mid", "category": "vegetable"}, | |
| {"canonical_name": "butter", "display_name": "Butter", "quantity": 0.25, "unit": "kg", "price": 120.0, "store": "Big Bazaar", "location": "fridge", "category": "dairy"}, | |
| ] | |
| PRICE_OBSERVATIONS = [ | |
| ("milk", 74.0, 1.0, "liter", "local milk booth"), | |
| ("milk", 72.0, 1.0, "liter", "Big Bazaar"), | |
| ("rice", 410.0, 5.0, "kg", "Big Bazaar"), | |
| ("rice", 425.0, 5.0, "kg", "Supermart"), | |
| ("eggs", 96.0, 12.0, "pieces", "Morning Eggstop"), | |
| ("eggs", 102.0, 12.0, "pieces", "Big Bazaar"), | |
| ("onion", 32.0, 1.0, "kg", "Local Vendor"), | |
| ("onion", 35.0, 1.0, "kg", "Big Bazaar"), | |
| ("toothpaste", 129.0, 1.0, "unit", "Apna Store"), | |
| ("olive oil", 690.0, 1.0, "L", "Supermart"), | |
| ("curd", 48.0, 0.5, "kg", "Fresh Dairy"), | |
| ("bread", 55.0, 1.0, "loaf", "Apna Store"), | |
| ("potato", 30.0, 1.0, "kg", "Local Vendor"), | |
| ("butter", 120.0, 0.25, "kg", "Big Bazaar"), | |
| ("milk", 76.0, 1.0, "liter", "Fresh Dairy"), | |
| ("eggs", 88.0, 6.0, "pieces", "Local Vendor"), | |
| ] | |
| SHOPPING_LIST_ITEMS = [ | |
| {"canonical_name": "eggs", "requested_quantity": 6, "unit": "pieces", "priority": "must_buy", "reason": "Only a few left, need for weekend cooking"}, | |
| {"canonical_name": "bread", "requested_quantity": 1, "unit": "loaf", "priority": "must_buy", "reason": "Almost finished"}, | |
| {"canonical_name": "milk", "requested_quantity": 2, "unit": "liter", "priority": "must_buy", "reason": "Running low, 2L for the week"}, | |
| {"canonical_name": "tomato", "requested_quantity": 1, "unit": "kg", "priority": "must_buy", "reason": "No tomatoes in stock"}, | |
| {"canonical_name": "cheese", "requested_quantity": 1, "unit": "pack", "priority": "optional", "reason": "Nice to have for sandwiches"}, | |
| {"canonical_name": "coriander", "requested_quantity": 1, "unit": "bunch", "priority": "optional", "reason": "Fresh garnish for dinner"}, | |
| ] | |
| def seed_inventory(db: Database, tools: ToolRegistry) -> int: | |
| existing = db.get_inventory() | |
| if existing: | |
| return 0 | |
| count = 0 | |
| for item in DEMO_ITEMS: | |
| tools.add_inventory_item( | |
| canonical_name=item["canonical_name"], | |
| display_name=item["display_name"], | |
| quantity=item["quantity"], | |
| unit=item["unit"], | |
| storage_location_id=item["location"], | |
| category=item["category"], | |
| price_paid=item["price"], | |
| source_event_id="demo_walkthrough", | |
| ) | |
| count += 1 | |
| return count | |
| def seed_prices(db: Database, tools: ToolRegistry) -> int: | |
| count = 0 | |
| for canonical, price, qty, unit, store in PRICE_OBSERVATIONS: | |
| tools.record_price_observation( | |
| canonical_name=canonical, | |
| price=price, | |
| quantity=qty, | |
| unit=unit, | |
| store_name=store, | |
| ) | |
| count += 1 | |
| return count | |
| def create_shopping_list(db: Database, tools: ToolRegistry) -> str | None: | |
| existing = db.get_active_shopping_list() | |
| if existing: | |
| return existing.list_id | |
| result = tools.create_or_update_shopping_list( | |
| goal="Weekly grocery restock", | |
| items=SHOPPING_LIST_ITEMS, | |
| ) | |
| return result["list"]["list_id"] | |
| def show_classification(db: Database, tools: ToolRegistry) -> None: | |
| decision_set = classify_all(db, tools) | |
| buy = len(decision_set.buy) | |
| skip = len(decision_set.skip) | |
| optional = len(decision_set.optional) | |
| use_soon = len(decision_set.use_soon) | |
| print(f" Total items evaluated: {len(decision_set.decisions)}") | |
| print(f" {'Buy':12s}{buy}") | |
| print(f" {'Skip':12s}{skip}") | |
| print(f" {'Optional':12s}{optional}") | |
| print(f" {'Use Soon':12s}{use_soon}") | |
| relevant = [d for d in decision_set.decisions if d.shopping_list_status == "on_list"] | |
| if relevant: | |
| print() | |
| for d in relevant: | |
| icon = {"buy": "β", "skip": "β", "optional": "?"}.get(d.action, " ") | |
| print(f" [{icon}] {d.display_name:25s} β {d.action:10s} {d.reason[:65]}") | |
| def show_inventory_summary(db: Database) -> None: | |
| all_inv = db.get_inventory() | |
| active = [lot for lot in all_inv if lot.status == "active"] | |
| total_qty = sum(lot.quantity for lot in active) | |
| categories = sorted({lot.category for lot in active if lot.category}) | |
| locations = sorted({lot.storage_location_id for lot in active if lot.storage_location_id}) | |
| print(f" Active lots: {len(active)}") | |
| print(f" Total units: {total_qty:.1f}") | |
| print(f" Categories: {', '.join(categories) if categories else 'none'}") | |
| print(f" Locations: {', '.join(locations) if locations else 'none'}") | |
| price_count = db.conn.execute("SELECT COUNT(*) FROM price_observations").fetchone()[0] | |
| print(f" Price obs: {price_count}") | |
| def show_shopping_list_summary(db: Database) -> None: | |
| active = db.get_active_shopping_list() | |
| if not active: | |
| print(" No active shopping list.") | |
| return | |
| pending = [i for i in active.items if i.status == "pending"] | |
| must_buy = [i for i in pending if i.priority == "must_buy"] | |
| optional = [i for i in pending if i.priority == "optional"] | |
| print(f" Goal: {active.goal or '(none)'}") | |
| print(f" Items: {len(active.items)} total, {len(pending)} pending") | |
| print(f" Must-buy: {len(must_buy)} | Optional: {len(optional)}") | |
| for item in pending: | |
| qty_s = f"{item.requested_quantity} {item.unit}" if item.requested_quantity and item.unit else "" | |
| pri = {"must_buy": "!", "optional": "?"}.get(item.priority, " ") | |
| print(f" [{pri}] {item.canonical_name:25s} {qty_s:15s} {item.reason[:40]}") | |
| def main() -> None: | |
| db = Database(settings.db_path) | |
| tools = ToolRegistry(db) | |
| section_gap = " " + "β" * 56 | |
| print() | |
| print(" ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ") | |
| print(" β ShopStack β Demo Walkthrough β") | |
| print(" ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ") | |
| print() | |
| print(" Step 1 β Seed demo inventory") | |
| print(section_gap) | |
| inv_count = seed_inventory(db, tools) | |
| if inv_count: | |
| print(f" Added {inv_count} items (milk, rice, eggs, onion, ...)") | |
| else: | |
| print(" Inventory already populated β reusing existing data") | |
| print() | |
| print(" Step 2 β Seed price observations") | |
| print(section_gap) | |
| price_count = seed_prices(db, tools) | |
| print(f" Recorded {price_count} price observations across stores") | |
| print() | |
| print(" Step 3 β Create shopping list") | |
| print(section_gap) | |
| list_id = create_shopping_list(db, tools) | |
| active = db.get_active_shopping_list() | |
| item_count = len(active.items) if active and active.items else 0 | |
| print(f" Active shopping list ({list_id[:8]}...) with {item_count} items") | |
| if active and active.items: | |
| for item in active.items: | |
| pri = "β‘" if item.priority == "must_buy" else "Β·" | |
| qty_s = f"{item.requested_quantity} {item.unit}" if item.requested_quantity and item.unit else "" | |
| print(f" {pri} {item.canonical_name:22s} {qty_s:12s}") | |
| print() | |
| print(" Step 4 β Classify list against inventory") | |
| print(section_gap) | |
| show_classification(db, tools) | |
| print() | |
| print(" Step 5 β Summary") | |
| print(section_gap) | |
| show_inventory_summary(db) | |
| print() | |
| show_shopping_list_summary(db) | |
| print() | |
| print(" " + "β" * 56) | |
| print(" Demo walkthrough complete.") | |
| print(f" Database: {db.db_path}") | |
| print() | |
| db.close() | |
| if __name__ == "__main__": | |
| main() | |