/** * Analyze Menu for Modifier Patterns * * Parses item descriptions to detect modifier groups and suggests * which templates should be created and applied. * * Detection patterns: * - Size variants: "Small available", "1/2 order available", "Small/Large" * - Protein choices: "With Chicken / With Steak or Shrimp" * - Required choices: "Boneless or bone in" * - Category-wide add-ons: "Add to any salad: ..." * - Price modifiers: "Add cheese +$0.50" * * POST JSON: * { * "categories": [ * { * "name": "Burgers", * "categoryNote": "All burgers served with lettuce, tomato...", * "items": [ * { "name": "Burger", "description": "Served with fries" } * ] * } * ] * } * * Returns analyzed menu with detected modifiers and suggestions */ response = { "OK": false }; // Pattern definitions for modifier detection patterns = { // Size patterns "size_small_available": { "regex": "(?i)\bsmall\s+(available|option)\b", "templateName": "Size", "options": [ { "name": "Regular", "price": 0, "isDefault": true }, { "name": "Small", "price": 0, "isDefault": false } ], "required": true, "maxSelections": 1 }, "size_half_available": { "regex": "(?i)\b(1/2|half)\s+(order\s+)?(available|option)\b", "templateName": "Size", "options": [ { "name": "Full", "price": 0, "isDefault": true }, { "name": "Half", "price": 0, "isDefault": false } ], "required": true, "maxSelections": 1 }, // Choice patterns (X or Y) "choice_or": { "regex": "(?i)\b(\w+)\s+or\s+(\w+)\b(?!\s+shrimp)", "templateName": "auto", // Will be generated from matched words "required": true, "maxSelections": 1 }, // Protein add-ons "protein_chicken_steak_shrimp": { "regex": "(?i)with\s+chicken\s*/?\s*(?:with\s+)?(?:steak\s+or\s+shrimp|steak\/shrimp)", "templateName": "Add Protein", "options": [ { "name": "Plain", "price": 0, "isDefault": true }, { "name": "With Chicken", "price": 0, "isDefault": false }, { "name": "With Steak or Shrimp", "price": 0, "isDefault": false } ], "required": false, "maxSelections": 1 }, // Add-on with price "addon_with_price": { "regex": "(?i)add\s+(\w+(?:\s+\w+)?)\s*[\+\$]?\s*\.?(\d+(?:\.\d{2})?)", "templateName": "Add-ons", "required": false, "maxSelections": 0 }, // Category note patterns "salad_addons": { "regex": "(?i)add\s+(?:the\s+following\s+)?to\s+any\s+salad[:\s]+(.+)", "templateName": "Add Protein", "appliesTo": "category", "required": false, "maxSelections": 1 } }; try { requestBody = toString(getHttpRequestData().content); if (!len(requestBody)) { throw(message="No request body provided"); } data = deserializeJSON(requestBody); categories = structKeyExists(data, "categories") ? data.categories : []; // Track detected templates and their usage detectedTemplates = {}; itemModifiers = {}; // Maps item name to array of template IDs // Analyze each category analyzedCategories = []; for (cat in categories) { catName = cat.name; catNote = structKeyExists(cat, "categoryNote") ? cat.categoryNote : ""; items = structKeyExists(cat, "items") ? cat.items : []; // Check for category-wide patterns in the note categoryTemplates = []; // Check for "Add to any salad" pattern if (reFindNoCase("add\s+(?:the\s+following\s+)?to\s+any\s+(salad|item)", catNote)) { // Extract the add-on items addOnMatch = reMatchNoCase("add[^:]+:\s*(.+)", catNote); if (arrayLen(addOnMatch)) { addOnText = addOnMatch[1]; // Split by comma addOnItems = listToArray(addOnText, ","); templateID = "cat_" & lCase(reReplace(catName, "\W+", "_", "all")) & "_addons"; if (!structKeyExists(detectedTemplates, templateID)) { options = [{ "name": "None", "price": 0, "isDefault": true }]; for (addon in addOnItems) { addon = trim(addon); if (len(addon)) { arrayAppend(options, { "name": addon, "price": 0, "isDefault": false }); } } detectedTemplates[templateID] = { "id": templateID, "name": "Add Protein", "options": options, "required": false, "maxSelections": 1, "detectedFrom": "Category note: " & catName, "appliesTo": [] }; } arrayAppend(categoryTemplates, templateID); } } // Analyze each item analyzedItems = []; for (item in items) { itemName = item.name; itemDesc = structKeyExists(item, "description") ? item.description : ""; itemMods = []; // Apply category-wide templates for (catTmpl in categoryTemplates) { arrayAppend(itemMods, catTmpl); arrayAppend(detectedTemplates[catTmpl].appliesTo, itemName); } // Check for size patterns if (reFindNoCase("\bsmall\s*(available|option)?\b", itemDesc) || reFindNoCase("\bsmall\b", itemName)) { templateID = "size_regular_small"; if (!structKeyExists(detectedTemplates, templateID)) { detectedTemplates[templateID] = { "id": templateID, "name": "Size", "options": [ { "name": "Regular", "price": 0, "isDefault": true }, { "name": "Small", "price": 0, "isDefault": false } ], "required": true, "maxSelections": 1, "detectedFrom": "Pattern: 'Small available'", "appliesTo": [] }; } arrayAppend(itemMods, templateID); arrayAppend(detectedTemplates[templateID].appliesTo, itemName); } // Check for half order pattern if (reFindNoCase("\b(1/2|half)\s*(order)?\s*(available)?\b", itemDesc)) { templateID = "size_full_half"; if (!structKeyExists(detectedTemplates, templateID)) { detectedTemplates[templateID] = { "id": templateID, "name": "Size", "options": [ { "name": "Full Order", "price": 0, "isDefault": true }, { "name": "Half Order", "price": 0, "isDefault": false } ], "required": true, "maxSelections": 1, "detectedFrom": "Pattern: '1/2 order available'", "appliesTo": [] }; } arrayAppend(itemMods, templateID); arrayAppend(detectedTemplates[templateID].appliesTo, itemName); } // Check for "boneless or bone in" pattern if (reFindNoCase("\bboneless\s+(or|/)\s*bone\s*-?\s*in\b", itemDesc)) { templateID = "wing_style"; if (!structKeyExists(detectedTemplates, templateID)) { detectedTemplates[templateID] = { "id": templateID, "name": "Style", "options": [ { "name": "Bone-In", "price": 0, "isDefault": true }, { "name": "Boneless", "price": 0, "isDefault": false } ], "required": true, "maxSelections": 1, "detectedFrom": "Pattern: 'Boneless or bone in'", "appliesTo": [] }; } arrayAppend(itemMods, templateID); arrayAppend(detectedTemplates[templateID].appliesTo, itemName); } // Check for protein add-on pattern (With Chicken / With Steak or Shrimp) if (reFindNoCase("with\s+chicken.*(?:steak|shrimp)", itemDesc)) { templateID = "protein_addon"; if (!structKeyExists(detectedTemplates, templateID)) { detectedTemplates[templateID] = { "id": templateID, "name": "Add Protein", "options": [ { "name": "Plain", "price": 0, "isDefault": true }, { "name": "With Chicken", "price": 0, "isDefault": false }, { "name": "With Steak or Shrimp", "price": 0, "isDefault": false } ], "required": false, "maxSelections": 1, "detectedFrom": "Pattern: 'With Chicken / With Steak or Shrimp'", "appliesTo": [] }; } arrayAppend(itemMods, templateID); arrayAppend(detectedTemplates[templateID].appliesTo, itemName); } // Check for "add X +$Y" pattern priceMatch = reMatchNoCase("add\s+(\w+(?:\s+(?:and\s+)?\w+)?)\s*[\.\+]?\s*(\d+(?:\.\d{2})?)", itemDesc); if (arrayLen(priceMatch)) { for (match in priceMatch) { // Parse the match to get item name and price parts = reMatchNoCase("add\s+(.+?)\s*[\.\+]?\s*(\d+(?:\.\d{2})?)\s*$", match); if (arrayLen(parts)) { templateID = "addon_" & lCase(reReplace(itemName, "\W+", "_", "all")); if (!structKeyExists(detectedTemplates, templateID)) { detectedTemplates[templateID] = { "id": templateID, "name": "Add-ons", "options": [], "required": false, "maxSelections": 0, "detectedFrom": "Pattern: 'Add X +$Y' in " & itemName, "appliesTo": [], "needsPricing": true }; } arrayAppend(itemMods, templateID); arrayAppend(detectedTemplates[templateID].appliesTo, itemName); } } } // Check for "with guacamole" type optional add-ons if (reFindNoCase("\bwith\s+guacamole\b", itemDesc)) { templateID = "addon_guacamole"; if (!structKeyExists(detectedTemplates, templateID)) { detectedTemplates[templateID] = { "id": templateID, "name": "Add Guacamole", "options": [ { "name": "No Guacamole", "price": 0, "isDefault": true }, { "name": "Add Guacamole", "price": 0, "isDefault": false } ], "required": false, "maxSelections": 1, "detectedFrom": "Pattern: 'With guacamole'", "appliesTo": [], "needsPricing": true }; } arrayAppend(itemMods, templateID); arrayAppend(detectedTemplates[templateID].appliesTo, itemName); } // Store item with detected modifiers arrayAppend(analyzedItems, { "name": itemName, "description": itemDesc, "price": structKeyExists(item, "price") ? item.price : 0, "detectedModifiers": itemMods, "originalDescription": itemDesc }); } arrayAppend(analyzedCategories, { "name": catName, "categoryNote": catNote, "items": analyzedItems, "categoryWideTemplates": categoryTemplates }); } // Build template array sorted by usage count templateArray = []; for (key in detectedTemplates) { tmpl = detectedTemplates[key]; tmpl["usageCount"] = arrayLen(tmpl.appliesTo); arrayAppend(templateArray, tmpl); } // Sort by usage count descending arraySort(templateArray, function(a, b) { return b.usageCount - a.usageCount; }); // Generate questions for owner confirmation questions = []; // Question about detected templates if (arrayLen(templateArray)) { arrayAppend(questions, { "type": "confirm_templates", "question": "I detected these modifier groups. Are they correct?", "templates": templateArray }); } // Question about items needing pricing itemsNeedingPrices = []; for (cat in analyzedCategories) { for (item in cat.items) { if (item.price == 0) { arrayAppend(itemsNeedingPrices, { "category": cat.name, "item": item.name }); } } } if (arrayLen(itemsNeedingPrices)) { arrayAppend(questions, { "type": "pricing_needed", "question": "These items need prices:", "items": itemsNeedingPrices }); } // Question about template options needing pricing templatesNeedingPrices = []; for (tmpl in templateArray) { if (structKeyExists(tmpl, "needsPricing") && tmpl.needsPricing) { arrayAppend(templatesNeedingPrices, { "templateId": tmpl.id, "templateName": tmpl.name, "appliesTo": tmpl.appliesTo }); } } if (arrayLen(templatesNeedingPrices)) { arrayAppend(questions, { "type": "modifier_pricing_needed", "question": "These add-on options need prices:", "templates": templatesNeedingPrices }); } response.OK = true; response.analyzedMenu = { "categories": analyzedCategories, "detectedTemplates": templateArray, "totalItems": 0, "totalTemplates": arrayLen(templateArray), "totalModifierLinks": 0 }; // Count totals totalItems = 0; totalLinks = 0; for (cat in analyzedCategories) { totalItems += arrayLen(cat.items); for (item in cat.items) { totalLinks += arrayLen(item.detectedModifiers); } } response.analyzedMenu.totalItems = totalItems; response.analyzedMenu.totalModifierLinks = totalLinks; response.questions = questions; response.summary = "Analyzed " & totalItems & " items, detected " & arrayLen(templateArray) & " modifier templates with " & totalLinks & " links"; } catch (any e) { response.error = e.message; if (len(e.detail)) { response.errorDetail = e.detail; } } writeOutput(serializeJSON(response));