import type { ParsedNaturalLanguageRequest, ProductFilters } from "./types.js"; function cleanQuery(text: string): string { return text .replace(/\breview score of\b/gi, " ") .replace(/\brating of\b/gi, " ") .replace(/\b(?:delivery|shipping)\s+only\b/gi, " ") .replace(/\blow\s+to\s+high\b/gi, " ") .replace(/\bhigh\s+to\s+low\b/gi, " ") .replace(/\bof\s+in\s+width\b/gi, " ") .replace(/\bin\s+width\b/gi, " ") .replace(/\b(?:that|and|with|have)\b/gi, " ") .replace(/[,\s]+/g, " ") .replace(/\s+/g, " ") .replace(/\s+(and|or|a)$/i, "") .trim(); } function removeMatched(text: string, match: RegExpMatchArray | null): string { if (!match) { return text; } return text.replace(match[0], " "); } export function parseNaturalLanguageRequest(input: string): ParsedNaturalLanguageRequest { let remaining = input.trim(); const filters: ProductFilters = { includeKeywords: [], excludeKeywords: [] }; let limit: number | undefined; const limitMatch = remaining.match(/\b(?:return|limit|top)\s+(\d{1,3})\b/i); if (limitMatch) { limit = Number(limitMatch[1]); remaining = removeMatched(remaining, limitMatch); } const sortByPriceMatch = remaining.match(/\b(?:by price|sort(?:ed)? by price|lowest price|cheapest|least expensive)\b/i); if (sortByPriceMatch) { filters.sortBy = "price"; remaining = removeMatched(remaining, sortByPriceMatch); } const deliveryTomorrowMatch = remaining.match(/\b(?:delivery|delivered|arrives?|shipping|ships?)\s+(?:by\s+)?tomorrow\b/i); const deliveryTodayMatch = remaining.match(/\b(?:delivery|delivered|arrives?|shipping|ships?)\s+(?:by\s+)?today\b/i) ?? remaining.match(/\bsame[- ]day\s+(?:delivery|shipping)\b/i); const overnightMatch = remaining.match(/\bovernight\s+(?:delivery|shipping)\b/i) ?? remaining.match(/\bnext[- ]day\s+(?:delivery|shipping)\b/i); const deliveryMatch = overnightMatch ?? deliveryTomorrowMatch ?? deliveryTodayMatch; if (deliveryMatch) { filters.deliveryBy = overnightMatch ? "overnight" : deliveryTomorrowMatch ? "tomorrow" : "today"; remaining = removeMatched(remaining, deliveryMatch); } const primeMatch = remaining.match(/\b(?:(?:shipped|ships|shipping|delivery|delivered)\s+(?:with|by|from)\s+)?prime\b/i); if (primeMatch) { filters.requirePrime = true; remaining = removeMatched(remaining, primeMatch); } const widthMatch = remaining.match(/\b(?:width\s*(?:of\s*)?)?([0-9]+(?:\.[0-9]+)?)\s*(?:inches|inch|in\.?|")\s*(?:or\s+)?(?:wider|wide|larger|longer)\b/i) ?? remaining.match(/\b([0-9]+(?:\.[0-9]+)?)\s*(?:inches|inch|in\.?|")\s*(?:or\s+)?(?:wider|wide|larger|longer)\s+(?:in\s+)?width\b/i) ?? remaining.match(/\b(?:at least|minimum|min\.?)\s+([0-9]+(?:\.[0-9]+)?)\s*(?:inches|inch|in\.?|")\s+(?:wide|width)\b/i); if (widthMatch) { filters.minWidthInches = Number(widthMatch[1]); filters.widthComparison = "gte"; remaining = removeMatched(remaining, widthMatch); } const unitPriceMatch = remaining.match(/\b(?:cost\s+)?(?:less than|under|below)\s+\$([0-9]+(?:\.[0-9]{1,2})?)\s*(?:each|per\b|\/\s*(?:count|unit|item))\b/i); if (unitPriceMatch) { filters.maxUnitPrice = Number(unitPriceMatch[1]); remaining = removeMatched(remaining, unitPriceMatch); } const maxPriceMatch = remaining.match(/\b(?:cost\s+)?(?:less than|under|below)\s+\$([0-9]+(?:\.[0-9]{1,2})?)\b/i); if (maxPriceMatch) { filters.maxPrice = Number(maxPriceMatch[1]); remaining = removeMatched(remaining, maxPriceMatch); } const exclusiveReviews = remaining.match(/\b(?:over|more than|above)\s+([0-9][0-9,]*)\s*(?:reviews?|ratings?)\b/i); const inclusiveReviews = remaining.match(/\b(?:at least|minimum|min\.?)\s+([0-9][0-9,]*)\s*(?:reviews?|ratings?)\b/i) ?? remaining.match(/\b([0-9][0-9,]*)\s*\+\s*(?:reviews?|ratings?)\b/i); const reviewMatch = exclusiveReviews ?? inclusiveReviews; if (reviewMatch) { filters.minReviews = Number(reviewMatch[1].replace(/,/g, "")); filters.reviewCountComparison = exclusiveReviews ? "gt" : "gte"; remaining = removeMatched(remaining, reviewMatch); } const exclusiveRating = remaining.match(/\b(?:a\s+)?(?:(?:review score|rating)\s+of\s+|rating\s+)?(?:more than|over|above|rated above)\s+([0-5](?:\.[0-9])?)\s*(?:stars?)?\b/i); const inclusiveRating = remaining.match(/\b(?:a\s+)?(?:review score|rating)(?:\s+of)?\s+([0-5](?:\.[0-9])?)\s*(?:stars?)?\s+(?:or|and)\s+(?:higher|better)\b/i) ?? remaining.match(/\b([0-5](?:\.[0-9])?)\s*stars?\s+or\s+better\b/i) ?? remaining.match(/\b([0-5](?:\.[0-9])?)\s*stars?\s+(?:or|and)\s+(?:higher|better)\b/i) ?? remaining.match(/\b(?:at least|minimum|min\.?)\s+([0-5](?:\.[0-9])?)\s*(?:stars?|rating)?\b/i); const ratingMatch = exclusiveRating ?? inclusiveRating; if (ratingMatch) { filters.minRating = Number(ratingMatch[1]); filters.ratingComparison = exclusiveRating ? "gt" : "gte"; remaining = removeMatched(remaining, ratingMatch); } return { query: cleanQuery(remaining), filters, limit }; }