fix(amazon-shopping): enforce rating filters in chat output
This commit is contained in:
@@ -28,7 +28,7 @@ Use single quotes around product requests that contain dollar amounts so the she
|
||||
|
||||
Default to at most 15 products unless the user asks for a different count. For requested counts above 30, ask before continuing or split the request into batches. Always include source URLs, report missing fields explicitly, and do not claim review histogram data unless it was visible and extracted.
|
||||
|
||||
For user-facing answers, use the generated markdown table as the presentation template. Keep the columns `#`, `Product`, `Price`, `Rating`, `Reviews`, `Width`, `Prime`, `Delivery`, and `Link` when those fields are relevant. Do not rewrite Prime or delivery status as verified unless the helper marks it verified.
|
||||
For user-facing answers, use the generated chat-safe result blocks as the presentation template. Keep the direct `Link: https://...` line for every product because WhatsApp and Telegram do not reliably render markdown tables. Do not rewrite Prime or delivery status as verified unless the helper marks it verified.
|
||||
|
||||
Supported filters include minimum rating, minimum reviews, maximum price, maximum unit price, minimum width in inches, Prime delivery, delivery by today/tomorrow/overnight, and sort by price. Natural language such as `77 inches or wider`, `shipped with Prime`, `delivery by tomorrow`, `overnight shipping`, and `top 10 by price` is parsed automatically. CLI flags are also available: `--min-width`, `--require-prime`, `--delivery-by`, and `--sort-by price`.
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ 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, " ")
|
||||
@@ -89,7 +92,7 @@ export function parseNaturalLanguageRequest(input: string): ParsedNaturalLanguag
|
||||
}
|
||||
|
||||
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(?:review score|rating)\s+of\s+([0-5](?:\.[0-9])?)\s*(?:stars?)?\s+(?:or|and)\s+(?:higher|better)\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);
|
||||
|
||||
@@ -44,8 +44,8 @@ function formatFilters(filters: ProductFilters): string {
|
||||
return parts.length > 0 ? parts.join(", ") : "none";
|
||||
}
|
||||
|
||||
function escapeCell(value: string): string {
|
||||
return value.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
||||
function compactText(value: string): string {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function marker(passes: boolean | undefined, enabled: boolean): string {
|
||||
@@ -82,24 +82,18 @@ function deliveryCell(product: ProductSearchResult, filters: ProductFilters): st
|
||||
return `${display}${marker(passes, true)}`;
|
||||
}
|
||||
|
||||
function resultTable(products: ProductSearchResult[], filters: ProductFilters): string[] {
|
||||
const rows = [
|
||||
"| # | Product | Price | Rating | Reviews | Width | Prime | Delivery | Link |",
|
||||
"|---|---|---:|---:|---:|---:|---|---|---|",
|
||||
...products.map((product, index) => [
|
||||
String(index + 1),
|
||||
escapeCell(product.title),
|
||||
product.price?.display ?? "unknown",
|
||||
`${product.rating ?? "unknown"} stars`,
|
||||
product.reviewCount?.toLocaleString("en-US") ?? "unknown",
|
||||
widthCell(product, filters),
|
||||
primeCell(product, filters),
|
||||
escapeCell(deliveryCell(product, filters)),
|
||||
`[Amazon](${product.url})`
|
||||
].join(" | "))
|
||||
.map((row) => `| ${row} |`)
|
||||
];
|
||||
return rows;
|
||||
function resultBlocks(products: ProductSearchResult[], filters: ProductFilters): string[] {
|
||||
return products.flatMap((product, index) => [
|
||||
`${index + 1}. ${compactText(product.title)}`,
|
||||
`Price: ${product.price?.display ?? "unknown"}`,
|
||||
`Rating: ${product.rating ?? "unknown"} stars`,
|
||||
`Reviews: ${product.reviewCount?.toLocaleString("en-US") ?? "unknown"}`,
|
||||
`Width: ${widthCell(product, filters)}`,
|
||||
`Prime: ${primeCell(product, filters)}`,
|
||||
`Delivery: ${compactText(deliveryCell(product, filters))}`,
|
||||
`Link: ${product.url}`,
|
||||
""
|
||||
]);
|
||||
}
|
||||
|
||||
function metadataLines(products: ProductSearchResult[]): string[] {
|
||||
@@ -129,7 +123,7 @@ export function createMarkdownReport(response: SearchProductsResponse): string {
|
||||
"## Best Matches",
|
||||
"",
|
||||
response.results.length > 0 ? "" : "No products matched all requested filters.",
|
||||
...resultTable(response.results, response.filters),
|
||||
...resultBlocks(response.results, response.filters),
|
||||
"",
|
||||
...metadataLines(response.results)
|
||||
].filter((line) => line !== "");
|
||||
|
||||
@@ -35,6 +35,22 @@ describe("parseNaturalLanguageRequest", () => {
|
||||
assert.equal(parsed.filters.minRating, 4);
|
||||
});
|
||||
|
||||
it("extracts rating filters without requiring the word of", () => {
|
||||
const parsed = parseNaturalLanguageRequest(
|
||||
"sofa bed, 77 inches or wider, over 50 reviews, rating 4.0 or better, Prime delivery only, sort by price low to high"
|
||||
);
|
||||
|
||||
assert.equal(parsed.query, "sofa bed");
|
||||
assert.equal(parsed.filters.minWidthInches, 77);
|
||||
assert.equal(parsed.filters.widthComparison, "gte");
|
||||
assert.equal(parsed.filters.minReviews, 50);
|
||||
assert.equal(parsed.filters.reviewCountComparison, "gt");
|
||||
assert.equal(parsed.filters.minRating, 4);
|
||||
assert.equal(parsed.filters.ratingComparison, "gte");
|
||||
assert.equal(parsed.filters.requirePrime, true);
|
||||
assert.equal(parsed.filters.sortBy, "price");
|
||||
});
|
||||
|
||||
it("extracts limit and max product price phrases", () => {
|
||||
const parsed = parseNaturalLanguageRequest("return 5 wireless mouse under $30");
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("report", () => {
|
||||
assert.match(markdown, /https:\/\/www\.amazon\.com\/dp\/B0TEST0001/);
|
||||
});
|
||||
|
||||
it("creates a table template with constraint status markers", () => {
|
||||
it("creates a chat-safe template with direct product links and constraint status markers", () => {
|
||||
const markdown = createMarkdownReport(createResponse({
|
||||
query: "sofa bed beige",
|
||||
filters: {
|
||||
@@ -87,11 +87,13 @@ describe("report", () => {
|
||||
}));
|
||||
|
||||
assert.match(markdown, /## Best Matches/);
|
||||
assert.match(markdown, /\| # \| Product \| Price \| Rating \| Reviews \| Width \| Prime \| Delivery \| Link \|/);
|
||||
assert.doesNotMatch(markdown, /\| # \| Product \| Price \| Rating \| Reviews \| Width \| Prime \| Delivery \| Link \|/);
|
||||
assert.doesNotMatch(markdown, /\[Amazon\]\(https:\/\/www\.amazon\.com\/dp\/B0SOFABED1\)/);
|
||||
assert.match(markdown, /HONBAY Modular Sectional Sleeper/);
|
||||
assert.match(markdown, /Link: https:\/\/www\.amazon\.com\/dp\/B0SOFABED1/);
|
||||
assert.match(markdown, /83\.4" OK/);
|
||||
assert.match(markdown, /Prime OK/);
|
||||
assert.match(markdown, /Tomorrow OK/);
|
||||
assert.match(markdown, /\[Amazon\]\(https:\/\/www\.amazon\.com\/dp\/B0SOFABED1\)/);
|
||||
assert.match(markdown, /Price: \$539\.99/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user