Spaces:
Running
Running
AuthorBot Cursor commited on
Commit ·
4e43a29
1
Parent(s): 66a04fa
fix: don't re-show unlabeled price table when format is unknown
Browse files
app/services/intent.py
CHANGED
|
@@ -103,6 +103,7 @@ _PRICE_INQUIRY_SIGNALS: tuple[str, ...] = (
|
|
| 103 |
"ebook price", "ebook prices", "e-book price",
|
| 104 |
"hardcover price", "hardcover prices", "paperback price", "paperback prices",
|
| 105 |
"what about audio", "what about ebook", "what about hardcover",
|
|
|
|
| 106 |
)
|
| 107 |
|
| 108 |
_PURCHASE_SIGNALS: tuple[str, ...] = (
|
|
|
|
| 103 |
"ebook price", "ebook prices", "e-book price",
|
| 104 |
"hardcover price", "hardcover prices", "paperback price", "paperback prices",
|
| 105 |
"what about audio", "what about ebook", "what about hardcover",
|
| 106 |
+
"is this price", "is that price", "price of hard",
|
| 107 |
)
|
| 108 |
|
| 109 |
_PURCHASE_SIGNALS: tuple[str, ...] = (
|
app/services/price_catalog_service.py
CHANGED
|
@@ -133,7 +133,10 @@ def classify_price_query(message: str) -> PriceQuery:
|
|
| 133 |
"ebook or", "e-book or", "paperback or",
|
| 134 |
"which format", "what format", "are those", "are these",
|
| 135 |
"is that the", "is that hardcover", "is that audiobook",
|
| 136 |
-
"is that ebook", "
|
|
|
|
|
|
|
|
|
|
| 137 |
)
|
| 138 |
if any(s in q for s in clarify):
|
| 139 |
return PriceQuery(kind="format_clarify", platform_ids=platforms, format_hint=fmt)
|
|
@@ -344,27 +347,36 @@ class PriceCatalogService:
|
|
| 344 |
book_title: str,
|
| 345 |
format_hint: str,
|
| 346 |
) -> dict[str, Any]:
|
| 347 |
-
"""Honest reply when a named format has no stored prices
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
available = await self.catalog_for_book(author_id, book_id, format_hint="")
|
| 349 |
-
|
|
|
|
| 350 |
if formats:
|
| 351 |
named = ", ".join(formats)
|
| 352 |
text = (
|
| 353 |
f"I don't have {format_hint} prices stored for {book_title} yet. "
|
| 354 |
f"On file I have: {named}. Pick a Buy button for those."
|
| 355 |
)
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
"
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
return {
|
| 362 |
"text": text[:380],
|
| 363 |
"links": [],
|
| 364 |
"has_links": False,
|
| 365 |
-
"platform_offers":
|
| 366 |
-
offer_to_platform_item(o) for o in available
|
| 367 |
-
] if available else None,
|
| 368 |
}
|
| 369 |
|
| 370 |
async def _build_empty_fallback(
|
|
@@ -418,17 +430,22 @@ class PriceCatalogService:
|
|
| 418 |
f"Here’s each listed price for {book_title} with its format "
|
| 419 |
f"({named}). I only show formats I have stored — nothing invented."
|
| 420 |
)
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
"
|
| 425 |
-
"
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
return {
|
| 428 |
"text": text[:380],
|
| 429 |
"links": [],
|
| 430 |
"has_links": False,
|
| 431 |
-
"platform_offers":
|
| 432 |
}
|
| 433 |
|
| 434 |
def _build_cheapest_answer(self, offers: list[Offer], book_title: str) -> dict[str, Any]:
|
|
|
|
| 133 |
"ebook or", "e-book or", "paperback or",
|
| 134 |
"which format", "what format", "are those", "are these",
|
| 135 |
"is that the", "is that hardcover", "is that audiobook",
|
| 136 |
+
"is that ebook", "is that hard", "is this hard",
|
| 137 |
+
"is this price", "is that price", "is this the",
|
| 138 |
+
"price of hard", "price of audio", "price of e-book", "price of ebook",
|
| 139 |
+
"format are", "those prices",
|
| 140 |
)
|
| 141 |
if any(s in q for s in clarify):
|
| 142 |
return PriceQuery(kind="format_clarify", platform_ids=platforms, format_hint=fmt)
|
|
|
|
| 347 |
book_title: str,
|
| 348 |
format_hint: str,
|
| 349 |
) -> dict[str, Any]:
|
| 350 |
+
"""Honest reply when a named format has no stored prices.
|
| 351 |
+
|
| 352 |
+
Only attach platform_offers when other formats are labeled — never re-show
|
| 353 |
+
an unlabeled price grid that contradicts 'I don't have that format'.
|
| 354 |
+
"""
|
| 355 |
available = await self.catalog_for_book(author_id, book_id, format_hint="")
|
| 356 |
+
labeled = [o for o in available if o.format]
|
| 357 |
+
formats = sorted({o.format for o in labeled})
|
| 358 |
if formats:
|
| 359 |
named = ", ".join(formats)
|
| 360 |
text = (
|
| 361 |
f"I don't have {format_hint} prices stored for {book_title} yet. "
|
| 362 |
f"On file I have: {named}. Pick a Buy button for those."
|
| 363 |
)
|
| 364 |
+
return {
|
| 365 |
+
"text": text[:380],
|
| 366 |
+
"links": [],
|
| 367 |
+
"has_links": False,
|
| 368 |
+
"platform_offers": [offer_to_platform_item(o) for o in labeled],
|
| 369 |
+
}
|
| 370 |
+
text = (
|
| 371 |
+
f"I can't confirm whether that price is {format_hint} for {book_title} — "
|
| 372 |
+
"I don't have format labels stored for these listings yet. "
|
| 373 |
+
"Check the retailer page for binding or edition details."
|
| 374 |
+
)
|
| 375 |
return {
|
| 376 |
"text": text[:380],
|
| 377 |
"links": [],
|
| 378 |
"has_links": False,
|
| 379 |
+
"platform_offers": None,
|
|
|
|
|
|
|
| 380 |
}
|
| 381 |
|
| 382 |
async def _build_empty_fallback(
|
|
|
|
| 430 |
f"Here’s each listed price for {book_title} with its format "
|
| 431 |
f"({named}). I only show formats I have stored — nothing invented."
|
| 432 |
)
|
| 433 |
+
return {
|
| 434 |
+
"text": text[:380],
|
| 435 |
+
"links": [],
|
| 436 |
+
"has_links": False,
|
| 437 |
+
"platform_offers": [offer_to_platform_item(o) for o in offers],
|
| 438 |
+
}
|
| 439 |
+
text = (
|
| 440 |
+
f"I can't confirm the format for these prices on {book_title} — "
|
| 441 |
+
"I don't have format labels stored yet. "
|
| 442 |
+
"Check the retailer page for hardcover, ebook, or audiobook details."
|
| 443 |
+
)
|
| 444 |
return {
|
| 445 |
"text": text[:380],
|
| 446 |
"links": [],
|
| 447 |
"has_links": False,
|
| 448 |
+
"platform_offers": None,
|
| 449 |
}
|
| 450 |
|
| 451 |
def _build_cheapest_answer(self, offers: list[Offer], book_title: str) -> dict[str, Any]:
|
tests/unit/test_price_catalog_service.py
CHANGED
|
@@ -322,6 +322,60 @@ async def test_missing_audiobook_is_honest_and_shows_available():
|
|
| 322 |
assert "Buy Book" not in (result.get("links") or [])
|
| 323 |
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
@pytest.mark.asyncio
|
| 326 |
async def test_catalog_uses_book_format_when_no_format_prices():
|
| 327 |
svc = PriceCatalogService(MagicMock())
|
|
|
|
| 322 |
assert "Buy Book" not in (result.get("links") or [])
|
| 323 |
|
| 324 |
|
| 325 |
+
@pytest.mark.asyncio
|
| 326 |
+
async def test_missing_hardcover_unlabeled_hides_table():
|
| 327 |
+
svc = PriceCatalogService(MagicMock())
|
| 328 |
+
|
| 329 |
+
async def _catalog(_a, _b, *, format_hint=""):
|
| 330 |
+
if format_hint == "Hardcover":
|
| 331 |
+
return []
|
| 332 |
+
return [
|
| 333 |
+
Offer("amazon", "Amazon", "$35.00", 3500, "https://a.com", True, ""),
|
| 334 |
+
Offer("apple_books", "Apple Books", "$14.99", 1499, "https://apple.com", False, ""),
|
| 335 |
+
]
|
| 336 |
+
|
| 337 |
+
svc.catalog_for_book = _catalog
|
| 338 |
+
result = await svc.handle(
|
| 339 |
+
"hardcover prices", "a1", "b1", "All We Say",
|
| 340 |
+
)
|
| 341 |
+
assert result["platform_offers"] is None
|
| 342 |
+
assert "hardcover" in result["text"].lower()
|
| 343 |
+
assert "can't confirm" in result["text"].lower() or "cannot confirm" in result["text"].lower()
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def test_is_this_price_hard_copy_is_format_clarify():
|
| 347 |
+
pq = classify_price_query("is this price of hard copy")
|
| 348 |
+
assert pq.kind == "format_clarify"
|
| 349 |
+
assert pq.format_hint == "Hardcover"
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
@pytest.mark.asyncio
|
| 353 |
+
async def test_is_this_hard_copy_unlabeled_hides_table():
|
| 354 |
+
svc = PriceCatalogService(MagicMock())
|
| 355 |
+
svc.catalog_for_book = AsyncMock(return_value=[
|
| 356 |
+
Offer("amazon", "Amazon", "$35.00", 3500, "https://a.com", True, ""),
|
| 357 |
+
Offer("apple_books", "Apple Books", "$14.99", 1499, "https://apple.com", False, ""),
|
| 358 |
+
])
|
| 359 |
+
result = await svc.handle(
|
| 360 |
+
"is this price of hard copy", "a1", "b1", "All We Say",
|
| 361 |
+
)
|
| 362 |
+
assert result["platform_offers"] is None
|
| 363 |
+
assert "format" in result["text"].lower()
|
| 364 |
+
assert _classify_by_rules("is this price of hard copy") == "price_inquiry"
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
@pytest.mark.asyncio
|
| 368 |
+
async def test_format_clarify_unlabeled_hides_table():
|
| 369 |
+
svc = PriceCatalogService.__new__(PriceCatalogService)
|
| 370 |
+
offers = [
|
| 371 |
+
Offer("amazon", "Amazon", "$35.00", 3500, "https://a.com", True, ""),
|
| 372 |
+
Offer("apple_books", "Apple Books", "$14.99", 1499, "https://apple.com", False, ""),
|
| 373 |
+
]
|
| 374 |
+
result = svc._build_format_clarify_answer(offers, "All We Say")
|
| 375 |
+
assert result["platform_offers"] is None
|
| 376 |
+
assert "format" in result["text"].lower()
|
| 377 |
+
|
| 378 |
+
|
| 379 |
@pytest.mark.asyncio
|
| 380 |
async def test_catalog_uses_book_format_when_no_format_prices():
|
| 381 |
svc = PriceCatalogService(MagicMock())
|