shopstack / scripts /seed_demo.py
pranaysuyash's picture
Sync ShopStack HEAD 6f8adfc
d999bba verified
Raw
History Blame Contribute Delete
21.9 kB
#!/usr/bin/env python3
"""Seed ShopStack with realistic Indian household demo data.
Canonical run path (preferred):
uv run python scripts/seed_demo.py [--db PATH]
.venv/bin/python scripts/seed_demo.py [--db PATH]
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; doing so would mask
broken installs and hide the canonical dependency declaration in
``setup.py``.
Seeds:
- 30+ inventory items across all locations (fridge, pantry, spice box, etc.)
- 5 stores (Reliance Fresh, DMart, local kirana, etc.)
- 20+ price observations across stores for price intelligence
- Active shopping list with mixed priorities
- 10+ purchase events
- 5+ agent traces (market lens, shopping list, ask)
- Low-stock and expiring-soon items for dashboard alerts
"""
from __future__ import annotations
import argparse
import os
from datetime import date, datetime, timedelta
from shopstack.config import settings
from shopstack.persistence.database import Database
from shopstack.schemas.models import (
InventoryLot,
PriceObservation,
PurchaseEvent,
ShoppingListItem,
Store,
)
from shopstack.traces.export import create_trace
def seed_stores(db: Database) -> int:
stores = [
Store(store_id="reliance_fresh", name="Reliance Fresh", location="Koramangala", store_type="supermarket"),
Store(store_id="dmart", name="DMart", location="HSR Layout", store_type="supermarket"),
Store(store_id="kirana_1", name="Sharma Kirana", location="5th Block", store_type="kirana"),
Store(store_id="big_basket", name="BigBasket", location="Online", store_type="online"),
Store(store_id="local_vendor", name="Green Vegetable Cart", location="Street Corner", store_type="vendor"),
]
for s in stores:
existing = db.conn.execute("SELECT 1 FROM stores WHERE store_id = ?", (s.store_id,)).fetchone()
if not existing:
db.add_store(s)
return len(stores)
def seed_inventory(db: Database) -> int:
today = date.today()
yesterday = today - timedelta(days=1)
last_week = today - timedelta(days=7)
two_weeks_ago = today - timedelta(days=14)
items = [
# --- Fridge ---
{"canonical_name": "milk", "display_name": "Amul Taaza Milk", "quantity": 1.5, "unit": "L",
"location": "fridge", "category": "dairy", "price": 64.0, "store": "kirana_1",
"purchase_date": yesterday, "expiry": today + timedelta(days=3)},
{"canonical_name": "curd", "display_name": "Nestle Curd", "quantity": 0.4, "unit": "kg",
"location": "fridge", "category": "dairy", "price": 45.0, "store": "reliance_fresh",
"purchase_date": yesterday, "expiry": today + timedelta(days=2)},
{"canonical_name": "eggs", "display_name": "Country Eggs", "quantity": 8.0, "unit": "pieces",
"location": "fridge_top", "category": "protein", "price": 84.0, "store": "kirana_1",
"purchase_date": last_week},
{"canonical_name": "cheese", "display_name": "Amul Cheese Slices", "quantity": 1.0, "unit": "pack",
"location": "fridge", "category": "dairy", "price": 135.0, "store": "dmart",
"purchase_date": last_week, "expiry": today + timedelta(days=30)},
{"canonical_name": "butter", "display_name": "Amul Butter", "quantity": 0.5, "unit": "kg",
"location": "fridge", "category": "dairy", "price": 235.0, "store": "reliance_fresh",
"purchase_date": last_week},
{"canonical_name": "coriander", "display_name": "Fresh Coriander", "quantity": 1.0, "unit": "bunch",
"location": "fridge_drawer", "category": "vegetable", "price": 10.0, "store": "local_vendor",
"purchase_date": yesterday, "expiry": today + timedelta(days=2)},
{"canonical_name": "ginger", "display_name": "Ginger", "quantity": 0.2, "unit": "kg",
"location": "fridge_drawer", "category": "vegetable", "price": 12.0, "store": "local_vendor",
"purchase_date": last_week},
{"canonical_name": "green_chili", "display_name": "Green Chilies", "quantity": 0.1, "unit": "kg",
"location": "fridge_drawer", "category": "vegetable", "price": 8.0, "store": "local_vendor",
"purchase_date": yesterday},
{"canonical_name": "bread", "display_name": "Britannia Brown Bread", "quantity": 1.0, "unit": "loaf",
"location": "fridge_top", "category": "bakery", "price": 55.0, "store": "kirana_1",
"purchase_date": yesterday, "expiry": today + timedelta(days=4)},
# --- Pantry ---
{"canonical_name": "rice", "display_name": "India Gate Basmati", "quantity": 8.0, "unit": "kg",
"location": "pantry_mid", "category": "grains", "price": 680.0, "store": "dmart",
"purchase_date": two_weeks_ago},
{"canonical_name": "wheat_flour", "display_name": "Aashirvaad Atta", "quantity": 4.5, "unit": "kg",
"location": "pantry_mid", "category": "grains", "price": 295.0, "store": "reliance_fresh",
"purchase_date": last_week},
{"canonical_name": "toor_dal", "display_name": "Tata Toor Dal", "quantity": 0.8, "unit": "kg",
"location": "pantry_top", "category": "pulses", "price": 165.0, "store": "dmart",
"purchase_date": two_weeks_ago},
{"canonical_name": "moong_dal", "display_name": "Organic Moong Dal", "quantity": 0.5, "unit": "kg",
"location": "pantry_top", "category": "pulses", "price": 120.0, "store": "big_basket",
"purchase_date": two_weeks_ago},
{"canonical_name": "onion", "display_name": "Onion", "quantity": 2.0, "unit": "kg",
"location": "pantry_mid", "category": "vegetable", "price": 40.0, "store": "local_vendor",
"purchase_date": last_week},
{"canonical_name": "potato", "display_name": "Potato", "quantity": 1.5, "unit": "kg",
"location": "pantry_mid", "category": "vegetable", "price": 30.0, "store": "local_vendor",
"purchase_date": last_week},
{"canonical_name": "garlic", "display_name": "Garlic", "quantity": 0.3, "unit": "kg",
"location": "pantry", "category": "vegetable", "price": 35.0, "store": "local_vendor",
"purchase_date": two_weeks_ago},
{"canonical_name": "cooking_oil", "display_name": "Fortune Sunflower Oil", "quantity": 0.8, "unit": "L",
"location": "pantry_mid", "category": "cooking", "price": 165.0, "store": "dmart",
"purchase_date": two_weeks_ago},
{"canonical_name": "salt", "display_name": "Tata Salt", "quantity": 0.9, "unit": "kg",
"location": "pantry_top", "category": "staples", "price": 28.0, "store": "kirana_1",
"purchase_date": two_weeks_ago},
{"canonical_name": "sugar", "display_name": "Madhur Sugar", "quantity": 1.8, "unit": "kg",
"location": "pantry_top", "category": "staples", "price": 90.0, "store": "kirana_1",
"purchase_date": two_weeks_ago},
{"canonical_name": "tea_leaves", "display_name": "Tata Premium Tea", "quantity": 0.2, "unit": "kg",
"location": "pantry_top", "category": "beverage", "price": 160.0, "store": "reliance_fresh",
"purchase_date": two_weeks_ago},
{"canonical_name": "biscuits", "display_name": "Parle-G Biscuits", "quantity": 2.0, "unit": "pack",
"location": "pantry", "category": "snacks", "price": 20.0, "store": "kirana_1",
"purchase_date": last_week},
# --- Spice Box ---
{"canonical_name": "turmeric_powder", "display_name": "Turmeric Powder", "quantity": 0.1, "unit": "kg",
"location": "spice_box", "category": "spice", "price": 18.0, "store": "kirana_1",
"purchase_date": two_weeks_ago},
{"canonical_name": "chili_powder", "display_name": "Red Chili Powder", "quantity": 0.1, "unit": "kg",
"location": "spice_box", "category": "spice", "price": 22.0, "store": "kirana_1",
"purchase_date": two_weeks_ago},
{"canonical_name": "garam_masala", "display_name": "MDH Garam Masala", "quantity": 0.05, "unit": "kg",
"location": "spice_box", "category": "spice", "price": 55.0, "store": "reliance_fresh",
"purchase_date": two_weeks_ago},
{"canonical_name": "mustard_seeds", "display_name": "Mustard Seeds", "quantity": 0.05, "unit": "kg",
"location": "spice_box", "category": "spice", "price": 15.0, "store": "kirana_1",
"purchase_date": two_weeks_ago},
{"canonical_name": "cumin_seeds", "display_name": "Cumin Seeds (Jeera)", "quantity": 0.05, "unit": "kg",
"location": "spice_box", "category": "spice", "price": 20.0, "store": "kirana_1",
"purchase_date": two_weeks_ago},
# --- Bathroom ---
{"canonical_name": "toothpaste", "display_name": "Colgate Strong Teeth", "quantity": 1.0, "unit": "tube",
"location": "bathroom_cabinet", "category": "personal_care", "price": 99.0, "store": "dmart",
"purchase_date": two_weeks_ago},
{"canonical_name": "soap", "display_name": "Dove Cream Bar", "quantity": 3.0, "unit": "bar",
"location": "bathroom_cabinet", "category": "personal_care", "price": 42.0, "store": "dmart",
"purchase_date": two_weeks_ago},
{"canonical_name": "shampoo", "display_name": "Head & Shoulders", "quantity": 0.34, "unit": "L",
"location": "bathroom_cabinet", "category": "personal_care", "price": 185.0, "store": "reliance_fresh",
"purchase_date": two_weeks_ago},
# --- Low stock / expiring for dashboard alerts ---
{"canonical_name": "tomato", "display_name": "Tomato", "quantity": 0.3, "unit": "kg",
"location": "fridge_drawer", "category": "vegetable", "price": 18.0, "store": "local_vendor",
"purchase_date": last_week, "expiry": today + timedelta(days=1)},
{"canonical_name": "lemon", "display_name": "Lemon", "quantity": 2.0, "unit": "pieces",
"location": "fridge_drawer", "category": "vegetable", "price": 6.0, "store": "local_vendor",
"purchase_date": yesterday, "expiry": today + timedelta(days=5)},
]
existing = db.get_inventory()
if existing:
return 0
added = 0
for item in items:
lot = InventoryLot(
canonical_name=item["canonical_name"],
display_name=item["display_name"],
quantity=item["quantity"],
unit=item["unit"],
storage_location_id=item["location"],
purchase_date=item.get("purchase_date", today),
estimated_use_by_date=item.get("expiry"),
price_paid=item.get("price"),
category=item.get("category", ""),
source_event_id="demo_seed",
)
db.add_inventory_lot(lot)
added += 1
return added
def seed_prices(db: Database) -> int:
today = date.today()
last_week = today - timedelta(days=7)
two_weeks = today - timedelta(days=14)
three_weeks = today - timedelta(days=21)
prices = [
# Milk - price comparison across stores
("milk", 64.0, 1.0, "L", "kirana_1", "Sharma Kirana", today),
("milk", 68.0, 1.0, "L", "reliance_fresh", "Reliance Fresh", today),
("milk", 62.0, 1.0, "L", "dmart", "DMart", last_week),
("milk", 66.0, 1.0, "L", "big_basket", "BigBasket", last_week),
("milk", 60.0, 1.0, "L", "kirana_1", "Sharma Kirana", two_weeks),
("milk", 63.0, 1.0, "L", "dmart", "DMart", three_weeks),
# Rice - showing price trend
("rice", 680.0, 5.0, "kg", "dmart", "DMart", today),
("rice", 720.0, 5.0, "kg", "reliance_fresh", "Reliance Fresh", last_week),
("rice", 695.0, 5.0, "kg", "big_basket", "BigBasket", last_week),
("rice", 710.0, 5.0, "kg", "dmart", "DMart", two_weeks),
("rice", 730.0, 5.0, "kg", "reliance_fresh", "Reliance Fresh", three_weeks),
# Onion - volatile pricing
("onion", 40.0, 1.0, "kg", "local_vendor", "Green Vegetable Cart", today),
("onion", 35.0, 1.0, "kg", "dmart", "DMart", today),
("onion", 38.0, 1.0, "kg", "reliance_fresh", "Reliance Fresh", last_week),
("onion", 28.0, 1.0, "kg", "local_vendor", "Green Vegetable Cart", two_weeks),
("onion", 22.0, 1.0, "kg", "dmart", "DMart", three_weeks),
# Other items
("eggs", 84.0, 12.0, "pieces", "kirana_1", "Sharma Kirana", today),
("eggs", 90.0, 12.0, "pieces", "reliance_fresh", "Reliance Fresh", last_week),
("wheat_flour", 295.0, 5.0, "kg", "reliance_fresh", "Reliance Fresh", today),
("wheat_flour", 280.0, 5.0, "kg", "dmart", "DMart", last_week),
("cooking_oil", 165.0, 1.0, "L", "dmart", "DMart", today),
("cooking_oil", 175.0, 1.0, "L", "kirana_1", "Sharma Kirana", last_week),
("toor_dal", 165.0, 1.0, "kg", "dmart", "DMart", today),
("toor_dal", 180.0, 1.0, "kg", "big_basket", "BigBasket", last_week),
("tomato", 18.0, 1.0, "kg", "local_vendor", "Green Vegetable Cart", today),
("tomato", 22.0, 1.0, "kg", "reliance_fresh", "Reliance Fresh", last_week),
("potato", 30.0, 1.0, "kg", "local_vendor", "Green Vegetable Cart", today),
("potato", 28.0, 1.0, "kg", "dmart", "DMart", last_week),
]
added = 0
for canonical, price, qty, unit, store_id, store_name, obs_date in prices:
obs = PriceObservation(
canonical_name=canonical,
price=price,
quantity=qty,
unit=unit,
store_id=store_id,
store_name=store_name,
observation_date=obs_date,
notes="demo_seed",
)
db.record_price(obs)
added += 1
return added
def seed_shopping_list(db: Database) -> int:
existing = db.get_active_shopping_list()
if existing:
return 0
sl = db.create_shopping_list(name="Weekly Grocery Run", goal="Stock up for the week")
items = [
("milk", 2.0, "L", "must_buy", "Only 1.5L left, family uses 0.5L/day"),
("coriander", 2.0, "bunch", "must_buy", "Need fresh for weekend cooking"),
("tomato", 1.0, "kg", "must_buy", "Almost out, 0.3kg left"),
("bread", 1.0, "loaf", "optional", "Current loaf expires in 4 days"),
("toor_dal", 1.0, "kg", "must_buy", "Running low on dal"),
("biscuits", 3.0, "pack", "optional", "Kids want Parle-G"),
("toothpaste", 1.0, "tube", "must_buy", "Colgate almost empty"),
]
for canonical, qty, unit, priority, reason in items:
item = ShoppingListItem(
canonical_name=canonical,
requested_quantity=qty,
unit=unit,
priority=priority,
reason=reason,
)
db.add_list_item(sl.list_id, item)
return len(items)
def seed_purchase_events(db: Database) -> int:
today = datetime.now()
events = [
("milk", 1.0, "L", 64.0, "kirana_1", "Sharma Kirana", today - timedelta(days=1)),
("rice", 5.0, "kg", 680.0, "dmart", "DMart", today - timedelta(days=7)),
("eggs", 12.0, "pieces", 84.0, "kirana_1", "Sharma Kirana", today - timedelta(days=7)),
("onion", 2.0, "kg", 40.0, "local_vendor", "Green Vegetable Cart", today - timedelta(days=7)),
("wheat_flour", 5.0, "kg", 295.0, "reliance_fresh", "Reliance Fresh", today - timedelta(days=7)),
("cooking_oil", 1.0, "L", 165.0, "dmart", "DMart", today - timedelta(days=14)),
("toor_dal", 1.0, "kg", 165.0, "dmart", "DMart", today - timedelta(days=14)),
("toothpaste", 1.0, "tube", 99.0, "dmart", "DMart", today - timedelta(days=14)),
("butter", 0.5, "kg", 235.0, "reliance_fresh", "Reliance Fresh", today - timedelta(days=7)),
("bread", 1.0, "loaf", 55.0, "kirana_1", "Sharma Kirana", today - timedelta(days=1)),
]
added = 0
for canonical, qty, unit, price, store_id, store_name, ts in events:
event = PurchaseEvent(
canonical_name=canonical,
quantity=qty,
unit=unit,
total_price=price,
currency="INR",
source_type="manual",
store_name=store_name,
confirmed=True,
timestamp=ts,
)
db.add_purchase_event(event)
added += 1
return added
def seed_traces(db: Database) -> int:
traces_data = [
{
"input_type": "vision",
"user_goal": "market_lens",
"perception": {"items_detected": ["milk", "bread", "eggs"], "image": True, "audio": False},
"decision": {"decision_count": 3, "items": ["milk:buy", "bread:optional", "eggs:skip"]},
"tool_calls": [
{"tool_name": "compare_visible_item_to_inventory", "args": {"canonical_name": "milk", "quantity": 1.0, "unit": "unit"}},
{"tool_name": "compare_visible_item_to_inventory", "args": {"canonical_name": "bread", "quantity": 1.0, "unit": "unit"}},
{"tool_name": "compare_visible_item_to_inventory", "args": {"canonical_name": "eggs", "quantity": 1.0, "unit": "unit"}},
],
"response": "<div>Market Lens: 1 buy, 1 optional, 1 skip</div>",
"confirmation": "confirmed-buy",
},
{
"input_type": "text",
"user_goal": "Plan shopping for the week",
"perception": {"goal": "Weekly grocery run", "item_count": 7},
"decision": {"must_buy": 5, "optional": 2, "skip": 0},
"tool_calls": [{"tool_name": "create_or_update_shopping_list", "args": {
"items": [
{"canonical_name": "milk", "requested_quantity": 2, "unit": "L", "priority": "must_buy"},
{"canonical_name": "toothpaste", "requested_quantity": 1, "unit": "tube", "priority": "must_buy"},
],
"goal": "Weekly grocery run",
}}],
"response": "<div>Created shopping list with 7 items</div>",
"confirmation": "auto-summarized",
},
{
"input_type": "text",
"user_goal": "ask_shopstack",
"perception": {"query": "Do we have milk?"},
"decision": {"response_type": "ask_shopstack", "found": True},
"tool_calls": [{"tool_name": "find_item", "args": {"query": "milk"}}],
"response": "<div>Found Amul Taaza Milk (1.5L) in Fridge</div>",
"confirmation": "responded",
},
{
"input_type": "form",
"user_goal": "add_purchase",
"perception": {"item": "rice", "quantity": 8, "store": "DMart"},
"decision": {"action": "add_inventory_item", "lot_id": "demo_lot"},
"tool_calls": [{"tool_name": "add_inventory_item", "args": {"canonical_name": "rice", "quantity": 8, "unit": "kg"}}],
"response": "<div>Added India Gate Basmati (8kg)</div>",
"confirmation": "confirmed-by-user",
},
{
"input_type": "form",
"user_goal": "complete_shopping_list",
"perception": {"goal": "Weekly Grocery Run", "item_count": 7, "added_count": 5},
"decision": {"action": "mark_list_complete"},
"tool_calls": [],
"response": "<div>Completed list with 5 items added to inventory</div>",
"confirmation": "auto-confirmed",
},
]
added = 0
for t in traces_data:
create_trace(
db,
input_type=t["input_type"],
user_goal=t["user_goal"],
redacted_user_request=t["user_goal"],
perception=t["perception"],
inventory_context={},
decision=t["decision"],
proposed_tool_calls=t["tool_calls"],
final_response=t["response"],
human_confirmation=t["confirmation"],
)
added += 1
return added
def seed_field_notes(db: Database) -> None:
notes = """## Kitchen Notes
- Milk delivery: Sharma Kirana delivers by 7 AM, best to order previous night
- Tomato prices spike on weekends, buy from local vendor on weekdays
- DMart has best prices for staples (rice, dal, oil) but fresh produce is better at the cart
## Price Watch
- Onion prices trending up: ₹22 → ₹40 in 3 weeks
- Rice prices softening: ₹730 → ₹680 in 3 weeks
- Milk stable at ₹60-68 across stores
## Recipes This Week
- Dal chawal (need toor dal + rice)
- Tomato onion sabzi (need more tomatoes)
- Egg curry (eggs expiring soon, use by Thursday)
"""
db.set_config_value("field_notes_markdown", notes)
def main():
parser = argparse.ArgumentParser(description="Seed ShopStack demo data")
parser.add_argument("--db", default=None, help="Database path (default: settings.db_path)")
parser.add_argument("--reset", action="store_true", help="Clear existing data before seeding")
args = parser.parse_args()
db_path = args.db or settings.db_path
if args.reset:
for suffix in ("", "-shm", "-wal"):
f = db_path + suffix
if os.path.exists(f):
os.remove(f)
print(f"Cleared existing database: {db_path}")
db = Database(db_path)
print("Seeding ShopStack demo data...")
stores = seed_stores(db)
print(f" Stores: {stores}")
inv = seed_inventory(db)
print(f" Inventory: {inv} items")
prices = seed_prices(db)
print(f" Price observations: {prices}")
sl = seed_shopping_list(db)
print(f" Shopping list items: {sl}")
purchases = seed_purchase_events(db)
print(f" Purchase events: {purchases}")
traces = seed_traces(db)
print(f" Agent traces: {traces}")
seed_field_notes(db)
print(" Field notes: configured")
db.close()
print(f"\nDone! Database: {db_path}")
print("Run: uv run python app.py")
if __name__ == "__main__":
main()