mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-11 17:29:14 +02:00
99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
import logging
|
|
import json
|
|
import os
|
|
from typing import Optional
|
|
from app.services.ai_service import AIService
|
|
from litellm import acompletion
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class AIPriceExtractor:
|
|
# Primary model requested by user (Gemma 3 Free)
|
|
PRIMARY_MODEL = "openrouter/google/gemma-3-27b-it:free"
|
|
# Fallback model (known to work/be free)
|
|
FALLBACK_MODEL = "openrouter/google/gemma-2-9b-it:free"
|
|
|
|
@classmethod
|
|
async def extract_price(cls, html_content: str, product_title: str) -> Optional[float]:
|
|
"""
|
|
Extract price from HTML using AI (Gemma 3 via OpenRouter with fallback).
|
|
"""
|
|
try:
|
|
# Get config to retrieve API key
|
|
config = AIService.get_ai_config()
|
|
api_key = config.get("api_key")
|
|
|
|
# Fallback to env var if not in DB config
|
|
if not api_key:
|
|
api_key = os.getenv("OPENROUTER_API_KEY")
|
|
|
|
if not api_key:
|
|
logger.warning("No OpenRouter API key found for AI Price Extraction")
|
|
return None
|
|
|
|
# Smart truncation: keep first 15000 chars to ensure we capture the price
|
|
# (3000 was too short for B&M)
|
|
clean_html = html_content[:15000]
|
|
|
|
prompt = f"""
|
|
You are a price extraction expert.
|
|
Task: Extract the CURRENT SELLING PRICE for the product "{product_title}" from the HTML snippet below.
|
|
|
|
Rules:
|
|
1. Return ONLY the numeric value (e.g., 24.95).
|
|
2. Ignore crossed-out prices (old prices).
|
|
3. If multiple prices exist, choose the one that seems to be the current effective price (usually the lowest non-crossed-out one).
|
|
4. If no price is found, return "null".
|
|
5. Output format: JSON {{ "price": 24.95 }}
|
|
|
|
HTML Snippet:
|
|
{clean_html}
|
|
"""
|
|
|
|
try:
|
|
# Try Primary Model (Gemma 3)
|
|
# Note: Removed response_format={"type": "json_object"} as it's not supported by all models
|
|
response = await acompletion(
|
|
model=cls.PRIMARY_MODEL,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
api_key=api_key,
|
|
api_base="https://openrouter.ai/api/v1",
|
|
temperature=0.1
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Primary model {cls.PRIMARY_MODEL} failed ({e}), trying fallback {cls.FALLBACK_MODEL}...")
|
|
# Try Fallback Model (Gemma 2)
|
|
response = await acompletion(
|
|
model=cls.FALLBACK_MODEL,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
api_key=api_key,
|
|
api_base="https://openrouter.ai/api/v1",
|
|
temperature=0.1
|
|
)
|
|
|
|
content = response.choices[0].message.content
|
|
if not content:
|
|
return None
|
|
|
|
# Clean markdown code blocks if present (common with LLMs)
|
|
content = content.replace("```json", "").replace("```", "").strip()
|
|
|
|
try:
|
|
data = json.loads(content)
|
|
price = data.get("price")
|
|
|
|
if price and str(price).lower() != "null":
|
|
# Handle string price "24,95" or "24.95"
|
|
if isinstance(price, str):
|
|
price = price.replace(',', '.').replace('€', '').strip()
|
|
return float(price)
|
|
except json.JSONDecodeError:
|
|
logger.warning(f"Failed to parse AI response as JSON: {content}")
|
|
return None
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"AI Price Extraction failed: {e}")
|
|
return None
|