fix(amazon-shopping): enforce rating filters in chat output
This commit is contained in:
@@ -16,7 +16,7 @@ This repository contains practical OpenClaw skills and companion integrations. I
|
||||
|
||||
| Skill | What it does | Path |
|
||||
|---|---|---|
|
||||
| `amazon-shopping` | Search Amazon.com product results with bounded web-automation, price/unit-price/width/Prime/delivery filters, specs, ratings, review metadata, and fixed markdown tables. | `skills/amazon-shopping` |
|
||||
| `amazon-shopping` | Search Amazon.com product results with bounded web-automation, price/unit-price/width/Prime/delivery filters, specs, ratings, review metadata, and chat-safe result blocks with direct links. | `skills/amazon-shopping` |
|
||||
| `flight-finder` | Collect structured flight-search inputs, run bounded multi-source travel searches, rank results in USD, and gate fixed-template PDF/email delivery through Luke’s sender path. | `skills/flight-finder` |
|
||||
| `gitea-api` | Interact with Gitea via REST API (repos, issues, PRs, releases, branches, user info). | `skills/gitea-api` |
|
||||
| `nordvpn-client` | Install, log in to, connect, disconnect, and verify NordVPN sessions across Linux CLI and macOS NordLynx/WireGuard backends. | `skills/nordvpn-client` |
|
||||
|
||||
+11
-5
@@ -87,17 +87,22 @@ Each result includes the product ASIN, title, source URL, price, unit price when
|
||||
|
||||
Unknown or hidden fields stay unknown. The skill does not invent delivery dates, star histograms, prices, or review counts.
|
||||
|
||||
Markdown output uses a fixed table intended for direct user-facing answers:
|
||||
Markdown output uses chat-safe result blocks intended for direct user-facing answers in WhatsApp, Telegram, and terminals. Each product must keep a direct URL line:
|
||||
|
||||
```markdown
|
||||
## Best Matches
|
||||
|
||||
| # | Product | Price | Rating | Reviews | Width | Prime | Delivery | Link |
|
||||
|---|---|---:|---:|---:|---:|---|---|---|
|
||||
| 1 | Example Sofa Bed | $399.99 | 4.3 stars | 250 | 83" OK | Prime OK | FREE delivery Tomorrow OK | [Amazon](https://www.amazon.com/dp/ASIN) |
|
||||
1. Example Sofa Bed
|
||||
Price: $399.99
|
||||
Rating: 4.3 stars
|
||||
Reviews: 250
|
||||
Width: 83" OK
|
||||
Prime: Prime OK
|
||||
Delivery: FREE delivery Tomorrow OK
|
||||
Link: https://www.amazon.com/dp/ASIN
|
||||
```
|
||||
|
||||
The `OK` / `NO` marker is only attached to columns that correspond to requested filters. For example, `Prime OK` means the helper found a Prime signal on the search card or detail page; `not verified NO` means the product did not pass a requested Prime filter.
|
||||
The `OK` / `NO` marker is only attached to fields that correspond to requested filters. For example, `Prime OK` means the helper found a Prime signal on the search card or detail page; `not verified NO` means the product did not pass a requested Prime filter.
|
||||
|
||||
## Filters
|
||||
|
||||
@@ -123,6 +128,7 @@ Examples of supported natural-language filters:
|
||||
- `delivery by tomorrow`
|
||||
- `overnight shipping`
|
||||
- `top 10 by price`
|
||||
- `rating 4.0 or better`
|
||||
|
||||
Equivalent CLI flags:
|
||||
|
||||
|
||||
@@ -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