|
"""
|
|
Web search module for Gift Finder to search for real gift products.
|
|
|
|
This module uses SerpAPI (or falls back to custom web scraping) to find real gift recommendations
|
|
based on the search queries generated by the Gift Finder model.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
import random
|
|
import time
|
|
import hashlib
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Dict, Any, Optional
|
|
import requests
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class GiftSearchEngine:
|
|
"""Search engine for finding gift products online."""
|
|
|
|
def __init__(self, api_key: Optional[str] = None, cache_ttl: int = 3600):
|
|
"""
|
|
Initialize the gift search engine.
|
|
|
|
Args:
|
|
api_key: API key for SerpAPI (optional)
|
|
cache_ttl: Cache time-to-live in seconds (default: 1 hour)
|
|
"""
|
|
self.api_key = api_key or os.environ.get("SERPAPI_API_KEY")
|
|
self.cache = {}
|
|
self.cache_ttl = cache_ttl
|
|
|
|
if not self.api_key:
|
|
logger.warning("No SerpAPI key provided. Will use fallback search methods.")
|
|
|
|
def search_gifts(self, query: str,
|
|
num_results: int = 5,
|
|
price_min: Optional[float] = None,
|
|
price_max: Optional[float] = None) -> List[Dict[str, Any]]:
|
|
"""
|
|
Search for gifts based on the query.
|
|
|
|
Args:
|
|
query: The search query
|
|
num_results: Number of results to return
|
|
price_min: Minimum price (optional)
|
|
price_max: Maximum price (optional)
|
|
|
|
Returns:
|
|
List of gift recommendations
|
|
"""
|
|
|
|
cache_key = self._create_cache_key(query, num_results, price_min, price_max)
|
|
|
|
|
|
cached_results = self._get_from_cache(cache_key)
|
|
if cached_results:
|
|
logger.info(f"Using cached results for query: {query}")
|
|
return cached_results
|
|
|
|
|
|
if self.api_key:
|
|
try:
|
|
results = self._search_with_serpapi(query, num_results, price_min, price_max)
|
|
if results:
|
|
|
|
self._add_to_cache(cache_key, results)
|
|
return results
|
|
except Exception as e:
|
|
logger.error(f"Error using SerpAPI: {str(e)}")
|
|
logger.warning("Falling back to alternative search method")
|
|
|
|
|
|
results = self._search_with_fallback(query, num_results, price_min, price_max)
|
|
|
|
|
|
self._add_to_cache(cache_key, results)
|
|
|
|
return results
|
|
|
|
def _create_cache_key(self, query: str, num_results: int,
|
|
price_min: Optional[float], price_max: Optional[float]) -> str:
|
|
"""Create a unique cache key from the search parameters."""
|
|
key_parts = [
|
|
query.lower().strip(),
|
|
str(num_results),
|
|
str(price_min) if price_min is not None else "none",
|
|
str(price_max) if price_max is not None else "none"
|
|
]
|
|
key_string = ":".join(key_parts)
|
|
return hashlib.md5(key_string.encode()).hexdigest()
|
|
|
|
def _add_to_cache(self, key: str, results: List[Dict[str, Any]]) -> None:
|
|
"""Add results to the cache with expiration time."""
|
|
self.cache[key] = {
|
|
"results": results,
|
|
"expires": datetime.now() + timedelta(seconds=self.cache_ttl)
|
|
}
|
|
|
|
def _get_from_cache(self, key: str) -> Optional[List[Dict[str, Any]]]:
|
|
"""Get results from cache if they exist and are not expired."""
|
|
if key in self.cache:
|
|
cache_item = self.cache[key]
|
|
if cache_item["expires"] > datetime.now():
|
|
return cache_item["results"]
|
|
else:
|
|
|
|
del self.cache[key]
|
|
return None
|
|
|
|
def _search_with_serpapi(self, query: str,
|
|
num_results: int = 5,
|
|
price_min: Optional[float] = None,
|
|
price_max: Optional[float] = None) -> List[Dict[str, Any]]:
|
|
"""
|
|
Search for gifts using SerpAPI.
|
|
|
|
Args:
|
|
query: The search query
|
|
num_results: Number of results to return
|
|
price_min: Minimum price (optional)
|
|
price_max: Maximum price (optional)
|
|
|
|
Returns:
|
|
List of gift recommendations
|
|
"""
|
|
logger.info(f"Searching for gifts with SerpAPI: {query}")
|
|
|
|
|
|
params = {
|
|
"api_key": self.api_key,
|
|
"engine": "google_shopping",
|
|
"q": f"{query}",
|
|
"num": num_results,
|
|
"hl": "en",
|
|
"gl": "us"
|
|
}
|
|
|
|
|
|
if price_min is not None:
|
|
params["price_min"] = price_min
|
|
if price_max is not None:
|
|
params["price_max"] = price_max
|
|
|
|
|
|
response = requests.get(
|
|
"https://serpapi.com/search",
|
|
params=params
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
logger.error(f"SerpAPI error: {response.status_code} - {response.text}")
|
|
return []
|
|
|
|
|
|
try:
|
|
data = response.json()
|
|
shopping_results = data.get("shopping_results", [])
|
|
|
|
if not shopping_results:
|
|
logger.warning("No shopping results found")
|
|
return []
|
|
|
|
|
|
results = []
|
|
for item in shopping_results[:num_results]:
|
|
if 'title' not in item or 'link' not in item:
|
|
continue
|
|
|
|
result = {
|
|
"name": item.get("title", ""),
|
|
"price": item.get("price", ""),
|
|
"url": item.get("link", ""),
|
|
"description": item.get("snippet", ""),
|
|
"image": item.get("thumbnail", ""),
|
|
"source": "serpapi"
|
|
}
|
|
results.append(result)
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error parsing SerpAPI response: {str(e)}")
|
|
return []
|
|
|
|
def _search_with_fallback(self, query: str,
|
|
num_results: int = 5,
|
|
price_min: Optional[float] = None,
|
|
price_max: Optional[float] = None) -> List[Dict[str, Any]]:
|
|
"""
|
|
Fallback search method when SerpAPI is not available.
|
|
This uses alternative free methods or simulates results.
|
|
|
|
Args:
|
|
query: The search query
|
|
num_results: Number of results to return
|
|
price_min: Minimum price (optional)
|
|
price_max: Maximum price (optional)
|
|
|
|
Returns:
|
|
List of gift recommendations
|
|
"""
|
|
logger.info(f"Using fallback search for: {query}")
|
|
|
|
|
|
terms = query.lower().split()
|
|
occasion = next((term for term in ["birthday", "christmas", "anniversary", "wedding", "graduation"]
|
|
if term in terms), "gift")
|
|
|
|
relationship = next((term for term in ["brother", "sister", "mom", "dad", "wife", "husband", "friend", "girlfriend", "boyfriend"]
|
|
if term in terms), "person")
|
|
|
|
interests = []
|
|
interest_keywords = ["enjoys", "likes", "loves", "fan", "hobby", "interest", "passion"]
|
|
for i, word in enumerate(terms):
|
|
if word in interest_keywords and i+1 < len(terms):
|
|
|
|
interests.extend(terms[i+1:i+5])
|
|
|
|
|
|
budget_terms = {
|
|
"cheap": (5, 30),
|
|
"inexpensive": (5, 30),
|
|
"affordable": (20, 50),
|
|
"moderate": (30, 100),
|
|
"expensive": (75, 200),
|
|
"luxury": (150, 500)
|
|
}
|
|
|
|
budget_term = next((term for term in budget_terms if term in terms), "moderate")
|
|
budget_range = budget_terms.get(budget_term, (30, 100))
|
|
|
|
if price_min is None:
|
|
price_min = budget_range[0]
|
|
if price_max is None:
|
|
price_max = budget_range[1]
|
|
|
|
|
|
products = self._generate_gift_ideas(occasion, relationship, interests, price_min, price_max)
|
|
|
|
|
|
return products[:num_results]
|
|
|
|
def _generate_gift_ideas(self, occasion: str, relationship: str, interests: List[str],
|
|
price_min: float, price_max: float) -> List[Dict[str, Any]]:
|
|
"""
|
|
Generate realistic gift ideas based on the extracted information.
|
|
|
|
Args:
|
|
occasion: The gift occasion
|
|
relationship: Relationship to the recipient
|
|
interests: List of recipient interests
|
|
price_min: Minimum price
|
|
price_max: Maximum price
|
|
|
|
Returns:
|
|
List of gift recommendations
|
|
"""
|
|
|
|
gift_ideas = {
|
|
"gaming": [
|
|
{"name": "Nintendo Switch Lite", "price": "$199.99", "description": "Portable gaming console with access to a vast library of games"},
|
|
{"name": "PlayStation Store Gift Card", "price": "$50.00", "description": "Digital gift card for PlayStation games and content"},
|
|
{"name": "Gaming Mouse", "price": "$29.99", "description": "High-precision gaming mouse with customizable RGB lighting"},
|
|
{"name": "Gaming Headset", "price": "$59.99", "description": "Immersive gaming headset with noise-canceling microphone"},
|
|
{"name": "Xbox Game Pass Subscription", "price": "$44.99", "description": "3-month subscription to Xbox Game Pass with access to hundreds of games"}
|
|
],
|
|
"reading": [
|
|
{"name": "Kindle Paperwhite", "price": "$139.99", "description": "Waterproof e-reader with adjustable warm light"},
|
|
{"name": "Book of the Month Subscription", "price": "$49.99", "description": "3-month subscription to receive bestselling books"},
|
|
{"name": "Personalized Bookmark Set", "price": "$15.99", "description": "Set of 3 custom engraved metal bookmarks"},
|
|
{"name": "Reading Light", "price": "$19.99", "description": "Rechargeable LED reading light with multiple brightness levels"},
|
|
{"name": "Literary Candle", "price": "$24.99", "description": "Book-inspired scented candle perfect for reading sessions"}
|
|
],
|
|
"cooking": [
|
|
{"name": "Instant Pot Duo", "price": "$89.99", "description": "7-in-1 multi-functional pressure cooker"},
|
|
{"name": "Chef's Knife", "price": "$49.99", "description": "High-carbon stainless steel chef's knife for precision cutting"},
|
|
{"name": "Spice Set", "price": "$35.99", "description": "Collection of 20 essential cooking spices in glass jars"},
|
|
{"name": "Digital Kitchen Scale", "price": "$22.99", "description": "Precise digital scale for cooking and baking"},
|
|
{"name": "Cookbook Stand", "price": "$29.99", "description": "Adjustable cookbook holder with splatter guard"}
|
|
],
|
|
"music": [
|
|
{"name": "Bluetooth Speaker", "price": "$79.99", "description": "Portable wireless speaker with rich sound and long battery life"},
|
|
{"name": "Vinyl Record Player", "price": "$149.99", "description": "Vintage-style turntable with modern Bluetooth connectivity"},
|
|
{"name": "Spotify Premium Gift Card", "price": "$29.99", "description": "3-month subscription to ad-free music streaming"},
|
|
{"name": "Noise-Canceling Headphones", "price": "$129.99", "description": "Over-ear headphones with active noise cancellation"},
|
|
{"name": "Concert Ticket Gift Card", "price": "$50.00", "description": "Gift card toward concert tickets at major venues"}
|
|
],
|
|
"sports": [
|
|
{"name": "Fitness Tracker", "price": "$89.99", "description": "Activity tracker with heart rate monitoring and GPS"},
|
|
{"name": "Basketball", "price": "$29.99", "description": "Official size indoor/outdoor basketball"},
|
|
{"name": "Sports Team Merchandise", "price": "$45.99", "description": "Official licensed team apparel"},
|
|
{"name": "Yoga Mat", "price": "$24.99", "description": "Non-slip exercise mat for yoga and fitness"},
|
|
{"name": "Insulated Water Bottle", "price": "$34.99", "description": "Vacuum-insulated stainless steel water bottle"}
|
|
],
|
|
"technology": [
|
|
{"name": "Wireless Earbuds", "price": "$129.99", "description": "True wireless earbuds with noise cancellation and long battery life"},
|
|
{"name": "Smart Speaker", "price": "$99.99", "description": "Voice-controlled speaker with digital assistant"},
|
|
{"name": "Portable Charger", "price": "$49.99", "description": "20000mAh power bank for smartphones and tablets"},
|
|
{"name": "Smart Watch", "price": "$199.99", "description": "Fitness and health tracking smartwatch"},
|
|
{"name": "Wireless Charging Pad", "price": "$29.99", "description": "Fast wireless charger compatible with most smartphones"}
|
|
],
|
|
"art": [
|
|
{"name": "Drawing Tablet", "price": "$79.99", "description": "Digital drawing tablet with pressure sensitivity"},
|
|
{"name": "Watercolor Paint Set", "price": "$45.99", "description": "Professional grade watercolor paints with brushes"},
|
|
{"name": "Adult Coloring Book Set", "price": "$24.99", "description": "Set of 3 detailed coloring books with pencils"},
|
|
{"name": "Digital Picture Frame", "price": "$99.99", "description": "Wi-Fi enabled digital frame that displays photos from the cloud"},
|
|
{"name": "Art Print", "price": "$35.99", "description": "Museum-quality art print from independent artists"}
|
|
],
|
|
"travel": [
|
|
{"name": "Packing Cubes", "price": "$24.99", "description": "Set of 6 lightweight packing organizers for travel"},
|
|
{"name": "Travel Pillow", "price": "$29.99", "description": "Memory foam neck pillow for comfortable travel"},
|
|
{"name": "Scratch-Off World Map", "price": "$35.99", "description": "Interactive map to track visited countries"},
|
|
{"name": "Luggage Tag Set", "price": "$19.99", "description": "Set of 4 personalized luggage tags"},
|
|
{"name": "Universal Travel Adapter", "price": "$24.99", "description": "All-in-one adapter with USB ports for international travel"}
|
|
]
|
|
}
|
|
|
|
|
|
gift_ideas["generic"] = [
|
|
{"name": "Custom Photo Frame", "price": "$29.99", "description": "Personalized photo frame with custom engraving"},
|
|
{"name": "Scented Candle Gift Set", "price": "$34.99", "description": "Set of 3 premium scented candles in gift box"},
|
|
{"name": "Gourmet Chocolate Box", "price": "$39.99", "description": "Assortment of premium chocolates from around the world"},
|
|
{"name": "Streaming Service Subscription", "price": "$44.99", "description": "3-month subscription to premium streaming service"},
|
|
{"name": "Premium Coffee Sampler", "price": "$24.99", "description": "Collection of specialty coffees from top roasters"}
|
|
]
|
|
|
|
|
|
categories = ["generic"]
|
|
for interest in interests:
|
|
for category in gift_ideas.keys():
|
|
if interest and category in interest:
|
|
categories.append(category)
|
|
|
|
|
|
if len(categories) == 1:
|
|
|
|
categories.append(random.choice(list(gift_ideas.keys())))
|
|
|
|
|
|
potential_gifts = []
|
|
for category in categories:
|
|
potential_gifts.extend(gift_ideas.get(category, []))
|
|
|
|
|
|
filtered_gifts = []
|
|
for gift in potential_gifts:
|
|
price_str = gift["price"]
|
|
price_value = float(price_str.replace("$", "").replace(",", ""))
|
|
if price_min <= price_value <= price_max:
|
|
filtered_gifts.append(gift)
|
|
|
|
|
|
if len(filtered_gifts) < 3:
|
|
filtered_gifts = potential_gifts
|
|
|
|
|
|
random.shuffle(filtered_gifts)
|
|
|
|
|
|
results = []
|
|
for item in filtered_gifts:
|
|
|
|
retailers = {
|
|
"amazon": "https://www.amazon.com/s?k=",
|
|
"walmart": "https://www.walmart.com/search/?query=",
|
|
"target": "https://www.target.com/s?searchTerm=",
|
|
"bestbuy": "https://www.bestbuy.com/site/searchpage.jsp?st="
|
|
}
|
|
|
|
retailer = random.choice(list(retailers.keys()))
|
|
query_term = item["name"].replace(" ", "+")
|
|
url = f"{retailers[retailer]}{query_term}"
|
|
|
|
|
|
image_urls = {
|
|
"gaming": "https://m.media-amazon.com/images/I/71S-OtNPVrL._AC_SL1500_.jpg",
|
|
"reading": "https://m.media-amazon.com/images/I/61Ek12ufRxL._AC_SL1500_.jpg",
|
|
"cooking": "https://m.media-amazon.com/images/I/71WtwLzGhWL._AC_SL1500_.jpg",
|
|
"music": "https://m.media-amazon.com/images/I/61stQYWQO4L._AC_SL1500_.jpg",
|
|
"sports": "https://m.media-amazon.com/images/I/61wEWNA5P4L._AC_SL1500_.jpg",
|
|
"technology": "https://m.media-amazon.com/images/I/61aJaknbncL._AC_SL1500_.jpg",
|
|
"art": "https://m.media-amazon.com/images/I/71KvYB+qyYL._AC_SL1500_.jpg",
|
|
"travel": "https://m.media-amazon.com/images/I/81kJQoZcBfL._AC_SL1500_.jpg",
|
|
"generic": "https://m.media-amazon.com/images/I/81O+V3oLSOL._AC_SL1500_.jpg"
|
|
}
|
|
|
|
|
|
relevant_category = "generic"
|
|
for category in gift_ideas.keys():
|
|
if item in gift_ideas[category]:
|
|
relevant_category = category
|
|
break
|
|
|
|
image_url = image_urls.get(relevant_category, image_urls["generic"])
|
|
|
|
result = {
|
|
"name": item["name"],
|
|
"price": item["price"],
|
|
"url": url,
|
|
"description": item["description"],
|
|
"image": image_url,
|
|
"retailer": retailer,
|
|
"source": "custom_search"
|
|
}
|
|
results.append(result)
|
|
|
|
return results
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
search_engine = GiftSearchEngine()
|
|
|
|
|
|
query = "birthday gift for brother who enjoys gaming and basketball under $50"
|
|
|
|
|
|
results = search_engine.search_gifts(query, num_results=3, price_max=50)
|
|
|
|
|
|
print(f"Search query: {query}")
|
|
print(f"Found {len(results)} results:")
|
|
for i, result in enumerate(results, 1):
|
|
print(f"{i}. {result['name']} - {result['price']}")
|
|
print(f" {result['description']}")
|
|
print(f" {result['url']}")
|
|
print() |