mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-10-23 20:21:20 +00:00
LLM restock: fix locale-formatted string prices (1.299,95 read as 1.29995) (#4544)
* LLM restock: parse string prices with price_parser instead of stripping separators
If the model returns the price as page text, e.g. "1.299,95 €", stripping
everything but digits and dots gave 1.29995. price_parser (already a
dependency) handles the locale formats the prompt lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* LLM restock: use Restock.parse_currency() like the other price extractors
Same parser c8afea1b (#4494) moved the structured-data extractors to, so
there's one place price text becomes a number. parse_currency() now lists
every caller, and the call sites point back to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
350f6a9de4
commit
7ec79fc231
4 files changed
+30
-2
No files matched your search
@@ -16,6 +16,11 @@ _price_re = re.compile(r"Price:\s*(\d+(?:\.\d+)?)", re.IGNORECASE)
|
||||
|
||||
class Restock(dict):
|
||||
|
||||
# The one place price text becomes a number - keep it that way, don't strip to [^\d.] and float()
|
||||
# at the call site, that turns "39,99" into 3999 (#4493). Used by:
|
||||
# processor.py _deduplicate_prices() (itemprop/JSON-LD prices)
|
||||
# pure_python_extractor.py MicrodataExtractor, query_price_availability()
|
||||
# plugins/llm_restock.py get_itemprop_availability_override() (price the LLM returns as a string)
|
||||
def parse_currency(self, raw_value: str) -> Union[float, None]:
|
||||
# Clean and standardize the value (ie 1,400.00 should be 1400.00), even better would be store the whole thing as an integer.
|
||||
standardized_value = raw_value
|
||||
|
||||
@@ -334,7 +334,11 @@ def get_itemprop_availability_override(content, fetcher_name, fetcher_instance,
|
||||
if price is not None:
|
||||
try:
|
||||
if isinstance(price, str):
|
||||
price = float(re.sub(r'[^\d.]', '', price))
|
||||
# The model sometimes returns the price as page text ("1.299,95 €"), stripping
|
||||
# everything but digits and dots would turn that into 1.29995. Same parser as the
|
||||
# structured-data extractors, see Restock.parse_currency() for every caller.
|
||||
from changedetectionio.processors.restock_diff import Restock
|
||||
price = Restock().parse_currency(price)
|
||||
else:
|
||||
price = float(price)
|
||||
except (ValueError, TypeError):
|
||||
|
||||
@@ -136,6 +136,7 @@ class MicrodataExtractor(HTMLParser):
|
||||
if self.current_itemprop == 'price':
|
||||
# Try to extract numeric price from text
|
||||
try:
|
||||
# Not [^\d.] + float(), that breaks "39,99" (#4493), see Restock.parse_currency() for every caller
|
||||
price = Restock().parse_currency(data.strip())
|
||||
if price is not None:
|
||||
self.microdata['price'] = price
|
||||
@@ -240,7 +241,7 @@ def query_price_availability(extracted_data):
|
||||
if isinstance(price_val, (int, float)):
|
||||
result['price'] = float(price_val)
|
||||
elif isinstance(price_val, str):
|
||||
# Extract numeric value from string
|
||||
# Extract numeric value from string, see Restock.parse_currency() for every caller
|
||||
try:
|
||||
result['price'] = Restock().parse_currency(price_val)
|
||||
except ValueError:
|
||||
|
||||
@@ -193,6 +193,24 @@ class TestLLMRestockPluginExtraction:
|
||||
assert result is not None
|
||||
assert result['price'] == 49.99
|
||||
|
||||
@pytest.mark.parametrize('price_str, expected', [
|
||||
('1.299,95', 1299.95),
|
||||
('1.299,95 €', 1299.95),
|
||||
('1,299.95', 1299.95),
|
||||
('10 000 Kč', 10000.0),
|
||||
('10 000,50', 10000.5),
|
||||
('$127.99', 127.99),
|
||||
])
|
||||
def test_handles_locale_formatted_string_price(self, price_str, expected):
|
||||
"""A string price in page format must not be mangled by stripping its separators (same as #4493)."""
|
||||
llm_json = json.dumps({'price': price_str, 'currency': None, 'availability': 'instock'})
|
||||
result = _call_plugin(
|
||||
f'<html><body>{price_str}</body></html>',
|
||||
llm_json=llm_json,
|
||||
)
|
||||
assert result is not None
|
||||
assert result['price'] == expected
|
||||
|
||||
|
||||
class TestLLMRestockPluginTokenAccounting:
|
||||
def test_result_includes_token_metadata(self):
|
||||
|
||||
Reference in new issue
Block a user