import json import html import pandas as pd import gradio as gr import torch from transformers import AutoModelForCausalLM, AutoTokenizer MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct" ORDER_COLUMNS = [ "customer", "item", "quantity", "flavor", "pickup_time", "delivery_address", "payment_status", "notes", "missing_fields", ] tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch.float32) model.eval() SYSTEM_PROMPT = """ You are a careful order extraction engine for tiny sellers. Extract customer orders from messy DMs. Return only valid JSON with this exact shape: { "orders": [ { "customer": "", "item": "", "quantity": "", "flavor": "", "pickup_time": "", "delivery_address": "", "payment_status": "", "notes": "", "missing_fields": [] } ], "prep_list": [], "reply_drafts": [] } Critical rules: - Treat each line as one separate customer message. - The text before the first ":" is the customer name. - Copy customer names exactly as written. Do not uppercase or lowercase them. - Never copy details from one customer's message into another customer's order. - Include every customer message that looks like an order or possible order. - Use only facts explicitly present in that customer's own message. - If a value is unknown, use an empty string. - Do not add order_id or total_cost. - For pickup orders, put pickup time in pickup_time. Put a pickup place or delivery address in delivery_address. - If the customer is unsure, still include the order and describe the uncertainty in notes. - missing_fields should only include fields the seller needs to ask for: quantity, flavor, pickup_time, delivery_address, payment_status. - Always set prep_list to []. - Always set reply_drafts to []. """ SINGLE_ORDER_PROMPT = """ You extract one order from one customer's DM. Return only valid JSON with this exact shape: { "item": "", "quantity": "", "flavor": "", "pickup_time": "", "delivery_address": "", "payment_status": "", "notes": "", "missing_fields": [] } Rules: - Use only facts from this one message. - Do not invent details. - Put dates and times in pickup_time, such as "tomorrow", "Saturday morning", or "Friday 5pm". - Put pickup places or delivery addresses in delivery_address, such as "farmers market". - "pickup at the farmers market" means delivery_address is "farmers market", not pickup_time. - "paid already" means payment_status is "paid". - "I can pay Venmo" means payment_status is "can pay Venmo". - If unknown, use an empty string. - Do not ask for flavor unless the product clearly needs a flavor choice. - missing_fields can only contain: quantity, flavor, pickup_time, delivery_address, payment_status. """ EXAMPLE_INPUT = """Maya: Hi! Can I get 2 dozen cupcakes for Saturday morning? Half vanilla, half chocolate. Sam: Need 1 birthday cake, chocolate, for pickup Friday 5pm. I can pay Venmo. Lena: Do you still have lemon bars? I need some for tomorrow but not sure how many yet. Chris: 12 cookies please, pickup at the farmers market. Paid already. """ FOOD_TRUCK_EXAMPLE = """Alex: Can I get 3 chicken tacos for pickup at 6:30 tonight? Paid on Cash App. Jamie: Do you still have vegan bowls? Need 2 tomorrow for office lunch. Priya: One brisket sandwich, no onions. I'll pick up at the truck on Main Street. Nate: 4 lemonades for the soccer team, pickup after practice. """ CRAFT_SELLER_EXAMPLE = """Olivia: I want 2 custom mugs with blue initials. Can you ship to 18 Pine Road? Ben: Need one candle gift box for Saturday. Lavender if you have it. Rosa: Can I order 3 tote bags? I can pick up at the market. Eli: Do you still make birthday stickers? Need some next week but not sure how many. """ FARMERS_MARKET_EXAMPLE = """Grace: Can you hold 2 sourdough loaves for Sunday pickup? Leo: I need 1 jar of strawberry jam and 2 honey bottles. Paid already. Mina: Do you have eggs this weekend? Maybe 2 dozen if available. Noah: Please save me 3 bags of granola, pickup at the farmers market. """ EXAMPLES = [ [EXAMPLE_INPUT], [FOOD_TRUCK_EXAMPLE], [CRAFT_SELLER_EXAMPLE], [FARMERS_MARKET_EXAMPLE], ] def extract_json(text): start = text.find("{") end = text.rfind("}") if start == -1 or end == -1: raise ValueError("No JSON object found") return json.loads(text[start:end + 1]) def normalize_orders(data): rows = [] for order in data.get("orders", []): row = {} for col in ORDER_COLUMNS: value = order.get(col, "") if isinstance(value, list): value = ", ".join(str(v) for v in value) row[col] = value rows.append(row) return pd.DataFrame(rows, columns=ORDER_COLUMNS) def format_list(title, items): if not items: return f"### {title}\nNothing found." lines = [] for item in items: if isinstance(item, dict): lines.append("- " + json.dumps(item, ensure_ascii=False)) else: lines.append(f"- {item}") return f"### {title}\n" + "\n".join(lines) def format_replies(replies): if not replies: return "### Reply drafts\nNothing found." lines = [] for reply in replies: customer = reply.get("customer", "Customer") text = reply.get("reply", "") lines.append(f"**{customer}**\n\n{text}") return "### Reply drafts\n\n" + "\n\n---\n\n".join(lines) def text_value(value): if isinstance(value, list): return ", ".join(str(v) for v in value if str(v).strip()) if value is None: return "" return str(value).strip() def missing_list(order): raw = order.get("missing_fields", []) if isinstance(raw, str): fields = [part.strip() for part in raw.split(",") if part.strip()] else: fields = [str(part).strip() for part in raw if str(part).strip()] allowed = {"quantity", "flavor", "pickup_time", "delivery_address", "payment_status"} fields = [field for field in fields if field in allowed] item = text_value(order.get("item")) quantity = text_value(order.get("quantity")) flavor = text_value(order.get("flavor")) pickup_time = text_value(order.get("pickup_time")) delivery_address = text_value(order.get("delivery_address")) payment_status = text_value(order.get("payment_status")) if item and not quantity: fields.append("quantity") if pickup_time: fields = [field for field in fields if field != "pickup_time"] if delivery_address: fields = [field for field in fields if field != "delivery_address"] if payment_status: fields = [field for field in fields if field != "payment_status"] if flavor: fields = [field for field in fields if field != "flavor"] if "flavor" in fields and item.lower() not in ["cake", "birthday cake", "cupcakes"]: fields = [field for field in fields if field != "flavor"] return sorted(set(fields)) def post_process_order(order, message): msg = message.lower() if "paid already" in msg or "already paid" in msg: order["payment_status"] = "paid" elif "venmo" in msg: order["payment_status"] = "can pay Venmo" elif "paid" not in msg and "venmo" not in msg: order["payment_status"] = "" pickup_time = text_value(order.get("pickup_time")) if "paid" in pickup_time.lower() or "venmo" in pickup_time.lower(): order["pickup_time"] = "" if "farmers market" in msg: order["delivery_address"] = "farmers market" if "farmers market" in text_value(order.get("pickup_time")).lower(): order["pickup_time"] = "" order["missing_fields"] = missing_list(order) return order def build_prep_list(data): items = [] for order in data.get("orders", []): item = text_value(order.get("item")) if not item: continue customer = text_value(order.get("customer")) or "customer" quantity = text_value(order.get("quantity")) or "quantity to confirm" flavor = text_value(order.get("flavor")) line = f"{quantity} {item}" if flavor: line += f" ({flavor})" line += f" - {customer}" items.append(line) return items def build_reply_drafts(data): replies = [] labels = { "quantity": "quantity", "flavor": "flavor", "pickup_time": "pickup or delivery time", "delivery_address": "pickup place", "payment_status": "payment status", } for order in data.get("orders", []): customer = text_value(order.get("customer")) or "there" item = text_value(order.get("item")) or "order" quantity = text_value(order.get("quantity")) flavor = text_value(order.get("flavor")) missing = [labels.get(field, field) for field in missing_list(order)] if missing: needed = ", ".join(missing) reply = f"Thanks, {customer}! I have your {item} order. Could you confirm the {needed}?" else: summary = f"{quantity} {item}".strip() if flavor: summary += f" ({flavor})" reply = f"Thanks, {customer}! Confirming your order: {summary}." replies.append({"customer": customer, "reply": reply}) return replies def build_summary(data): orders = data.get("orders", []) total = len(orders) followups = sum(1 for order in orders if missing_list(order)) ready = total - followups return f"""