shopstack / scripts /demo_walkthrough.py
pranaysuyash's picture
Sync ShopStack HEAD 6f8adfc
d999bba verified
Raw
History Blame Contribute Delete
10.2 kB
"""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()