Files
Priceflow/app/services/scheduler_service.py
T
Claude 03793cea0a fix: Add extract_text parameter to fix broken product search
**Problem:**
Previous commit changed get_page_content() to always return visible text (inner_text)
instead of HTML source. This broke product search which uses BeautifulSoup to parse
HTML with CSS selectors. Search was returning 0 results for all sites.

**Root Cause:**
- Search service uses: `soup.select(config["product_selector"])` on HTML
- But was receiving plain text instead of HTML structure
- BeautifulSoup couldn't find any product links → 0 results

**Solution: Add extract_text parameter**

Modified `get_page_content()` signature:
```python
async def get_page_content(
    url: str,
    use_proxy: bool = False,
    wait_selector: str = None,
    extract_text: bool = False  # NEW parameter
) -> tuple[str, str]:
```

**Behavior:**
- `extract_text=False` (DEFAULT): Returns HTML source via `page.content()`
  → Used by search service for BeautifulSoup parsing

- `extract_text=True`: Returns visible text via `page.inner_text('body')`
  → Used by AI monitoring for price extraction
  → Includes Amazon price prepending ("PRIX DÉTECTÉ: ...")

**Changes:**

1. **browserless_service.py** (Lines 197-329)
   - Added `extract_text` parameter with default `False`
   - Conditional logic: if extract_text, use inner_text + Amazon extraction
   - Else: use page.content() (HTML source)
   - Preserves backward compatibility (default = HTML)

2. **scheduler_service.py** (Line 113)
   - Added `extract_text=True` for AI monitoring
   - Ensures AI gets visible text with Amazon prices

3. **search_service.py** (Line 104)
   - Added `extract_text=True` for scrape_item() AI analysis
   - Search itself uses default (HTML) for BeautifulSoup parsing

**Impact:**
✅ Search now works again (gets HTML for BeautifulSoup)
✅ AI monitoring gets visible text (better extraction)
✅ Amazon price extraction only runs when extract_text=True
✅ Backward compatible (default behavior = HTML)

Fixes issue where all searches returned: "Found 0 results for [site]"
2025-11-29 23:23:24 +00:00

185 lines
7.6 KiB
Python

import asyncio
import logging
from datetime import UTC, datetime
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from app import database, models
from app.ai_schema import AIExtractionMetadata, AIExtractionResponse
from app.services.ai_service import AIService
from app.services.item_service import ItemService
from app.services.notification_service import NotificationService
from app.services.browserless_service import browserless_service
logger = logging.getLogger(__name__)
scheduler = AsyncIOScheduler()
PRICE_CHANGE_THRESHOLD_PERCENT = 20.0
LOW_CONFIDENCE_THRESHOLD = 0.7
def _get_thresholds():
with database.SessionLocal() as session:
settings = {s.key: s.value for s in session.query(models.Settings).all()}
return {
"price": float(settings.get("confidence_threshold_price", "0.5")),
"stock": float(settings.get("confidence_threshold_stock", "0.5")),
}
def _update_db_result(
item_id: int,
extraction: AIExtractionResponse,
metadata: AIExtractionMetadata,
thresholds: dict,
screenshot_path: str,
):
with database.SessionLocal() as session:
item = session.query(models.Item).filter(models.Item.id == item_id).first()
if not item:
return None, None
old_price, old_stock = item.current_price, item.in_stock
price, in_stock = extraction.price, extraction.in_stock
p_conf, s_conf = extraction.price_confidence, extraction.in_stock_confidence
if price is not None:
if p_conf >= thresholds["price"]:
if (
old_price
and (abs(price - old_price) / old_price * 100 > PRICE_CHANGE_THRESHOLD_PERCENT)
and p_conf < LOW_CONFIDENCE_THRESHOLD
):
item.last_error = f"Uncertain: Large price change with low confidence ({p_conf:.2f})"
else:
item.last_error = None
item.current_price = price
item.current_price_confidence = p_conf
session.add(
models.PriceHistory(
item_id=item.id,
price=price,
screenshot_path=screenshot_path,
price_confidence=p_conf,
in_stock_confidence=s_conf,
ai_model=metadata.model_name,
ai_provider=metadata.provider,
prompt_version=metadata.prompt_version,
repair_used=metadata.repair_used,
)
)
if in_stock is not None and s_conf >= thresholds["stock"]:
item.in_stock = in_stock
item.in_stock_confidence = s_conf
item.last_checked = datetime.now(UTC)
item.is_refreshing = False
if item.last_error and not item.last_error.startswith("Uncertain:"):
item.last_error = None
session.commit()
return old_price, old_stock
def _update_db_error(item_id, error_msg):
with database.SessionLocal() as session:
if item := session.query(models.Item).filter(models.Item.id == item_id).first():
item.is_refreshing = False
item.last_error = error_msg
item.last_checked = datetime.now(UTC)
session.commit()
async def process_item_check(item_id: int):
loop = asyncio.get_running_loop()
with database.SessionLocal() as session:
item_data, config = await loop.run_in_executor(None, ItemService.get_item_data_for_checking, session, item_id)
if not item_data:
logger.error(f"process_item_check: Item ID {item_id} not found")
return
try:
logger.info(f"Checking item: {item_data['name']} ({item_data['url']})")
# Use new browserless service
# Note: smart_scroll is handled internally by browserless_service
# extract_text=True to get visible text for AI analysis (not HTML)
page_text, screenshot_path = await browserless_service.get_page_content(
item_data["url"],
use_proxy="amazon" in item_data["url"], # Simple heuristic for now
wait_selector=item_data["selector"],
extract_text=True # Get visible text for AI, not raw HTML
)
# Determine availability based on content presence
# If we got content, we assume available unless proven otherwise
is_available = bool(page_text and len(page_text) > 500)
# Update availability status in database
with database.SessionLocal() as session:
if item := session.query(models.Item).filter(models.Item.id == item_id).first():
item.is_available = is_available
if not is_available:
logger.warning(f"Product {item_id} marked as unavailable")
item.last_error = "Product no longer available (404 or not found)"
session.commit()
if not screenshot_path:
raise Exception("Failed to capture screenshot")
if not (ai_result := await AIService.analyze_image(screenshot_path, page_text=page_text)):
raise Exception("AI analysis failed")
extraction, metadata = ai_result
thresholds = await loop.run_in_executor(None, _get_thresholds)
old_price, old_stock = await loop.run_in_executor(
None, _update_db_result, item_id, extraction, metadata, thresholds, screenshot_path
)
# Check for notifications
if item_data["notification_channel"] and extraction.price:
channel = item_data["notification_channel"]
# 1. Target Price Reached
if item_data["target_price"] and extraction.price <= item_data["target_price"]:
# Only notify if we haven't already notified for this price (or if price dropped further)
# For simplicity, we notify if current price <= target.
# Ideally we should check if we already notified recently, but let's keep it simple.
if not old_price or old_price > item_data["target_price"]:
await NotificationService.send_notification(
channel,
title=f"🎯 Prix cible atteint : {item_data['name']}",
body=f"Le prix de {item_data['name']} est passé à {extraction.price}€ (Cible: {item_data['target_price']}€)\n{item_data['url']}"
)
# 2. Price Drop
elif old_price and extraction.price < old_price:
drop_percent = (old_price - extraction.price) / old_price * 100
if drop_percent >= 5: # Notify only for significant drops (> 5%)
await NotificationService.send_notification(
channel,
title=f"📉 Baisse de prix : {item_data['name']}",
body=f"Le prix de {item_data['name']} a baissé de {drop_percent:.1f}% !\nNouveau prix : {extraction.price}€ (Ancien: {old_price}€)\n{item_data['url']}"
)
except Exception as e:
logger.error(f"Error in process_item_check: {e}")
await loop.run_in_executor(None, _update_db_error, item_id, str(e))
async def scheduled_refresh():
logger.info("Heartbeat: Checking for items due for refresh")
loop = asyncio.get_running_loop()
try:
with database.SessionLocal() as session:
due_items = await loop.run_in_executor(None, ItemService.get_due_items, session)
for item_id, _, _ in due_items:
await process_item_check(item_id)
except Exception as e:
logger.error(f"Error in scheduled refresh: {e}", exc_info=True)