Spaces:
Sleeping
Sleeping
| from langchain_google_genai import ChatGoogleGenerativeAI | |
| from langchain.agents import AgentExecutor, create_tool_calling_agent | |
| from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder | |
| from langchain.memory import ConversationBufferMemory | |
| from typing import Dict, Any, List, Optional | |
| import json | |
| import re | |
| from config import settings | |
| from tools import ALL_TOOLS | |
| from database import db, Platform | |
| SYSTEM_PROMPT = """You are a helpful E-commerce Product Suggestion Bot for users in India. | |
| CRITICAL FIRST STEP - USER PROFILE COLLECTION: | |
| 1. ALWAYS start by checking if user profile is complete (has name, age, location) | |
| 2. If ANY field is missing, collect it first before proceeding to product suggestions | |
| 3. Store each detail immediately after collection | |
| PROFILE COLLECTION FLOW: | |
| - If location is missing: Ask "Welcome! To provide personalized suggestions, what's your location? (e.g., Mumbai, Maharashtra or pincode)" | |
| - If name is missing: Ask "Great! What's your name?" | |
| - If age is missing: Ask "Thanks! What's your age?" | |
| - Once profile complete: Say "Perfect! Now you can ask for product suggestions. Example: 'I need a laptop under ₹50000'" | |
| PRODUCT SUGGESTION WORKFLOW (only after profile is complete): | |
| 1. Extract the product type/category and budget from user's message | |
| 2. Search for ANY products the user asks for - mobiles, laptops, cycles, books, electronics, appliances, clothing, etc. | |
| 3. Show top 3-5 products ranked by value-for-money, rating, and availability | |
| 4. Format response: "Top Picks: [name1], [name2], [name3]" as first line | |
| CONSTRAINTS: | |
| - NEVER proceed to product suggestions if profile is incomplete | |
| - Search for ANY product type the user requests | |
| - Always enforce price ≤ budget | |
| - Keep text short for low bandwidth""" | |
| class SuggestionAgent: | |
| def __init__(self): | |
| self.llm = ChatGoogleGenerativeAI( | |
| model="gemini-2.5-flash", | |
| api_key=settings.gemini_api_key, | |
| temperature=settings.temperature, | |
| ) | |
| def process_request( | |
| self, | |
| platform: str, | |
| platform_user_id: str, | |
| user_message: str, | |
| image_data: Optional[bytes] = None, | |
| audio_data: Optional[bytes] = None | |
| ) -> Dict[str, Any]: | |
| """Process user request and return suggestions.""" | |
| try: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| # Step 1: Get or create user | |
| user = db.get_user(Platform(platform), platform_user_id) | |
| if not user: | |
| user_id = db.upsert_user(Platform(platform), platform_user_id) | |
| user = db.get_user(Platform(platform), platform_user_id) | |
| # Step 2: Check if profile is complete | |
| needs_location = not user.get("location_str") | |
| needs_name = not user.get("name") | |
| needs_age = not user.get("age") | |
| # Step 3: Handle profile collection | |
| if needs_location: | |
| # Check if user is providing location | |
| if self._looks_like_location(user_message): | |
| db.upsert_user(Platform(platform), platform_user_id, location_str=user_message) | |
| return { | |
| "success": True, | |
| "response_text": "Great! Location saved. What's your name?", | |
| "picks": [] | |
| } | |
| else: | |
| return { | |
| "success": True, | |
| "response_text": "Welcome! To provide personalized suggestions, what's your location? (e.g., Mumbai, Maharashtra or pincode)", | |
| "picks": [] | |
| } | |
| if needs_name: | |
| # Check if this is a name (simple heuristic) | |
| if not any(char.isdigit() for char in user_message) and len(user_message.split()) <= 3: | |
| db.upsert_user(Platform(platform), platform_user_id, name=user_message.strip()) | |
| return { | |
| "success": True, | |
| "response_text": "Nice to meet you! What's your age?", | |
| "picks": [] | |
| } | |
| return { | |
| "success": True, | |
| "response_text": "Thanks! What's your name?", | |
| "picks": [] | |
| } | |
| if needs_age: | |
| # Check if this is an age | |
| try: | |
| age = int(user_message.strip()) | |
| if 5 <= age <= 120: | |
| db.upsert_user(Platform(platform), platform_user_id, age=age) | |
| return { | |
| "success": True, | |
| "response_text": "Perfect! Now you can ask for product suggestions.\n\nExamples:\n• 'I need a laptop under ₹50000'\n• 'Show me headphones under ₹3000'\n• 'Cycle under ₹8000'\n• 'Mobile under 15k'", | |
| "picks": [] | |
| } | |
| except: | |
| pass | |
| return { | |
| "success": True, | |
| "response_text": "Great! What's your age?", | |
| "picks": [] | |
| } | |
| # Step 4: Profile complete - handle product search | |
| enriched_message = user_message | |
| if image_data or audio_data: | |
| logger.info("Processing multimodal input...") | |
| media_context = self._process_multimodal(image_data, audio_data, user_message) | |
| logger.info(f"Media context extracted: {media_context}") | |
| # For multimodal inputs, let Gemini handle everything and pass the full context | |
| if media_context and len(media_context.strip()) > 0: | |
| enriched_message = media_context | |
| logger.info(f"Using multimodal context as search query: {enriched_message}") | |
| # For image data, we keep the original caption and add context | |
| if image_data and media_context and len(media_context.strip()) > 0: | |
| enriched_message = f"{user_message}\n\n{media_context}" | |
| # Generate search query using Gemini (supports all languages) | |
| search_query = self._generate_search_query(enriched_message, image_data, audio_data) | |
| if not search_query or len(search_query.strip()) == 0: | |
| return { | |
| "success": True, | |
| "response_text": "Please specify what product you're looking for and your budget.\n\nExamples:\n• 'I need a laptop under ₹50000'\n• 'Show me cycles under ₹7000'\n• 'Headphones under ₹2000'\n• 'Mobile under ₹15k'", | |
| "picks": [] | |
| } | |
| # Extract category and budget from the generated query for caching | |
| category, budget = self._extract_category_budget(search_query) | |
| if not category or not budget: | |
| return { | |
| "success": True, | |
| "response_text": "Please specify what product you're looking for and your budget.\n\nExamples:\n• 'I need a laptop under ₹50000'\n• 'Show me cycles under ₹7000'\n• 'Headphones under ₹2000'\n• 'Mobile under ₹15k'", | |
| "picks": [] | |
| } | |
| # Check cache | |
| location_key = db.make_location_key(user.get("location_str")) | |
| cached = db.cache_get(category, budget, location_key) | |
| if cached: | |
| logger.info("Using cached results") | |
| return { | |
| "success": True, | |
| "response_text": cached["response_text"], | |
| "picks": cached.get("picks_json", []) | |
| } | |
| # Search for products using the generated search query | |
| picks = self._search_products(search_query, budget, user.get("location_str", "India")) | |
| if not picks: | |
| return { | |
| "success": True, | |
| "response_text": f"I couldn't find {category} products under ₹{budget} right now. Try:\n• Increasing your budget (e.g., ₹{int(budget * 1.5)})\n• Using different keywords (e.g., 'smartphone' instead of 'mobile')\n• Trying a different product category", | |
| "picks": [] | |
| } | |
| # Pass products to LLM for natural formatting | |
| num_to_show = min(5, len(picks)) | |
| top_picks = picks[:num_to_show] | |
| # Prepare product data for LLM | |
| products_data = [] | |
| for i, pick in enumerate(top_picks, 1): | |
| product_info = { | |
| "number": i, | |
| "name": pick['name'], | |
| "price": f"₹{pick['price']:,}", | |
| "rating": f"{pick.get('rating', 0):.1f}/5" if pick.get('rating') and pick['rating'] > 0 else "N/A", | |
| "store": pick.get('source', 'Online'), | |
| "link": pick.get('link', '') | |
| } | |
| products_data.append(product_info) | |
| # Ask LLM to format the response | |
| response = self._format_products_with_llm(category, budget, products_data, user.get("name", "there")) | |
| # Cache the results | |
| picks_json = [{"name": p["name"], "rank": i+1, "price": p["price"], "link": p.get("link", "")} for i, p in enumerate(top_picks)] | |
| db.cache_put( | |
| user_id=user["id"], | |
| category=category, | |
| budget_inr=budget, | |
| location_key=location_key, | |
| response_text=response, | |
| picks_json=picks_json, | |
| ttl_hours=24 | |
| ) | |
| return { | |
| "success": True, | |
| "response_text": response, | |
| "picks": picks_json | |
| } | |
| except Exception as e: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.error(f"Error in process_request: {str(e)}", exc_info=True) | |
| return { | |
| "success": False, | |
| "error": str(e), | |
| "response_text": "Sorry, I encountered an error. Please try again later.", | |
| "picks": [] | |
| } | |
| def _format_products_with_llm(self, category: str, budget: int, products: list, user_name: str) -> str: | |
| """Use LLM to format product suggestions naturally.""" | |
| try: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| # Create prompt for LLM | |
| products_json = json.dumps(products, indent=2) | |
| prompt = f"""You are a helpful shopping assistant. Format these products for the user. | |
| User: {user_name} | |
| Looking for: {category} under ₹{budget} | |
| Products data: | |
| {products_json} | |
| Format EXACTLY like this structure. DO NOT skip any fields, especially links: | |
| Top Picks: <first 3 product names, shortened> | |
| Here are {len(products)} great {category} options for you! | |
| <For each product in the list above> | |
| <number>. <name> | |
| 💰 Price: <price from data> | |
| ⭐ Rating: <rating from data> | |
| 🛒 Store: <store from data> | |
| 🔗 Link: <link from data - COPY EXACT URL> | |
| CRITICAL: You MUST include the "🔗 Link:" line with the actual URL from the data for EVERY product. Do not skip links! | |
| """ | |
| # Call LLM | |
| response = self.llm.invoke(prompt) | |
| if hasattr(response, 'content'): | |
| return response.content | |
| else: | |
| return str(response) | |
| except Exception as e: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.error(f"LLM formatting error: {e}") | |
| # Fallback to simple formatting | |
| response = f"Top Picks: {', '.join([p['name'][:50] for p in products[:3]])}\n\n" | |
| response += f"Here are {len(products)} great {category} options under ₹{budget}:\n\n" | |
| for product in products: | |
| response += f"{product['number']}. {product['name']}\n" | |
| response += f" 💰 Price: {product['price']}\n" | |
| if product['rating'] != "N/A": | |
| response += f" ⭐ Rating: {product['rating']}\n" | |
| response += f" 🛒 Store: {product['store']}\n" | |
| if product['link']: | |
| response += f" 🔗 Link: {product['link']}\n" | |
| response += "\n" | |
| return response | |
| def _looks_like_location(self, message: str) -> bool: | |
| """Check if message looks like a location.""" | |
| message = message.strip() | |
| # Check if it's a 6-digit pincode | |
| if message.isdigit() and len(message) == 6: | |
| return True | |
| # Check if it's a city/location name (no numbers, reasonable length) | |
| if not any(char.isdigit() for char in message) and 3 <= len(message) <= 50: | |
| # Exclude common greetings | |
| greetings = ['hi', 'hello', 'hey', 'thanks', 'ok', 'yes', 'no', 'hii', 'hiii'] | |
| if message.lower() not in greetings: | |
| return True | |
| return False | |
| def _search_products(self, search_query: str, budget: int, location: str) -> List[Dict]: | |
| """Search for products using Google Shopping Light API - supports ANY product type.""" | |
| try: | |
| import serpapi | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| # Use the search query directly (already processed by Gemini) | |
| query = search_query | |
| logger.info(f"Searching for: {query}") | |
| client = serpapi.Client(api_key=settings.serpapi_api_key) | |
| results = client.search({ | |
| "engine": "google_shopping_light", | |
| "q": query, | |
| "gl": "in", | |
| "hl": "en", | |
| "max_price": budget | |
| }) | |
| candidates = [] | |
| shopping_results = results.get("shopping_results", []) | |
| logger.info(f"Found {len(shopping_results)} shopping results from API") | |
| for item in shopping_results: | |
| try: | |
| # Try multiple price fields | |
| price_str = item.get("extracted_price", | |
| item.get("price", | |
| item.get("displayed_price", "0"))) | |
| if isinstance(price_str, str): | |
| # Remove currency symbols and commas | |
| price_clean = price_str.replace(",", "").replace("₹", "").replace("Rs", "").replace("INR", "").strip() | |
| # Extract just the number | |
| import re | |
| price_match = re.search(r'(\d+(?:\.\d+)?)', price_clean) | |
| if price_match: | |
| price = float(price_match.group(1)) | |
| else: | |
| continue | |
| else: | |
| price = float(price_str) | |
| # Be more lenient with budget (allow up to 10% over) | |
| if price > 0 and price <= budget * 1.1: | |
| # Extract rating safely | |
| rating = item.get("rating", 0) | |
| if rating and isinstance(rating, (int, float)): | |
| rating = float(rating) | |
| else: | |
| rating = 0.0 | |
| product_link = item.get("product_link", item.get("link", "")) | |
| # URL encode the link to handle spaces and special characters | |
| if product_link: | |
| from urllib.parse import quote | |
| # Only encode the query part and parameters, not the base URL | |
| if 'http' in product_link: | |
| try: | |
| # Parse the URL and encode the query string | |
| from urllib.parse import urlparse, parse_qs, urlencode, urlunparse | |
| parsed = urlparse(product_link) | |
| if parsed.query: | |
| # Re-encode the query string to handle spaces | |
| encoded_query = urlencode(parse_qs(parsed.query), doseq=True) | |
| product_link = urlunparse(parsed._replace(query=encoded_query)) | |
| except: | |
| # Fallback: URL encode the entire string if parsing fails | |
| product_link = quote(product_link, safe=':/?#[]@!$&\'()*+,;=') | |
| candidates.append({ | |
| "name": item.get("title", "Unknown Product"), | |
| "price": int(price), | |
| "rating": rating, | |
| "source": item.get("source", "Online"), | |
| "link": product_link | |
| }) | |
| except Exception as e: | |
| logger.debug(f"Skipping item due to error: {e}") | |
| continue | |
| logger.info(f"Found {len(candidates)} products within budget") | |
| # If we found very few results, try a broader search | |
| if len(candidates) < 3: | |
| logger.info("Few results found, trying broader search...") | |
| # Extract category from search query for broader search fallback | |
| fallback_category, _ = self._extract_category_budget(search_query) | |
| broad_query = f"{fallback_category or search_query} India" | |
| results = client.search({ | |
| "engine": "google_shopping_light", | |
| "q": broad_query, | |
| "gl": "in", | |
| "hl": "en", | |
| "max_price": budget | |
| }) | |
| for item in results.get("shopping_results", []): | |
| try: | |
| price_str = item.get("extracted_price", item.get("price", "0")) | |
| if isinstance(price_str, str): | |
| price_clean = price_str.replace(",", "").replace("₹", "").replace("Rs", "").replace("INR", "").strip() | |
| price_match = re.search(r'(\d+(?:\.\d+)?)', price_clean) | |
| if price_match: | |
| price = float(price_match.group(1)) | |
| else: | |
| continue | |
| else: | |
| price = float(price_str) | |
| if price > 0 and price <= budget * 1.1: | |
| rating = item.get("rating", 0) | |
| if rating and isinstance(rating, (int, float)): | |
| rating = float(rating) | |
| else: | |
| rating = 0.0 | |
| # Avoid duplicates | |
| title = item.get("title", "Unknown Product") | |
| if not any(c["name"] == title for c in candidates): | |
| product_link = item.get("product_link", item.get("link", "")) | |
| # URL encode the link to handle spaces and special characters | |
| if product_link: | |
| from urllib.parse import quote | |
| # Only encode the query part and parameters, not the base URL | |
| if 'http' in product_link: | |
| try: | |
| # Parse the URL and encode the query string | |
| from urllib.parse import urlparse, parse_qs, urlencode, urlunparse | |
| parsed = urlparse(product_link) | |
| if parsed.query: | |
| # Re-encode the query string to handle spaces | |
| encoded_query = urlencode(parse_qs(parsed.query), doseq=True) | |
| product_link = urlunparse(parsed._replace(query=encoded_query)) | |
| except: | |
| # Fallback: URL encode the entire string if parsing fails | |
| product_link = quote(product_link, safe=':/?#[]@!$&\'()*+,;=') | |
| candidates.append({ | |
| "name": title, | |
| "price": int(price), | |
| "rating": rating, | |
| "source": item.get("source", "Online"), | |
| "link": product_link | |
| }) | |
| except Exception as e: | |
| continue | |
| # Sort by: 1) Rating (highest first), 2) Price (lowest first) | |
| candidates.sort(key=lambda x: (-(x.get("rating") or 0), x["price"])) | |
| logger.info(f"Returning {len(candidates)} products") | |
| return candidates | |
| except Exception as e: | |
| import logging | |
| logging.getLogger(__name__).error(f"Search error: {e}", exc_info=True) | |
| return [] | |
| def _fallback_response(self, user_message: str, platform: str, platform_user_id: str) -> Dict[str, Any]: | |
| """Provide a fallback response when agent fails.""" | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.info("Using fallback response mechanism") | |
| # Check if user needs to set up profile | |
| user = db.get_user(Platform(platform), platform_user_id) | |
| if not user or not user.get("location_str"): | |
| return { | |
| "success": True, | |
| "response_text": "Welcome! To provide personalized suggestions, please share your location (e.g., 'Mumbai, Maharashtra' or a pincode).", | |
| "picks": [] | |
| } | |
| if not user.get("name"): | |
| return { | |
| "success": True, | |
| "response_text": "Great! Now, what's your name?", | |
| "picks": [] | |
| } | |
| if not user.get("age"): | |
| return { | |
| "success": True, | |
| "response_text": "Thanks! What's your age?", | |
| "picks": [] | |
| } | |
| # Try to extract category and budget | |
| category, budget = self._extract_category_budget(user_message) | |
| if not category: | |
| return { | |
| "success": True, | |
| "response_text": "I can help you find products! Please specify a category:\n- Mobile/Phone\n- Cycle/Bicycle\n- Books\n\nExample: 'I need a mobile under ₹5000'", | |
| "picks": [] | |
| } | |
| if not budget: | |
| return { | |
| "success": True, | |
| "response_text": f"What's your budget for {category}? (e.g., '₹5000' or '5000 rupees')", | |
| "picks": [] | |
| } | |
| # If we have all info, provide a generic response | |
| return { | |
| "success": True, | |
| "response_text": f"I'm currently processing your request for {category} under ₹{budget}. This might take a moment. If you don't see results, please try:\n\n1. Check your internet connection\n2. Try a different budget range\n3. Specify the category clearly\n\nExample: 'Show me mobiles under ₹6000'", | |
| "picks": [] | |
| } | |
| def _process_multimodal( | |
| self, | |
| image_data: Optional[bytes], | |
| audio_data: Optional[bytes], | |
| text_context: str | |
| ) -> str: | |
| """Use Gemini's multimodal capabilities to extract info from images/audio.""" | |
| try: | |
| from google import genai | |
| from google.genai import types | |
| client = genai.Client(api_key=settings.gemini_api_key) | |
| prompt = ( | |
| "You are a helpful assistant that extracts product search queries from any input type (text, voice, image).\n\n" | |
| "TASK: Convert the user's input into a clear product search query that can be used with Google Shopping.\n\n" | |
| "INSTRUCTIONS:\n" | |
| "- If this is VOICE: Transcribe the speech and extract the product request\n" | |
| "- If this is IMAGE: Analyze the image and extract product information\n" | |
| "- If this is TEXT: Use the text as-is\n" | |
| "- Return a SIMPLE, NATURAL search query like: 'mobile under 5000 rupees' or 'laptop under 40k'\n" | |
| "- Support ANY product type: mobiles, laptops, cycles, books, soap, clothes, etc.\n" | |
| "- Support ANY language: English, Hindi, or any other language\n" | |
| "- Include budget if mentioned, otherwise estimate based on context\n" | |
| "- Keep it concise but complete\n\n" | |
| f"User context: {text_context}\n\n" | |
| "IMPORTANT: Return ONLY the search query, nothing else. No explanations, no formatting." | |
| ) | |
| contents = [] | |
| # Add the text prompt | |
| contents.append(prompt) | |
| if image_data: | |
| contents.append( | |
| types.Part.from_bytes( | |
| data=image_data, | |
| mime_type='image/jpeg' | |
| ) | |
| ) | |
| if audio_data: | |
| # Telegram sends voice as OGG, but Gemini accepts various formats | |
| contents.append( | |
| types.Part.from_bytes( | |
| data=audio_data, | |
| mime_type='audio/ogg' # Telegram voice format | |
| ) | |
| ) | |
| response = client.models.generate_content( | |
| model='gemini-2.5-flash', | |
| contents=contents | |
| ) | |
| return response.text or "" | |
| except Exception as e: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.error(f"Multimodal processing failed: {e}") | |
| return f"Could not process media: {str(e)}" | |
| def _generate_search_query( | |
| self, | |
| enriched_message: str, | |
| image_data: Optional[bytes] = None, | |
| audio_data: Optional[bytes] = None | |
| ) -> str: | |
| """Use Gemini to generate a search query that supports all languages.""" | |
| try: | |
| from google import genai | |
| from google.genai import types | |
| client = genai.Client(api_key=settings.gemini_api_key) | |
| prompt = ( | |
| "You are a helpful assistant that converts user messages into English Google Shopping search queries.\n\n" | |
| "TASK: Convert any language user input into a clean, searchable English query for Google Shopping.\n\n" | |
| "INSTRUCTIONS:\n" | |
| "- Convert from ANY language (Hindi, Tamil, Telugu, etc.) to English\n" | |
| "- Create a natural search query like: 'mobile phone under 5000 rupees'\n" | |
| "- Include budget if mentioned (convert to rupees)\n" | |
| "- Support ANY product type: mobiles, laptops, cycles, books, clothing, appliances, etc.\n" | |
| "- Keep it concise but complete\n" | |
| "- Use common English search terms that work well with Google Shopping\n" | |
| "- Convert currency mentions to rupees (₹)\n\n" | |
| f"User message: {enriched_message}\n\n" | |
| "Return ONLY the search query in English, nothing else. No explanations." | |
| ) | |
| contents = [] | |
| # Add the text prompt | |
| contents.append(prompt) | |
| # Add image if provided | |
| if image_data: | |
| contents.append( | |
| types.Part.from_bytes( | |
| data=image_data, | |
| mime_type='image/jpeg' | |
| ) | |
| ) | |
| # Add audio if provided | |
| if audio_data: | |
| contents.append( | |
| types.Part.from_bytes( | |
| data=audio_data, | |
| mime_type='audio/ogg' | |
| ) | |
| ) | |
| response = client.models.generate_content( | |
| model='gemini-2.5-flash', | |
| contents=contents | |
| ) | |
| search_query = response.text.strip() if response.text else "" | |
| # Fallback to original message if Gemini fails to generate | |
| if not search_query or len(search_query) < 5: | |
| return enriched_message | |
| return search_query | |
| except Exception as e: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.error(f"Gemini search query generation failed: {e}") | |
| # Fallback to original message | |
| return enriched_message | |
| """Use Gemini to generate a search query that supports all languages.""" | |
| try: | |
| from google import genai | |
| from google.genai import types | |
| client = genai.Client(api_key=settings.gemini_api_key) | |
| prompt = ( | |
| "You are a helpful assistant that converts user messages into English Google Shopping search queries.\n\n" | |
| "TASK: Convert any language user input into a clean, searchable English query for Google Shopping.\n\n" | |
| "INSTRUCTIONS:\n" | |
| "- Convert from ANY language (Hindi, Tamil, Telugu, etc.) to English\n" | |
| "- Create a natural search query like: 'mobile phone under 5000 rupees'\n" | |
| "- Include budget if mentioned (convert to rupees)\n" | |
| "- Support ANY product type: mobiles, laptops, cycles, books, clothing, appliances, etc.\n" | |
| "- Keep it concise but complete\n" | |
| "- Use common English search terms that work well with Google Shopping\n" | |
| "- Convert currency mentions to rupees (₹)\n\n" | |
| f"User message: {enriched_message}\n\n" | |
| "Return ONLY the search query in English, nothing else. No explanations." | |
| ) | |
| contents = [prompt] | |
| # Add image if provided | |
| if image_data: | |
| contents.append( | |
| types.Part.from_bytes( | |
| data=image_data, | |
| mime_type='image/jpeg' | |
| ) | |
| ) | |
| # Add audio if provided | |
| if audio_data: | |
| contents.append( | |
| types.Part.from_bytes( | |
| data=audio_data, | |
| mime_type='audio/ogg' | |
| ) | |
| ) | |
| response = client.models.generate_content( | |
| model='gemini-2.5-flash', | |
| contents=contents | |
| ) | |
| search_query = response.text.strip() if response.text else "" | |
| # Fallback to original message if Gemini fails to generate | |
| if not search_query or len(search_query) < 5: | |
| return enriched_message | |
| return search_query | |
| except Exception as e: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.error(f"Gemini search query generation failed: {e}") | |
| # Fallback to original message | |
| return enriched_message | |
| def _extract_picks(self, response_text: str) -> List[Dict[str, str]]: | |
| """Extract the top 3 picks from response.""" | |
| pattern = r"Top Picks:\s*(.+?)(?:\n|$)" | |
| match = re.search(pattern, response_text, re.IGNORECASE) | |
| if match: | |
| picks_str = match.group(1) | |
| picks_list = [p.strip() for p in picks_str.split(',')] | |
| return [{"name": pick, "rank": i+1} for i, pick in enumerate(picks_list[:3])] | |
| return [] | |
| def _extract_category_budget(self, message: str) -> tuple: | |
| """Extract category and budget from message - supports ANY product type.""" | |
| message_lower = message.lower() | |
| # Try to extract ANY product type mentioned | |
| category = None | |
| # Common product keywords | |
| product_keywords = [ | |
| 'mobile', 'phone', 'smartphone', 'iphone', 'android', | |
| 'laptop', 'computer', 'pc', 'macbook', 'notebook', | |
| 'cycle', 'bicycle', 'bike', 'sycle', | |
| 'book', 'books', 'novel', 'textbook', | |
| 'tv', 'television', 'smart tv', | |
| 'watch', 'smartwatch', 'wristwatch', | |
| 'headphone', 'earphone', 'earbuds', 'airpods', | |
| 'speaker', 'bluetooth speaker', | |
| 'camera', 'dslr', 'webcam', | |
| 'tablet', 'ipad', | |
| 'shoe', 'shoes', 'footwear', 'sneaker', | |
| 'shirt', 'tshirt', 't-shirt', 'clothing', 'dress', | |
| 'bag', 'backpack', 'handbag', | |
| 'refrigerator', 'fridge', 'washing machine', 'ac', 'air conditioner', | |
| 'fan', 'cooler', | |
| 'table', 'chair', 'furniture', | |
| 'toy', 'toys', 'game' | |
| ] | |
| # Find first product keyword in message | |
| for keyword in product_keywords: | |
| if keyword in message_lower: | |
| category = keyword | |
| break | |
| # If no specific keyword found, try to extract noun after "need", "want", "show", "find" | |
| if not category: | |
| action_patterns = [ | |
| r'(?:need|want|looking for|show|find|search)\s+(?:a|an|some)?\s*([a-zA-Z]+)', | |
| r'([a-zA-Z]+)\s+under', | |
| ] | |
| for pattern in action_patterns: | |
| match = re.search(pattern, message_lower) | |
| if match: | |
| potential_category = match.group(1).strip() | |
| # Filter out common words | |
| if potential_category not in ['the', 'a', 'an', 'me', 'you', 'it', 'is', 'are', 'under', 'below']: | |
| category = potential_category | |
| break | |
| # Extract budget with support for "k" notation (e.g., "50k") | |
| budget = None | |
| budget_patterns = [ | |
| r'₹\s*(\d+)k', # ₹50k | |
| r'(\d+)k\s*(?:rupees|inr|rs)?', # 50k rupees | |
| r'(?:rs|₹)\s*(\d+)', # Rs 5000 or ₹5000 | |
| r'(?:under|below|max|maximum|budget|upto|up to)\s*[:\s]*(?:rs|₹)?\s*(\d+)k?', # under Rs 5000 | |
| r'(\d+)\s*(?:rupees|inr)', # 5000 rupees | |
| ] | |
| for pattern in budget_patterns: | |
| match = re.search(pattern, message_lower) | |
| if match: | |
| budget_str = match.group(1) | |
| # Check if pattern includes 'k' suffix | |
| if 'k' in pattern and 'k' in match.group(0): | |
| budget = int(budget_str) * 1000 | |
| else: | |
| budget = int(budget_str) | |
| break | |
| return category, budget | |
| agent = SuggestionAgent() | |