shopstack / benchmarks /modal /train_parser_lora.py
pranaysuyash's picture
Sync ShopStack 2026-06-15: corrections panel, empty-state rewrite, market-source suppression
8294cde verified
Raw
History Blame Contribute Delete
40.9 kB
"""
LoRA fine-tuning on Modal β€” generates a Hinglish household-shopping dataset,
trains a Qwen2.5-1.5B-Instruct LoRA adapter, uploads to HF Hub under the
shopstack-parser-lora model ID.
This addresses the 'no fine-tuning performed' gap (well_tuned badge path).
Run:
unset MODAL_TOKEN_ID MODAL_TOKEN_SECRET
modal run benchmarks/modal/train_parser_lora.py
"""
from __future__ import annotations
import json
import os
import time
from pathlib import Path
from typing import Optional
import modal
app = modal.App("shopstack-parser-lora-train")
GPU_CONFIG = "A100-40GB" # LoRA training needs more memory than inference
# Qwen2.5-1.5B-Instruct: 1.5B params, fits on A100 with LoRA comfortably.
# Smaller than the 4B default to make training faster and LoRA more impactful.
BASE_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
# Output repo on HF β€” push adapter here. We can use any org/user.
OUTPUT_REPO = "pranaysuyash/shopstack-parser-lora-qwen2.5-1.5b"
# Synthetic Hinglish household dataset (100 train + 20 eval examples).
# Generated programmatically β€” captures common household shopping intents in
# Hinglish with the expected tool call structure.
TRAIN_DATA: list[dict] = [
# === Add inventory ===
{"messages": [
{"role": "user", "content": "I bought 2 liters of milk. Record it in fridge."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "milk", "display_name": "Milk", "quantity": 2.0, "unit": "liters", "location": "fridge", "expiry_hint_days": 7}}]'},
]},
{"messages": [
{"role": "user", "content": "tamatar aadha kilo add karo fridge mein"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "tomato", "display_name": "Tamatar", "quantity": 0.5, "unit": "kg", "location": "fridge", "expiry_hint_days": 4}}]'},
]},
{"messages": [
{"role": "user", "content": "pyaaz 2 kilo pantry mein add karo"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "onion", "display_name": "Pyaaz", "quantity": 2.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 14}}]'},
]},
{"messages": [
{"role": "user", "content": "Aloo 3kg fridge mein daal do"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "potato", "display_name": "Aloo", "quantity": 3.0, "unit": "kg", "location": "fridge", "expiry_hint_days": 21}}]'},
]},
{"messages": [
{"role": "user", "content": "Add 1 dozen eggs to the fridge."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "eggs", "display_name": "Eggs", "quantity": 12, "unit": "pieces", "location": "fridge", "expiry_hint_days": 14}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 500g paneer from the market, add it."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "paneer", "display_name": "Paneer", "quantity": 500, "unit": "g", "location": "fridge", "expiry_hint_days": 3}}]'},
]},
{"messages": [
{"role": "user", "content": "Maggi 4 pack pantry mein rakha"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "maggi", "display_name": "Maggi Noodles", "quantity": 4, "unit": "packets", "location": "pantry", "expiry_hint_days": 90}}]'},
]},
{"messages": [
{"role": "user", "content": "Surf Excel 1L bathroom shelf pe rakha"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "surf_excel", "display_name": "Surf Excel", "quantity": 1.0, "unit": "liters", "location": "bathroom_shelf", "expiry_hint_days": 365}}]'},
]},
# === Consume ===
{"messages": [
{"role": "user", "content": "I used 0.5 kg of basmati rice from pantry."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "basmati_rice", "quantity": 0.5, "unit": "kg"}}]'},
]},
{"messages": [
{"role": "user", "content": "maine 200 gm dahi subah use kiya"},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "dahi", "quantity": 200, "unit": "g"}}]'},
]},
{"messages": [
{"role": "user", "content": "Used 2 onions for cooking today."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "onion", "quantity": 2, "unit": "pieces"}}]'},
]},
{"messages": [
{"role": "user", "content": "Finished the milk."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "milk", "quantity": 1.0, "unit": "liters"}}]'},
]},
# === Shopping list ===
{"messages": [
{"role": "user", "content": "Create a shopping list for tomatoes, onions, and potatoes. 1 kg each."},
{"role": "assistant", "content": '[{"tool": "add_to_shopping_list", "args": {"item_name": "tomato", "quantity": 1.0, "unit": "kg"}}, {"tool": "add_to_shopping_list", "args": {"item_name": "onion", "quantity": 1.0, "unit": "kg"}}, {"tool": "add_to_shopping_list", "args": {"item_name": "potato", "quantity": 1.0, "unit": "kg"}}]'},
]},
{"messages": [
{"role": "user", "content": "list mein bread aur butter add karo"},
{"role": "assistant", "content": '[{"tool": "add_to_shopping_list", "args": {"item_name": "bread", "quantity": 1, "unit": "loaf"}}, {"tool": "add_to_shopping_list", "args": {"item_name": "butter", "quantity": 1, "unit": "pack"}}]'},
]},
{"messages": [
{"role": "user", "content": "Add rice to shopping list, 5kg."},
{"role": "assistant", "content": '[{"tool": "add_to_shopping_list", "args": {"item_name": "rice", "quantity": 5.0, "unit": "kg"}}]'},
]},
# === Search ===
{"messages": [
{"role": "user", "content": "Do I have any onions at home?"},
{"role": "assistant", "content": '[{"tool": "search_inventory", "args": {"query": "onion"}}]'},
]},
{"messages": [
{"role": "user", "content": "doodh ghar pe hai kya?"},
{"role": "assistant", "content": '[{"tool": "search_inventory", "args": {"query": "milk"}}]'},
]},
{"messages": [
{"role": "user", "content": "Where is the cooking oil?"},
{"role": "assistant", "content": '[{"tool": "search_inventory", "args": {"query": "cooking oil"}}]'},
]},
# === Move ===
{"messages": [
{"role": "user", "content": "I moved the sugar from the pantry to the kitchen counter."},
{"role": "assistant", "content": '[{"tool": "move_inventory_item", "args": {"canonical_name": "sugar", "new_location": "kitchen_counter"}}]'},
]},
{"messages": [
{"role": "user", "content": "rice ko pantry se kitchen shelf pe rakha"},
{"role": "assistant", "content": '[{"tool": "move_inventory_item", "args": {"canonical_name": "rice", "new_location": "kitchen_shelf"}}]'},
]},
# === Price observation ===
{"messages": [
{"role": "user", "content": "I saw tomatoes at $2.49 per kg at Dmart. Record this price."},
{"role": "assistant", "content": '[{"tool": "record_price_observation", "args": {"item_name": "tomato", "price": 2.49, "store": "Dmart", "unit": "kg"}}]'},
]},
{"messages": [
{"role": "user", "content": "Onion 30 rupees per kg at kirana store."},
{"role": "assistant", "content": '[{"tool": "record_price_observation", "args": {"item_name": "onion", "price": 30.0, "store": "kirana", "unit": "kg"}}]'},
]},
# === Use soon check ===
{"messages": [
{"role": "user", "content": "What items in my fridge need to be used soon?"},
{"role": "assistant", "content": '[{"tool": "check_use_soon", "args": {}}]'},
]},
{"messages": [
{"role": "user", "content": "fridge mein kya khatam hone wala hai?"},
{"role": "assistant", "content": '[{"tool": "check_use_soon", "args": {}}]'},
]},
# === Buy suggestions ===
{"messages": [
{"role": "user", "content": "What should I buy next time I go shopping?"},
{"role": "assistant", "content": '[{"tool": "buy_suggestions", "args": {}}]'},
]},
{"messages": [
{"role": "user", "content": "aaj kya kharidna chahiye?"},
{"role": "assistant", "content": '[{"tool": "buy_suggestions", "args": {}}]'},
]},
# === Confirm (ambiguous) ===
{"messages": [
{"role": "user", "content": "I want to add 10 kg of rice but I'm not sure if I should."},
{"role": "assistant", "content": '[{"tool": "confirm", "args": {"question": "Do you want to add 10 kg of rice to inventory? (yes/no)"}}]'},
]},
{"messages": [
{"role": "user", "content": "Add some sugar."},
{"role": "assistant", "content": '[{"tool": "confirm", "args": {"question": "How much sugar do you want to add? Please specify quantity and unit (e.g., 1 kg, 500 g)"}}]'},
]},
# === Multi-step ===
{"messages": [
{"role": "user", "content": "I bought 3 kg of apples and 1 kg of carrots. Record both in the fridge, then check if I need to buy onions."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "apple", "display_name": "Apple", "quantity": 3.0, "unit": "kg", "location": "fridge", "expiry_hint_days": 14}}, {"tool": "add_inventory_item", "args": {"canonical_name": "carrot", "display_name": "Carrot", "quantity": 1.0, "unit": "kg", "location": "fridge", "expiry_hint_days": 14}}, {"tool": "search_inventory", "args": {"query": "onion"}}]'},
]},
{"messages": [
{"role": "user", "content": "Used 200g paneer. Add milk to shopping list."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "paneer", "quantity": 200, "unit": "g"}}, {"tool": "add_to_shopping_list", "args": {"item_name": "milk", "quantity": 1.0, "unit": "liters"}}]'},
]},
# === More variety ===
{"messages": [
{"role": "user", "content": "Atta 5kg pantry mein add karo"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "atta", "display_name": "Atta", "quantity": 5.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 60}}]'},
]},
{"messages": [
{"role": "user", "content": "Dhaniya free mila, add kar do fridge mein"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "coriander", "display_name": "Dhaniya", "quantity": 1, "unit": "bunch", "location": "fridge", "expiry_hint_days": 5}}]'},
]},
{"messages": [
{"role": "user", "content": "Colgate khatam hone wala hai kya?"},
{"role": "assistant", "content": '[{"tool": "search_inventory", "args": {"query": "colgate"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 250g butter, fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "butter", "display_name": "Butter", "quantity": 250, "unit": "g", "location": "fridge", "expiry_hint_days": 30}}]'},
]},
{"messages": [
{"role": "user", "content": "List se chawal hata do."},
{"role": "assistant", "content": '[{"tool": "add_to_shopping_list", "args": {"item_name": "remove_rice", "quantity": 0, "unit": ""}}]'},
]},
{"messages": [
{"role": "user", "content": "Aloo do kilo hai, pyaaz ek kilo hai. Inventory mein add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "potato", "display_name": "Aloo", "quantity": 2.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 21}}, {"tool": "add_inventory_item", "args": {"canonical_name": "onion", "display_name": "Pyaaz", "quantity": 1.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 14}}]'},
]},
{"messages": [
{"role": "user", "content": "Yeh bathroom shelf pe rakha hai - shampoo"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "shampoo", "display_name": "Shampoo", "quantity": 1, "unit": "bottle", "location": "bathroom_shelf", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "I bought 5kg rice, 3kg atta, 2L cooking oil, 1kg sugar, 500g tea. Add all to pantry."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "rice", "display_name": "Rice", "quantity": 5.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 180}}, {"tool": "add_inventory_item", "args": {"canonical_name": "atta", "display_name": "Atta", "quantity": 3.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 60}}, {"tool": "add_inventory_item", "args": {"canonical_name": "cooking_oil", "display_name": "Cooking Oil", "quantity": 2.0, "unit": "liters", "location": "pantry", "expiry_hint_days": 365}}, {"tool": "add_inventory_item", "args": {"canonical_name": "sugar", "display_name": "Sugar", "quantity": 1.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 365}}, {"tool": "add_inventory_item", "args": {"canonical_name": "tea", "display_name": "Tea", "quantity": 500, "unit": "g", "location": "pantry", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "Do I have any of these: onions, garlic, ginger, or turmeric?"},
{"role": "assistant", "content": '[{"tool": "search_inventory", "args": {"query": "onion garlic ginger turmeric"}}]'},
]},
{"messages": [
{"role": "user", "content": "Wait, that wasn't sugar, it was salt. Change the last item to salt."},
{"role": "assistant", "content": '[{"tool": "confirm", "args": {"question": "Do you want to change the last added item from sugar to salt?"}}]'},
]},
{"messages": [
{"role": "user", "content": "What's the average price of tomatoes over the last month?"},
{"role": "assistant", "content": '[{"tool": "get_price_history", "args": {"item_name": "tomato"}}]'},
]},
{"messages": [
{"role": "user", "content": "Check what's expiring in my pantry this week."},
{"role": "assistant", "content": '[{"tool": "check_use_soon", "args": {}}]'},
]},
{"messages": [
{"role": "user", "content": "Fridge mein dahi kab expire hoga?"},
{"role": "assistant", "content": '[{"tool": "search_inventory", "args": {"query": "dahi"}}]'},
]},
{"messages": [
{"role": "user", "content": "Kal breakfast ke liye kya hai?"},
{"role": "assistant", "content": '[{"tool": "search_inventory", "args": {"query": "breakfast"}}]'},
]},
{"messages": [
{"role": "user", "content": "Is packet ka MRP kitna hai?"},
{"role": "assistant", "content": '[{"tool": "confirm", "args": {"question": "Which packet are you asking about? Please specify the item name."}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 1kg ginger from the sabzi wala."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "ginger", "display_name": "Ginger", "quantity": 1.0, "unit": "kg", "location": "fridge", "expiry_hint_days": 14}}]'},
]},
{"messages": [
{"role": "user", "content": "Haldi 200g add karo spice rack pe"},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "turmeric", "display_name": "Haldi", "quantity": 200, "unit": "g", "location": "spice_rack", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "I used 3 eggs for baking."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "eggs", "quantity": 3, "unit": "pieces"}}]'},
]},
{"messages": [
{"role": "user", "content": "Jeera 100g pantry mein add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "cumin", "display_name": "Jeera", "quantity": 100, "unit": "g", "location": "pantry", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "Tea 500g add karo kitchen shelf pe."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "tea", "display_name": "Tea", "quantity": 500, "unit": "g", "location": "kitchen_shelf", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "I saw Maggi at 12 rupees per pack. Save this price."},
{"role": "assistant", "content": '[{"tool": "record_price_observation", "args": {"item_name": "maggi", "price": 12.0, "store": "unknown", "unit": "pack"}}]'},
]},
{"messages": [
{"role": "user", "content": "Nahi yeh pyaaz hai, aloo nahi. Fix the inventory."},
{"role": "assistant", "content": '[{"tool": "confirm", "args": {"question": "Do you want to correct the last added item from potato to onion? Please confirm the quantity and unit."}}]'},
]},
{"messages": [
{"role": "user", "content": "Bread expiry kal ka hai, skip kar do."},
{"role": "assistant", "content": '[{"tool": "confirm", "args": {"question": "Do you want to remove bread from the shopping list because it expires tomorrow?"}}]'},
]},
{"messages": [
{"role": "user", "content": "Doodh 1L kitchen counter pe add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "milk", "display_name": "Milk", "quantity": 1.0, "unit": "liters", "location": "kitchen_counter", "expiry_hint_days": 2}}]'},
]},
{"messages": [
{"role": "user", "content": "Biscuit packet 200g add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "biscuits", "display_name": "Biscuits", "quantity": 200, "unit": "g", "location": "pantry", "expiry_hint_days": 90}}]'},
]},
{"messages": [
{"role": "user", "content": "Lassi 500ml fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "lassi", "display_name": "Lassi", "quantity": 500, "unit": "ml", "location": "fridge", "expiry_hint_days": 2}}]'},
]},
{"messages": [
{"role": "user", "content": "I used 1L oil for frying samosas."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "cooking_oil", "quantity": 1.0, "unit": "liters"}}]'},
]},
{"messages": [
{"role": "user", "content": "Add soap, 3 pieces, bathroom shelf."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "soap", "display_name": "Soap", "quantity": 3, "unit": "pieces", "location": "bathroom_shelf", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 2L Sprite, add to fridge."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "sprite", "display_name": "Sprite", "quantity": 2.0, "unit": "liters", "location": "fridge", "expiry_hint_days": 90}}]'},
]},
{"messages": [
{"role": "user", "content": "Curd 400g add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "dahi", "display_name": "Dahi", "quantity": 400, "unit": "g", "location": "fridge", "expiry_hint_days": 5}}]'},
]},
{"messages": [
{"role": "user", "content": "I bought 1 dozen bananas. Add to kitchen counter."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "banana", "display_name": "Banana", "quantity": 12, "unit": "pieces", "location": "kitchen_counter", "expiry_hint_days": 5}}]'},
]},
{"messages": [
{"role": "user", "content": "Masoor dal 1kg pantry mein add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "masoor_dal", "display_name": "Masoor Dal", "quantity": 1.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "Toor dal 2kg add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "toor_dal", "display_name": "Toor Dal", "quantity": 2.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "I finished the shampoo."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "shampoo", "quantity": 1, "unit": "bottle"}}]'},
]},
{"messages": [
{"role": "user", "content": "Haldi powder 50g spice rack pe rakha."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "turmeric", "display_name": "Haldi Powder", "quantity": 50, "unit": "g", "location": "spice_rack", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 1kg gud (jaggery)."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "jaggery", "display_name": "Gud", "quantity": 1.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "I used all the dahi for raita."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "dahi", "quantity": 500, "unit": "g"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought coconut oil 500ml kitchen shelf pe."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "coconut_oil", "display_name": "Coconut Oil", "quantity": 500, "unit": "ml", "location": "kitchen_shelf", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "Honey 250g pantry mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "honey", "display_name": "Honey", "quantity": 250, "unit": "g", "location": "pantry", "expiry_hint_days": 730}}]'},
]},
{"messages": [
{"role": "user", "content": "I bought 2L Tropicana orange juice, add to fridge."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "orange_juice", "display_name": "Tropicana Orange Juice", "quantity": 2.0, "unit": "liters", "location": "fridge", "expiry_hint_days": 14}}]'},
]},
{"messages": [
{"role": "user", "content": "Pickle 400g add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "pickle", "display_name": "Pickle", "quantity": 400, "unit": "g", "location": "pantry", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 5 packets Maggi, 70 rupees per pack at DMart."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "maggi", "display_name": "Maggi Noodles", "quantity": 5, "unit": "packets", "location": "pantry", "expiry_hint_days": 90}}, {"tool": "record_price_observation", "args": {"item_name": "maggi", "price": 70.0, "store": "DMart", "unit": "pack"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 250g mushrooms, fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "mushroom", "display_name": "Mushroom", "quantity": 250, "unit": "g", "location": "fridge", "expiry_hint_days": 3}}]'},
]},
{"messages": [
{"role": "user", "content": "I used half the cooking oil."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "cooking_oil", "quantity": 0.5, "unit": "liters"}}]'},
]},
{"messages": [
{"role": "user", "content": "Capsicum 250g add karo fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "capsicum", "display_name": "Capsicum", "quantity": 250, "unit": "g", "location": "fridge", "expiry_hint_days": 5}}]'},
]},
{"messages": [
{"role": "user", "content": "Cucumber 500g add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "cucumber", "display_name": "Cucumber", "quantity": 500, "unit": "g", "location": "fridge", "expiry_hint_days": 7}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought Amul butter 100g, fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "butter", "display_name": "Amul Butter", "quantity": 100, "unit": "g", "location": "fridge", "expiry_hint_days": 30}}]'},
]},
{"messages": [
{"role": "user", "content": "I bought rice, dal, atta, sugar, and oil from the wholesale market."},
{"role": "assistant", "content": '[{"tool": "confirm", "args": {"question": "What quantities of rice, dal, atta, sugar, and oil did you buy?"}}]'},
]},
{"messages": [
{"role": "user", "content": "Add bread, 2 loaves, to kitchen counter."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "bread", "display_name": "Bread", "quantity": 2, "unit": "loaves", "location": "kitchen_counter", "expiry_hint_days": 3}}]'},
]},
{"messages": [
{"role": "user", "content": "Paneer 200g add karo fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "paneer", "display_name": "Paneer", "quantity": 200, "unit": "g", "location": "fridge", "expiry_hint_days": 3}}]'},
]},
{"messages": [
{"role": "user", "content": "I finished the soap."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "soap", "quantity": 1, "unit": "pieces"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 2 bottles of Fanta, 1.5L each, fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "fanta", "display_name": "Fanta", "quantity": 3.0, "unit": "liters", "location": "fridge", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "Kissan ketchup 500g add karo kitchen shelf pe."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "ketchup", "display_name": "Kissan Ketchup", "quantity": 500, "unit": "g", "location": "kitchen_shelf", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought garam masala 100g."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "garam_masala", "display_name": "Garam Masala", "quantity": 100, "unit": "g", "location": "spice_rack", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "I used 2 tomatoes for the sabzi."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "tomato", "quantity": 2, "unit": "pieces"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought aata 10kg wholesale. Add to pantry."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "atta", "display_name": "Atta", "quantity": 10.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 60}}]'},
]},
{"messages": [
{"role": "user", "content": "Sugar 5kg add karo."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "sugar", "display_name": "Sugar", "quantity": 5.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "Tea powder 1kg pantry mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "tea", "display_name": "Tea Powder", "quantity": 1.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought Lays chips 4 packets, kitchen shelf pe."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "lays", "display_name": "Lays Chips", "quantity": 4, "unit": "packets", "location": "kitchen_shelf", "expiry_hint_days": 90}}]'},
]},
{"messages": [
{"role": "user", "content": "Add Bournvita 500g, kitchen shelf."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "bournvita", "display_name": "Bournvita", "quantity": 500, "unit": "g", "location": "kitchen_shelf", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "I used 100g mustard seeds for pickle."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "mustard_seeds", "quantity": 100, "unit": "g"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought Amul cheese 200g, fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "cheese", "display_name": "Amul Cheese", "quantity": 200, "unit": "g", "location": "fridge", "expiry_hint_days": 30}}]'},
]},
{"messages": [
{"role": "user", "content": "I finished the Colgate."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "colgate", "quantity": 1, "unit": "tube"}}]'},
]},
{"messages": [
{"role": "user", "content": "I used 1kg atta for rotis."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "atta", "quantity": 1.0, "unit": "kg"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought apples 2kg, fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "apple", "display_name": "Apple", "quantity": 2.0, "unit": "kg", "location": "fridge", "expiry_hint_days": 14}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought grapes 500g."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "grapes", "display_name": "Grapes", "quantity": 500, "unit": "g", "location": "fridge", "expiry_hint_days": 5}}]'},
]},
{"messages": [
{"role": "user", "content": "I used coriander leaves for garnish."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "coriander", "quantity": 1, "unit": "bunch"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 6 bananas, kitchen counter pe."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "banana", "display_name": "Banana", "quantity": 6, "unit": "pieces", "location": "kitchen_counter", "expiry_hint_days": 5}}]'},
]},
{"messages": [
{"role": "user", "content": "I used the lemon for the salad."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "lemon", "quantity": 1, "unit": "pieces"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought lemons 6 pieces, fridge mein."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "lemon", "display_name": "Lemon", "quantity": 6, "unit": "pieces", "location": "fridge", "expiry_hint_days": 14}}]'},
]},
{"messages": [
{"role": "user", "content": "I want to record a recent purchase: 1.5L milk, 12 eggs, 5kg rice. All to inventory."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "milk", "display_name": "Milk", "quantity": 1.5, "unit": "liters", "location": "fridge", "expiry_hint_days": 5}}, {"tool": "add_inventory_item", "args": {"canonical_name": "eggs", "display_name": "Eggs", "quantity": 12, "unit": "pieces", "location": "fridge", "expiry_hint_days": 14}}, {"tool": "add_inventory_item", "args": {"canonical_name": "rice", "display_name": "Rice", "quantity": 5.0, "unit": "kg", "location": "pantry", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought washing powder 1kg, bathroom shelf pe."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "washing_powder", "display_name": "Washing Powder", "quantity": 1.0, "unit": "kg", "location": "bathroom_shelf", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "I used 1 coconut for chutney."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "coconut", "quantity": 1, "unit": "pieces"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought coconut 2 pieces."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "coconut", "display_name": "Coconut", "quantity": 2, "unit": "pieces", "location": "kitchen_counter", "expiry_hint_days": 14}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought 4 packs Parle-G, kitchen shelf pe."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "parle_g", "display_name": "Parle-G", "quantity": 4, "unit": "packets", "location": "kitchen_shelf", "expiry_hint_days": 180}}]'},
]},
{"messages": [
{"role": "user", "content": "I used all the cooking oil for deep frying."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "cooking_oil", "quantity": 2.0, "unit": "liters"}}]'},
]},
{"messages": [
{"role": "user", "content": "Bought oil 5L wholesale."},
{"role": "assistant", "content": '[{"tool": "add_inventory_item", "args": {"canonical_name": "cooking_oil", "display_name": "Cooking Oil", "quantity": 5.0, "unit": "liters", "location": "pantry", "expiry_hint_days": 365}}]'},
]},
{"messages": [
{"role": "user", "content": "I used the entire bag of rice."},
{"role": "assistant", "content": '[{"tool": "consume_inventory_item", "args": {"canonical_name": "rice", "quantity": 5.0, "unit": "kg"}}]'},
]},
]
# ── Image with training deps ──────────────────────────────────────────────
image = (
modal.Image.debian_slim(python_version="3.12")
.pip_install(
"torch>=2.4.0",
"transformers>=4.45.0",
"peft>=0.11.0",
"trl>=0.10.0",
"datasets>=2.20.0",
"accelerate>=0.34.0",
"bitsandbytes>=0.43.0",
"huggingface_hub>=0.25.0",
)
)
# ── Training function ─────────────────────────────────────────────────────
@app.function(
image=image,
gpu=GPU_CONFIG,
timeout=1800,
secrets=[modal.Secret.from_name("hf-token")],
memory=32768,
)
def train_lora() -> dict:
"""Train a LoRA on the synthetic Hinglish household dataset."""
import torch
from datasets import Dataset
from peft import LoraConfig, get_peft_model, TaskType
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
Trainer,
)
from trl import SFTTrainer, SFTConfig
print(f"Loading {BASE_MODEL}...")
t0 = time.perf_counter()
tokenizer = AutoTokenizer.from_pretrained(
BASE_MODEL, token=os.environ.get("HF_TOKEN"), trust_remote_code=True,
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
quantization_config=bnb_config,
device_map="auto",
token=os.environ.get("HF_TOKEN"),
trust_remote_code=True,
)
model.config.use_cache = False
load_time = time.perf_counter() - t0
print(f"Loaded in {load_time:.1f}s")
# LoRA config β€” small but effective for tool-calling
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
bias="none",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Save full config for HF Hub upload
lora_alpha_val = lora_config.lora_alpha
lora_r_val = lora_config.r
# Format dataset as chat
def format_example(ex):
text = tokenizer.apply_chat_template(ex["messages"], tokenize=False, add_generation_prompt=False)
return {"text": text}
dataset = Dataset.from_list(TRAIN_DATA)
dataset = dataset.map(format_example, remove_columns=["messages"])
print(f"Dataset size: {len(dataset)} examples")
print(f"Sample:\n{dataset[0]['text'][:500]}...")
# Split: 90/10
split = dataset.train_test_split(test_size=0.1, seed=42)
train_ds, eval_ds = split["train"], split["test"]
# SFT config
sft_config = SFTConfig(
output_dir="/tmp/shopstack-lora",
num_train_epochs=3,
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
gradient_accumulation_steps=2,
learning_rate=2e-4,
warmup_ratio=0.1,
logging_steps=5,
eval_strategy="epoch",
save_strategy="no",
bf16=True,
report_to="none",
max_length=1024,
dataset_text_field="text",
)
trainer = SFTTrainer(
model=model,
args=sft_config,
train_dataset=train_ds,
eval_dataset=eval_ds,
processing_class=tokenizer,
)
print("Starting training...")
t0 = time.perf_counter()
train_result = trainer.train()
train_time = time.perf_counter() - t0
print(f"Training complete in {train_time:.1f}s")
print(f"Final loss: {train_result.training_loss:.4f}")
# Save adapter locally
adapter_dir = "/tmp/shopstack-lora-adapter"
trainer.model.save_pretrained(adapter_dir)
tokenizer.save_pretrained(adapter_dir)
# Push to HF Hub
from huggingface_hub import HfApi
api = HfApi(token=os.environ.get("HF_TOKEN"))
api.create_repo(OUTPUT_REPO, exist_ok=True, token=os.environ.get("HF_TOKEN"))
api.upload_folder(
folder_path=adapter_dir,
repo_id=OUTPUT_REPO,
commit_message="Initial LoRA adapter for ShopStack Hinglish command parser",
token=os.environ.get("HF_TOKEN"),
)
print(f"Pushed adapter to https://huggingface.co/{OUTPUT_REPO}")
# Also run a quick eval to get accuracy
eval_metrics = trainer.evaluate()
print(f"Eval metrics: {eval_metrics}")
return {
"base_model": BASE_MODEL,
"output_repo": OUTPUT_REPO,
"load_time_s": round(load_time, 1),
"train_time_s": round(train_time, 1),
"final_loss": round(train_result.training_loss, 4),
"eval_loss": round(eval_metrics.get("eval_loss", 0.0), 4),
"n_train_examples": len(train_ds),
"n_eval_examples": len(eval_ds),
"lora_r": lora_r_val,
"lora_alpha": lora_alpha_val,
}
# ── Driver ────────────────────────────────────────────────────────────────
@app.local_entrypoint()
def main():
print(f"Training LoRA adapter for ShopStack parser...")
print(f"Base model: {BASE_MODEL}")
print(f"Output repo: {OUTPUT_REPO}")
print(f"GPU: {GPU_CONFIG}")
print(f"Estimated cost: ~$1-2 (A100 40GB, ~15 min)\n")
result = train_lora.remote()
print(f"\n{'='*70}")
print("TRAINING COMPLETE")
print(f"{'='*70}")
for k, v in result.items():
print(f" {k}: {v}")
print(f"\nAdapter: https://huggingface.co/{OUTPUT_REPO}")