diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -650,18 +650,22 @@ context_manager = VeterinaryContextManager() # Enhanced product response with veterinary domain expertise def generate_veterinary_product_response(product_info: Dict[str, Any], user_context: Dict[str, Any]) -> str: """Generate comprehensive veterinary product response with intelligent information handling""" + def clean_text(text): if pd.isna(text) or text is None: return "Not specified" return str(text).strip() + # Extract product details product_name = clean_text(product_info.get('Product Name', '')) product_type = clean_text(product_info.get('Type', '')) category = clean_text(product_info.get('Category', '')) indications = clean_text(product_info.get('Indications', '')) + # Check for PDF link in the CSV data pdf_link = "" try: + # Load CSV data to check for PDF link csv_data = pd.read_csv('Veterinary.csv') product_row = csv_data[csv_data['Product Name'] == product_name] if not product_row.empty: @@ -670,15 +674,27 @@ def generate_veterinary_product_response(product_info: Dict[str, Any], user_cont pdf_link = brochure_link.strip() except Exception as e: logger.warning(f"Error checking PDF link for {product_name}: {e}") - response = f"""🧪 *Name:* {product_name}\n📦 *Type:* {product_type}\n🏥 *Category:* {category}\n💊 *Used For:* {indications}""" + + # Build the response + response = f"""🧪 *Name:* {product_name} +📦 *Type:* {product_type} +🏥 *Category:* {category} +💊 *Used For:* {indications}""" + + # Add PDF link if available, in the requested format if pdf_link: response += f"\n\n📄 Product Brochure Available\n🔗 {product_name} PDF:\n{pdf_link}" + + # Add menu options response += f""" -\n💬 *Available Actions:* + +💬 *Available Actions:* 1️⃣ Talk to Veterinary Consultant 2️⃣ Inquire About Availability 3️⃣ Back to Main Menu -\n💬 Select an option or ask about related products""" + +💬 Select an option or ask about related products""" + return response def clean_text_for_pdf(text: str) -> str: @@ -812,10 +828,8 @@ def generate_veterinary_pdf(product: Dict[str, Any]) -> bytes: async def send_catalog_pdf(phone_number: str): """Send the complete product catalog as a link to the PDF""" try: - # Use the static file URL for Hugging Face Spaces - server_url = os.getenv("SERVER_URL", "https://your-huggingface-space-url.hf.space") - catalog_url = f"{server_url}/static/Hydropex.pdf" - + # Use the correct Google Drive link converted to direct download format + catalog_url = "https://drive.google.com/uc?export=download&id=1mxpkFf3DY-n3QHzZBe_CdksR-gHu2f_0" message = ( "📋 *Apex Biotical Veterinary Products Catalog*\n\n" "📄 Here's your complete product catalog with all our veterinary products:\n" @@ -842,16 +856,16 @@ async def send_individual_product_pdf(phone_number: str, product: Dict[str, Any] filename = f"{safe_name}_{timestamp}.pdf" # Save PDF to uploads directory - uploads_dir = "uploads" + uploads_dir = "../uploads" os.makedirs(uploads_dir, exist_ok=True) pdf_path = os.path.join(uploads_dir, filename) with open(pdf_path, 'wb') as f: f.write(pdf_content) - # Generate download URL for Hugging Face Spaces - server_url = os.getenv("SERVER_URL", "https://your-huggingface-space-url.hf.space") - download_url = f"{server_url}/uploads/{filename}" + # Generate download URL + base_url = os.getenv("PUBLIC_BASE_URL", "http://localhost:8000") + download_url = f"{base_url}/uploads/{filename}" # Send PDF via WhatsApp media success = send_whatsjet_message( @@ -866,7 +880,7 @@ async def send_individual_product_pdf(phone_number: str, product: Dict[str, Any] if success: message = ( f"📄 *{product_name} - Product Information*\n\n" - f"📎 [Direct Download Link]({download_url})\n\n" + "📎 [Direct Download Link]({download_url})\n\n" "💬 *If the PDF didn't download, use the link above*\n" "Type 'main' to return to main menu." ) @@ -875,7 +889,7 @@ async def send_individual_product_pdf(phone_number: str, product: Dict[str, Any] # If media send failed, send only the link message = ( f"📄 *{product_name} - Product Information*\n\n" - f"📎 [Download Product PDF]({download_url})\n\n" + "📎 [Download Product PDF]({download_url})\n\n" "💬 *Click the link above to download the product information*\n" "Type 'main' to return to main menu." ) @@ -892,173 +906,98 @@ def split_message_for_whatsapp(message: str, max_length: int = 1000) -> list: return [message[i:i+max_length] for i in range(0, len(message), max_length)] def send_whatsjet_message(phone_number: str, message: str, media_type: str = None, media_path: str = None, filename: str = None) -> bool: - """Send a message using WhatsJet API with optional media attachment. For images, send the image as a media message (no caption), then send the caption as a separate text message.""" + """Send a message using WhatsJet API with optional media attachment""" if not all([WHATSJET_API_URL, WHATSJET_VENDOR_UID, WHATSJET_API_TOKEN]): logger.error("[WhatsJet] Missing environment variables.") return False url = f"{WHATSJET_API_URL}/{WHATSJET_VENDOR_UID}/contact/send-message?token={WHATSJET_API_TOKEN}" - # For images: use /contact/send-media-message endpoint with media_url, media_type, caption, and file_name - if media_type in ["image/jpeg", "image/png"] and media_path: + # Handle media messages + if media_type and media_path: try: - # Use direct public URL for WhatsJet (media_url) - if media_path.startswith("http://") or media_path.startswith("https://"): - image_url = media_path - else: - # If local file, upload to a public location or return False (not supported here) - logger.error("[WhatsJet] Local file paths are not supported for media_url. Please provide a public URL.") - return False - media_api_url = f"{WHATSJET_API_URL}/{WHATSJET_VENDOR_UID}/contact/send-media-message" + with open(media_path, 'rb') as f: + media_content = f.read() + media_b64 = base64.b64encode(media_content).decode('utf-8') payload = { - "phone_number": phone_number, - "media_type": "image", - "media_url": image_url, - "caption": message, - "file_name": filename or "image.jpg" + "phone_number": phone_number, + "message_body": message, + 'media_type': media_type, + 'media_content': media_b64, + 'media_filename': filename or os.path.basename(media_path) } - logger.info(f"[WhatsJet][DEBUG] Media API URL: {media_api_url}") - logger.info(f"[WhatsJet][DEBUG] Media payload: {payload}") - headers = {"Authorization": f"Bearer {WHATSJET_API_TOKEN}"} - response = httpx.post( - media_api_url, - json=payload, - headers=headers, - timeout=30 - ) - logger.info(f"[WhatsJet][DEBUG] Media response status: {response.status_code}") - logger.info(f"[WhatsJet][DEBUG] Media response headers: {dict(response.headers)}") - logger.info(f"[WhatsJet][DEBUG] Media response body: {response.text}") - try: - resp_json = response.json() - if resp_json.get('result') == 'success': - logger.info(f"[WhatsJet] Media image sent successfully via media_url to {phone_number}") - return True - else: - logger.error(f"[WhatsJet][ERROR] Media send failed: {resp_json.get('message', 'Unknown error')}") - return False - except Exception as e: - logger.error(f"[WhatsJet][ERROR] Could not parse WhatsJet response JSON: {e}") - return False - except Exception as e: - import traceback - logger.error(f"[WhatsJet][ERROR] Exception sending image via media_url: {str(e)}\nTraceback: {traceback.format_exc()}") - # Fallback: send text only - return send_whatsjet_message(phone_number, message) - - # Handle other media messages (existing logic) - if media_type and media_path: - try: - if isinstance(media_path, str) and (media_path.startswith("http://") or media_path.startswith("https://")): - response = requests.get(media_path, stream=True, timeout=15) - response.raise_for_status() - media_content = response.content - logger.info(f"[WhatsJet][DEBUG] Downloaded media content-type: {response.headers.get('Content-Type')}") - logger.info(f"[WhatsJet][DEBUG] First 20 bytes: {media_content[:20]}") - else: - with open(media_path, 'rb') as f: - media_content = f.read() - # Try multipart first, then base64 + # Send message with increased timeout try: - files = { - 'media': (filename or 'file.bin', media_content, media_type) - } - data = { - 'phone_number': phone_number, - 'message_body': message - } - # Enhanced logging - logger.info(f"[WhatsJet][DEBUG] URL: {url}") - logger.info(f"[WhatsJet][DEBUG] Multipart data: {data}") - logger.info(f"[WhatsJet][DEBUG] Multipart files: {list(files.keys())}") - headers = {"Authorization": f"Bearer {WHATSJET_API_TOKEN}"} - response = httpx.post( - url, - data=data, - files=files, - headers=headers, - timeout=30 - ) - logger.info(f"[WhatsJet][DEBUG] Multipart response status: {response.status_code}") - logger.info(f"[WhatsJet][DEBUG] Multipart response headers: {dict(response.headers)}") - try: - response_text = response.text - logger.info(f"[WhatsJet][DEBUG] Multipart response body: {response_text[:1000]}" + ("..." if len(response_text) > 1000 else "")) - except Exception as e: - logger.info(f"[WhatsJet][DEBUG] Multipart response body: Unable to read: {e}") - response.raise_for_status() - logger.info(f"[WhatsJet] Media message sent successfully via multipart to {phone_number}") - return True - except Exception as e: - import traceback - logger.warning(f"[WhatsJet][ERROR] Multipart upload failed for media, trying base64: {e}\nTraceback: {traceback.format_exc()}") - - media_b64 = base64.b64encode(media_content).decode('utf-8') - payload = { - "phone_number": phone_number, - "message_body": message, - 'media_type': media_type, - 'media_content': media_b64, - 'media_filename': filename or os.path.basename(media_path) if not media_path.startswith('http') else filename or 'file.bin' - } - # Enhanced logging - logger.info(f"[WhatsJet][DEBUG] URL: {url}") - logger.info(f"[WhatsJet][DEBUG] Base64 payload: {{'phone_number': payload['phone_number'], 'media_type': payload['media_type'], 'media_filename': payload['media_filename'], 'message_body': payload['message_body'][:50] + '...', 'media_content_length': len(payload['media_content'])}}") - headers = {"Authorization": f"Bearer {WHATSJET_API_TOKEN}"} response = httpx.post( url, json=payload, - headers=headers, - timeout=30 + timeout=15 ) - logger.info(f"[WhatsJet][DEBUG] Base64 response status: {response.status_code}") - logger.info(f"[WhatsJet][DEBUG] Base64 response headers: {dict(response.headers)}") - try: - response_text = response.text - logger.info(f"[WhatsJet][DEBUG] Base64 response body: {response_text[:1000]}" + ("..." if len(response_text) > 1000 else "")) - except Exception as e: - logger.info(f"[WhatsJet][DEBUG] Base64 response body: Unable to read: {e}") response.raise_for_status() - logger.info(f"[WhatsJet] Media message sent successfully via base64 to {phone_number}") + logger.info(f"[WhatsJet] Media message sent successfully to {phone_number}") return True except Exception as e: - import traceback - logger.error(f"[WhatsJet][ERROR] Exception sending media message: {e}\nTraceback: {traceback.format_exc()}") + logger.error(f"[WhatsJet] Exception sending media message: {e}") return False except Exception as e: - import traceback - logger.error(f"[WhatsJet][ERROR] Exception preparing media message: {str(e)}\nTraceback: {traceback.format_exc()}") + logger.error(f"[WhatsJet] Exception preparing media message: {str(e)}") return False - # Handle text messages (existing logic) + # Handle text messages if not message.strip(): return True # Don't send empty messages for chunk in split_message_for_whatsapp(message): try: payload = {"phone_number": phone_number, "message_body": chunk} - # Enhanced logging - logger.info(f"[WhatsJet][DEBUG] URL: {url}") - logger.info(f"[WhatsJet][DEBUG] Payload: {json.dumps(payload)}") - headers = {"Authorization": f"Bearer {WHATSJET_API_TOKEN}"} - response = httpx.post( - url, - json=payload, - headers=headers, - timeout=15 - ) - logger.info(f"[WhatsJet][DEBUG] Response status: {response.status_code}") - logger.info(f"[WhatsJet][DEBUG] Response headers: {dict(response.headers)}") - logger.info(f"[WhatsJet][DEBUG] Response body: {response.text}") - response.raise_for_status() - logger.info(f"[WhatsJet] Text chunk sent successfully to {phone_number}") + # Send message with increased timeout + try: + response = httpx.post( + url, + json=payload, + timeout=15 + ) + response.raise_for_status() + logger.info(f"[WhatsJet] Text chunk sent successfully to {phone_number}") + except Exception as e: + logger.error(f"[WhatsJet] Exception sending text chunk: {e}") + return False except Exception as e: - import traceback - logger.error(f"[WhatsJet][ERROR] Exception sending text chunk: {e}\nTraceback: {traceback.format_exc()}") + logger.error(f"[WhatsJet] Exception preparing text chunk: {str(e)}") return False + logger.info(f"[WhatsJet] Successfully sent complete text message to {phone_number}") return True +def send_whatsjet_media_image_only(phone_number: str, image_url: str, filename: str = None) -> bool: + """Send an image only (no caption) using WhatsJet's /contact/send-media-message endpoint.""" + if not all([WHATSJET_API_URL, WHATSJET_VENDOR_UID, WHATSJET_API_TOKEN]): + logger.error("[WhatsJet] Missing environment variables for media message.") + return False + url = f"{WHATSJET_API_URL}/{WHATSJET_VENDOR_UID}/contact/send-media-message" + headers = { + "Authorization": f"Bearer {WHATSJET_API_TOKEN}", + "Content-Type": "application/json" + } + payload = { + "phone_number": phone_number, + "media_type": "image", + "media_url": image_url + } + if filename: + payload["file_name"] = filename + try: + logger.info(f"[WhatsJet] Sending image only: {payload}") + response = httpx.post(url, json=payload, headers=headers, timeout=30) + logger.info(f"[WhatsJet] Image only response status: {response.status_code}") + logger.info(f"[WhatsJet] Image only response body: {response.text[:500]}...") + response.raise_for_status() + logger.info(f"[WhatsJet] Image only sent successfully to {phone_number}") + return True + except Exception as e: + logger.error(f"[WhatsJet] Exception sending image only: {e}") + return False + # --- Health Check Endpoint --- @app.get("/health") async def health_check(): @@ -1558,7 +1497,7 @@ async def process_incoming_message(from_number: str, msg: dict): return else: # Only trigger menu mode if not in AI chat mode - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -1574,7 +1513,7 @@ async def process_incoming_message(from_number: str, msg: dict): mapped_navigation = process_intelligent_voice_command(message_body, current_state, user_context) if mapped_navigation == 'main': logger.info(f"[Process] Navigation command detected: '{message_body}' -> 'main'") - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -1586,7 +1525,7 @@ async def process_incoming_message(from_number: str, msg: dict): # Also check for text-based main commands if message_body.lower() in ['main', 'menu', 'start', 'home', 'back']: - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -1596,6 +1535,156 @@ async def process_incoming_message(from_number: str, msg: dict): ) return + # 🎯 PRIORITY: Check for product names FIRST - works from ANY menu state + # This ensures users can say product names like "hydropex", "respira aid plus", etc. from any menu + logger.info(f"[Process] Checking for product name in message: '{message_body}' from state: {current_state}") + products = get_veterinary_product_matches(message_body) + + if products: + logger.info(f"[Process] Product name detected: '{message_body}' -> Found {len(products)} products") + + # If single product found, show it directly + if len(products) == 1: + selected_product = products[0] + product_name = selected_product.get('Product Name', 'Unknown') + logger.info(f"[Process] Single product found: {product_name}") + + # Set current product and show details + context_manager.update_context( + from_number, + current_product=selected_product, + current_state='product_inquiry', + current_menu='product_inquiry', + current_menu_options=list(MENU_CONFIG['product_inquiry']['option_descriptions'].values()) + ) + + # Generate and send product response + response = generate_veterinary_product_response(selected_product, user_context) + send_whatsjet_message(from_number, response) + return + + # If multiple products found, show list for selection + elif len(products) > 1: + logger.info(f"[Process] Multiple products found: {len(products)} products") + + # Use OpenAI to generate a professional summary and list all products + if OPENAI_API_KEY: + try: + # Create a comprehensive prompt for multiple products + products_info = [] + for i, product in enumerate(products, 1): + product_name = product.get('Product Name', 'N/A') + category = product.get('Category', 'N/A') + target_species = product.get('Target Species', 'N/A') + products_info.append(f"{i}. {product_name} - {category} ({target_species})") + + products_text = "\n".join(products_info) + + prompt = f""" +You are a professional veterinary product assistant for Apex Biotical. The user asked about "{message_body}" and we found {len(products)} relevant products. + +Available Products: +{products_text} + +Please provide: +1. A professional, welcoming response acknowledging their query +2. A brief summary of what these products are for (if it's a category like "poultry products", explain the category) +3. List all products with their numbers and brief descriptions +4. Clear instructions on how to proceed + +Format your response professionally with emojis and clear structure. Keep it concise but informative. +""" + + response = openai.ChatCompletion.create( + model="gpt-4o", + messages=[{"role": "user", "content": prompt}], + temperature=0.7, + max_tokens=400 + ) + + ai_response = response.choices[0].message.content.strip() + + # Add instructions for selection + selection_instructions = ( + f"\n\n💬 *To view detailed information about any product, reply with its number (1-{len(products)})*\n" + "💬 *Type 'main' to return to the main menu*" + ) + + full_response = ai_response + selection_instructions + + # Translate response if needed + if reply_language == 'ur': + try: + translated_response = GoogleTranslator(source='auto', target='ur').translate(full_response) + send_whatsjet_message(from_number, translated_response) + except Exception as e: + logger.error(f"[AI] Translation error: {e}") + send_whatsjet_message(from_number, full_response) + else: + send_whatsjet_message(from_number, full_response) + + # Store the product list in context for selection handling + context_manager.update_context( + from_number, + current_state='intelligent_products_menu', + current_menu='intelligent_products_menu', + current_menu_options=[f"Product {i+1}" for i in range(len(products))], + available_products=products, + last_query=message_body + ) + + # Add to conversation history + context_manager.add_to_history(from_number, message_body, full_response) + return + + except Exception as e: + logger.error(f"[AI] Error generating product summary: {e}") + # Fall back to simple listing if AI fails + pass + + # Fallback: Simple listing without AI + message = f"🔍 *Found {len(products)} products matching '{message_body}':*\n\n" + + for i, product in enumerate(products, 1): + product_name = product.get('Product Name', 'N/A') + category = product.get('Category', 'N/A') + target_species = product.get('Target Species', 'N/A') + message += f"{format_number_with_emoji(i)} {product_name}\n" + message += f" 📦 {category} ({target_species})\n\n" + + message += ( + f"💬 *To view detailed information about any product, reply with its number (1-{len(products)})*\n" + "💬 *Type 'main' to return to the main menu*" + ) + + # Translate response if needed + if reply_language == 'ur': + try: + translated_message = GoogleTranslator(source='auto', target='ur').translate(message) + send_whatsjet_message(from_number, translated_message) + except Exception as e: + logger.error(f"[AI] Translation error: {e}") + send_whatsjet_message(from_number, message) + else: + send_whatsjet_message(from_number, message) + + # Store the product list in context for selection handling + context_manager.update_context( + from_number, + current_state='intelligent_products_menu', + current_menu='intelligent_products_menu', + current_menu_options=[f"Product {i+1}" for i in range(len(products))], + available_products=products, + last_query=message_body + ) + + # Add to conversation history + context_manager.add_to_history(from_number, message_body, message) + return + + # If no product found, continue with normal menu processing + logger.info(f"[Process] No product name detected, continuing with menu processing") + # Handle state-specific menu selections with intelligent voice command processing # Handle product follow-up menu selections (must be first) if current_state == 'product_inquiry': @@ -1606,7 +1695,7 @@ async def process_incoming_message(from_number: str, msg: dict): # Check for main navigation first if mapped_selection == 'main': logger.info(f"[Process] Main navigation from product_inquiry: '{message_body}' -> 'main'") - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -1645,7 +1734,7 @@ async def process_incoming_message(from_number: str, msg: dict): # Check for main navigation first if mapped_selection == 'main': logger.info(f"[Process] Main navigation from category_products_menu: '{message_body}' -> 'main'") - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -1678,7 +1767,7 @@ async def process_incoming_message(from_number: str, msg: dict): # Check for main navigation first if mapped_selection == 'main': logger.info(f"[Process] Main navigation from all_products_menu: '{message_body}' -> 'main'") - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -1709,7 +1798,7 @@ async def process_incoming_message(from_number: str, msg: dict): # Check for main navigation first if mapped_selection == 'main': logger.info(f"[Process] Main navigation from category_selection_menu: '{message_body}' -> 'main'") - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -1950,7 +2039,7 @@ async def handle_voice_message_complete(from_number: str, msg: dict): return else: # Only trigger menu mode if not in AI chat mode - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context(from_number, current_state='main_menu', current_menu='main_menu', current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values())) return @@ -2064,13 +2153,13 @@ async def handle_veterinary_menu_selection_complete(selection: str, from_number: await handle_all_products_selection(from_number, selection, user_context) else: - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context(from_number, current_state='main_menu', current_menu='main_menu', current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values())) except Exception as e: logger.error(f"[Menu] Error handling menu selection: {e}") - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context(from_number, current_state='main_menu', current_menu='main_menu', current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values())) @@ -2118,8 +2207,8 @@ async def handle_all_products_selection(from_number: str, selection: str, user_c current_menu='product_inquiry', current_menu_options=list(MENU_CONFIG['product_inquiry']['option_descriptions'].values()) ) - # Send image with caption (NEW) - await send_product_image_with_caption(from_number, selected_product, user_context) + response = generate_veterinary_product_response(selected_product, user_context) + send_whatsjet_message(from_number, response) else: send_whatsjet_message(from_number, "❌ Invalid selection. Please choose a valid product number.") except Exception as e: @@ -2127,7 +2216,7 @@ async def handle_all_products_selection(from_number: str, selection: str, user_c send_helpful_guidance(from_number, 'all_products_menu') async def handle_intelligent_product_inquiry(from_number: str, query: str, user_context: dict, reply_language: str = 'en'): - """Handle product inquiry with OpenAI intelligence and media support""" + """Handle product inquiry with OpenAI intelligence""" try: # First try direct product search products = get_veterinary_product_matches(query) @@ -2170,7 +2259,7 @@ Format your response professionally with emojis and clear structure. Keep it con max_tokens=400 ) - ai_response = response.choices[0].message['content'].strip() + ai_response = response.choices[0].message.content.strip() # Add instructions for selection selection_instructions = ( @@ -2250,7 +2339,7 @@ Format your response professionally with emojis and clear structure. Keep it con context_manager.add_to_history(from_number, query, message) else: - # Single product found - show detailed information with media support + # Single product found - show detailed information as before selected_product = products[0] context_manager.update_context( from_number, @@ -2262,15 +2351,21 @@ Format your response professionally with emojis and clear structure. Keep it con # Get updated context with last message context = context_manager.get_context(from_number) + response = generate_veterinary_product_response(selected_product, context) - # Use enhanced response with media support - response_with_media = generate_veterinary_product_response_with_media(selected_product, context) - - # Send the response - send_whatsjet_message(from_number, response_with_media['text']) + # Translate response if needed + if reply_language == 'ur': + try: + translated_response = GoogleTranslator(source='auto', target='ur').translate(response) + send_whatsjet_message(from_number, translated_response) + except Exception as e: + logger.error(f"[AI] Translation error: {e}") + send_whatsjet_message(from_number, response) + else: + send_whatsjet_message(from_number, response) # Add to conversation history - context_manager.add_to_history(from_number, query, response_with_media['text']) + context_manager.add_to_history(from_number, query, response) else: # Enhanced "not found" response with veterinary suggestions @@ -2305,7 +2400,7 @@ Format your response professionally with emojis and clear structure. Keep it con except Exception as e: logger.error(f"Error in product inquiry: {e}") # Instead of sending a generic error, return to main menu - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context(from_number, current_state='main_menu', current_menu='main_menu', current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values())) @@ -2347,7 +2442,7 @@ Always keep responses professional, concise, and user-friendly. max_tokens=300 ) - ai_response = response.choices[0].message['content'].strip() + ai_response = response.choices[0].message.content.strip() # Translate response if needed if reply_language == 'ur': @@ -2366,7 +2461,7 @@ Always keep responses professional, concise, and user-friendly. except Exception as e: logger.error(f"[AI] Error handling general query: {e}") # Instead of sending a generic error, return to main menu - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -2398,7 +2493,7 @@ async def handle_contact_request(from_number: str): except Exception as e: logger.error(f"[Contact] Error handling contact request: {e}") # Instead of sending a generic error, return to main menu - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -2422,7 +2517,7 @@ async def handle_contact_request_response(from_number: str, response: str): f.write(json.dumps(contact_data, ensure_ascii=False) + '\n') # Send inquiry to receiving number (admin) - receiving_number = "923102288328" + receiving_number = "923068222219" # Parse the response to separate name/location from details response_lines = response.strip().split('\n') @@ -2454,9 +2549,8 @@ async def handle_contact_request_response(from_number: str, response: str): ) except Exception as e: logger.error(f"[Contact] Error handling contact response: {e}") - # Get user context before using it - user_context = context_manager.get_context(from_number) - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + # Instead of sending a generic error, return to main menu + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -2495,7 +2589,7 @@ async def handle_availability_inquiry(from_number: str, user_context: dict): except Exception as e: logger.error(f"[Availability] Error handling availability inquiry: {e}") # Instead of sending a generic error, return to main menu - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -2519,7 +2613,7 @@ async def handle_availability_request_response(from_number: str, response: str): f.write(json.dumps(availability_data, ensure_ascii=False) + '\n') # Send inquiry to receiving number (admin) - receiving_number = "923102288328" + receiving_number = "923068222219" current_product = context_manager.get_context(from_number).get('current_product', {}) product_name = current_product.get('Product Name', 'N/A') if current_product else 'N/A' @@ -2559,9 +2653,8 @@ async def handle_availability_request_response(from_number: str, response: str): ) except Exception as e: logger.error(f"[Availability] Error handling availability response: {e}") - # Get user context before using it - user_context = context_manager.get_context(from_number) - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + # Instead of sending a generic error, return to main menu + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -2624,9 +2717,7 @@ def send_helpful_guidance(from_number: str, current_state: str): ) except Exception as e: logger.error(f"Error sending helpful guidance: {e}") - # Get user context before using it - user_context = context_manager.get_context(from_number) - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -2778,13 +2869,10 @@ async def handle_ai_chat_mode(from_number: str, query: str, reply_language: str try: logger.info(f"[AI Chat] Processing query: '{query}' for {from_number} in {reply_language}") - # Get user context - user_context = context_manager.get_context(from_number) - # Check for navigation commands first if query.lower().strip() in ['main', 'menu', 'start', 'home', 'back']: logger.info(f"[AI Chat] Navigation command detected: '{query}' -> returning to main menu") - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -2797,7 +2885,7 @@ async def handle_ai_chat_mode(from_number: str, query: str, reply_language: str # Check for greetings - return to main menu if is_greeting(query): logger.info(f"[AI Chat] Greeting detected: '{query}' -> returning to main menu") - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + welcome_msg = generate_veterinary_welcome_message() send_whatsjet_message(from_number, welcome_msg) context_manager.update_context( from_number, @@ -2817,15 +2905,13 @@ async def handle_ai_chat_mode(from_number: str, query: str, reply_language: str # Get all products data for context all_products = [] - total_products = 0 if products_df is not None and not products_df.empty: all_products = products_df.to_dict('records') - total_products = len(products_df) # Create comprehensive context for AI products_context = "" if all_products: - products_context = f"Available Veterinary Products (Total: {total_products} products):\n" + products_context = "Available Veterinary Products:\n" for i, product in enumerate(all_products[:50], 1): # Limit to first 50 products for context product_name = product.get('Product Name', 'N/A') category = product.get('Category', 'N/A') @@ -2835,15 +2921,13 @@ async def handle_ai_chat_mode(from_number: str, query: str, reply_language: str products_context += f" Composition: {composition}\n" products_context += f" Target Species: {target_species}\n\n" - # Create AI prompt with accurate product count + # Create AI prompt if reply_language == 'ur': prompt = f""" آپ Apex Biotical کے Veterinary AI Assistant ہیں۔ آپ کو veterinary products اور treatments کے بارے میں معلومات فراہم کرنی ہیں۔ یوزر کا سوال: {query} -کل veterinary products کی تعداد: {total_products} - دستیاب veterinary products: {products_context} @@ -2853,7 +2937,6 @@ async def handle_ai_chat_mode(from_number: str, query: str, reply_language: str 3. اگر یہ general veterinary advice ہے تو professional guidance دیں 4. اردو میں جواب دیں 5. جواب professional اور helpful ہو -6. اگر یوزر نے products کی تعداد کے بارے میں پوچھا ہے تو صحیح تعداد ({total_products}) بتائیں جواب: """ @@ -2863,8 +2946,6 @@ You are Apex Biotical's Veterinary AI Assistant. You provide information about v User Query: {query} -Total number of veterinary products: {total_products} - Available Veterinary Products: {products_context} @@ -2874,7 +2955,6 @@ Please: 3. If it's general veterinary advice, provide professional guidance 4. Answer in English 5. Keep the response professional and helpful -6. If the user asks about the number of products, provide the accurate count ({total_products}) Response: """ @@ -2887,7 +2967,7 @@ Response: max_tokens=500 ) - ai_response = response.choices[0].message['content'].strip() + ai_response = response.choices[0].message.content.strip() # Add instructions for returning to main menu if reply_language == 'ur': @@ -2921,591 +3001,42 @@ Response: error_msg = "❌ AI Assistant encountered an error. Please try again or type 'main' to return to main menu." send_whatsjet_message(from_number, error_msg) -def generate_veterinary_welcome_message(phone_number=None, user_context=None): - """Generate enhanced welcome message with veterinary domain expertise""" - welcome_msg = ( - "🏥 *Welcome to Apex Biotical Veterinary Bot*\n\n" - "We provide comprehensive veterinary products and support.\n\n" - "📋 *Available Options:*\n" - "1️⃣ Search Veterinary Products\n" - "2️⃣ Browse Categories\n" - "3️⃣ Download Catalog\n" - "4️⃣ Chat with Veterinary AI Assistant\n\n" - "💬 *Select an option or ask about specific products*\n" - "🎤 *You can also send voice messages*" - ) - return welcome_msg +# Load products on startup +load_products_data() -@app.get("/test-whatsjet") -async def test_whatsjet(): - try: - resp = requests.get("https://api.whatsjet.com", timeout=5) - return {"status": resp.status_code, "text": resp.text[:200]} - except Exception as e: - return {"error": str(e)} +# Add these functions after the existing imports and before the main functions -# Load products data on startup -def load_products_data(): - """Load products data from CSV file""" - global products_df +def get_product_image_path(product_name: str) -> str: + """ + Get the image path for a product + Returns the path to the product image if it exists, otherwise None + """ try: - if os.path.exists(CSV_FILE): - products_df = pd.read_csv(CSV_FILE) - logger.info(f"✅ Loaded {len(products_df)} products from {CSV_FILE}") - else: - logger.warning(f"⚠️ CSV file {CSV_FILE} not found") - products_df = pd.DataFrame() - except Exception as e: - logger.error(f"❌ Error loading products data: {e}") - products_df = pd.DataFrame() - -def convert_drive_link(link: str) -> str: - """Convert Google Drive link to direct download link""" - if 'drive.google.com' in link: - file_id = link.split('/')[-2] if '/d/' in link else link.split('/')[-1] - return f"https://drive.google.com/uc?export=download&id={file_id}" - return link - -def format_number_with_emoji(number: int) -> str: - """Format number with emoji""" - emoji_map = { - 1: "1️⃣", 2: "2️⃣", 3: "3️⃣", 4: "4️⃣", 5: "5️⃣", - 6: "6️⃣", 7: "7️⃣", 8: "8️⃣", 9: "9️⃣", 10: "🔟", - 11: "1️⃣1️⃣", 12: "1️⃣2️⃣", 13: "1️⃣3️⃣", 14: "1️⃣4️⃣", 15: "1️⃣5️⃣", - 16: "1️⃣6️⃣", 17: "1️⃣7️⃣", 18: "1️⃣8️⃣", 19: "1️⃣9️⃣", 20: "2️⃣0️⃣", - 21: "2️⃣1️⃣", 22: "2️⃣2️⃣", 23: "2️⃣3️⃣" - } - return emoji_map.get(number, f"{number}.") - -async def display_all_products(from_number: str): - """Display all products in multiple messages and update menu context""" - try: - user_context = context_manager.get_context(from_number) - current_state = user_context.get('current_state', 'main_menu') - logger.info(f"[Display] display_all_products called for {from_number} in state: {current_state}") - if current_state == 'all_products_menu': - logger.warning(f"[Display] Already in all_products_menu state for {from_number}, skipping display") - return - if products_df is None or products_df.empty: - send_whatsjet_message(from_number, "❌ No products available at the moment.") - return - # Set state to all_products_menu and store menu context - products = products_df.to_dict('records') - context_manager.update_context( - from_number, - current_state='all_products_menu', - current_menu='all_products_menu', - current_menu_options=[p.get('Product Name', 'Unknown') for p in products], - available_products=products - ) - logger.info(f"[Display] Set state to all_products_menu for {from_number}") - # Send products in chunks - chunk_size = 5 - for i in range(0, len(products), chunk_size): - chunk = products[i:i + chunk_size] - message = f"📋 *Products ({i+1}-{min(i+chunk_size, len(products))} of {len(products)})*\n\n" - for j, product in enumerate(chunk, i+1): - message += f"{format_number_with_emoji(j)} {product.get('Product Name', 'Unknown')}\n" - if product.get('Category'): - message += f" Category: {product.get('Category')}\n" - message += "\n" - send_whatsjet_message(from_number, message) - send_whatsjet_message(from_number, - "💬 Type a product name to get detailed information, or type 'main' to return to main menu.") - except Exception as e: - logger.error(f"[Display] Error displaying products: {e}") - send_whatsjet_message(from_number, "❌ Error displaying products. Please try again.") - -def get_all_categories(): - """Return a list of all unique categories from the products DataFrame""" - if products_df is not None and not products_df.empty: - return list(products_df['Category'].unique()) - return [] - -def get_products_by_category(category: str): - """Get products by category""" - if products_df is None or products_df.empty: - return [] - category_products = products_df[products_df['Category'] == category] - return category_products.to_dict('records') - -# Enhanced product follow-up handling -async def handle_veterinary_product_followup(selection: str, from_number: str) -> None: - """ - Handle product follow-up selections with enhanced veterinary domain support - """ - try: - user_context = context_manager.get_context(from_number) - current_product = user_context.get('current_product') - - if not current_product: - send_whatsjet_message(from_number, "❌ No product selected. Please search for a product first.") - return - - if selection == '1': - # Talk to Veterinary Consultant - product_name = current_product.get('Product Name', 'the selected product') - consultant_msg = ( - f"📞 Contact Veterinary Consultant\n\n" - f"Product: {product_name}\n\n" - "Please provide your details:\n" - "* Name and location\n" - "* Specific inquiry\n\n" - "💬 Example: Dr. Ali - Multan - Need consultation for respiratory problems\n\n" - "Type main at any time to go to main menu." - ) - send_whatsjet_message(from_number, consultant_msg) - context_manager.update_context( - from_number, - current_state='contact_request', - current_menu='contact_request', - current_menu_options=['Provide contact details'] - ) - - elif selection == '2': - # Inquire about Product Availability - await handle_availability_inquiry(from_number, user_context) - - elif selection == '3': - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) - send_whatsjet_message(from_number, welcome_msg) - context_manager.update_context( - from_number, - current_state='main_menu', - current_menu='main_menu', - current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values()) - ) - return - else: - send_whatsjet_message(from_number, "❌ Invalid selection. Please choose 1, 2, or 3.") - return - except Exception as e: - logger.error(f"Error in product follow-up: {e}") - user_context = context_manager.get_context(from_number) - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) - send_whatsjet_message(from_number, welcome_msg) - context_manager.update_context( - from_number, - current_state='main_menu', - current_menu='main_menu', - current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values()) - ) - -# Enhanced product inquiry handling -async def handle_veterinary_product_inquiry(product_name: str, from_number: str) -> None: - """ - Handle product inquiry with enhanced veterinary domain support and media - """ - try: - # Search for the product - products = get_veterinary_product_matches(product_name) - - if products: - selected_product = products[0] - context_manager.update_context( - from_number, - current_product=selected_product, - current_state='product_inquiry', - current_menu='product_inquiry', - current_menu_options=list(MENU_CONFIG['product_inquiry']['option_descriptions'].values()) - ) - - # Get updated context with last message - context = context_manager.get_context(from_number) - - # Use enhanced response with media support - response_with_media = generate_veterinary_product_response_with_media(selected_product, context) - - # Send the response - send_whatsjet_message(from_number, response_with_media['text']) - - # Add to conversation history - context_manager.add_to_history(from_number, product_name, response_with_media['text']) - - else: - # Enhanced "not found" response with veterinary suggestions - message = ( - "❌ *Product Not Found*\n\n" - f"🔍 *We couldn't find '{product_name}' in our veterinary database.*\n\n" - "💡 *Try these alternatives:*\n" - "• Check spelling (e.g., 'Hydropex' not 'Hydro pex')\n" - "• Search by symptoms (e.g., 'respiratory', 'liver support')\n" - "• Search by category (e.g., 'antibiotic', 'vitamin')\n" - "• Search by species (e.g., 'poultry', 'livestock')\n\n" - "🏥 *Popular Veterinary Products:*\n" - "• Hydropex (Electrolyte supplement)\n" - "• Heposel (Liver tonic)\n" - "• Bromacid (Respiratory support)\n" - "• Tribiotic (Antibiotic)\n" - "• Symodex (Multivitamin)\n\n" - "💬 *Type 'main' to return to main menu or try another search.*" - ) - - send_whatsjet_message(from_number, message) - - except Exception as e: - logger.error(f"Error in product inquiry: {e}") - # Get user context before using it - user_context = context_manager.get_context(from_number) - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) - send_whatsjet_message(from_number, welcome_msg) - context_manager.update_context(from_number, current_state='main_menu', current_menu='main_menu', current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values())) - -async def handle_intelligent_product_inquiry(from_number: str, query: str, user_context: dict, reply_language: str = 'en'): - """Handle product inquiry with OpenAI intelligence and media support""" - try: - # First try direct product search - products = get_veterinary_product_matches(query) - - if products: - # Check if this is a broad/category query (multiple products found) - if len(products) > 1: - # Use OpenAI to generate a professional summary and list all products - if OPENAI_API_KEY: - try: - # Create a comprehensive prompt for multiple products - products_info = [] - for i, product in enumerate(products, 1): - product_name = product.get('Product Name', 'N/A') - category = product.get('Category', 'N/A') - target_species = product.get('Target Species', 'N/A') - products_info.append(f"{i}. {product_name} - {category} ({target_species})") - - products_text = "\n".join(products_info) - - prompt = f""" -You are a professional veterinary product assistant for Apex Biotical. The user asked about "{query}" and we found {len(products)} relevant products. - -Available Products: -{products_text} - -Please provide: -1. A professional, welcoming response acknowledging their query -2. A brief summary of what these products are for (if it's a category like "poultry products", explain the category) -3. List all products with their numbers and brief descriptions -4. Clear instructions on how to proceed - -Format your response professionally with emojis and clear structure. Keep it concise but informative. -""" - - response = openai.ChatCompletion.create( - model="gpt-4o", - messages=[{"role": "user", "content": prompt}], - temperature=0.7, - max_tokens=400 - ) - - ai_response = response.choices[0].message['content'].strip() - - # Add instructions for selection - selection_instructions = ( - f"\n\n💬 *To view detailed information about any product, reply with its number (1-{len(products)})*\n" - "💬 *Type 'main' to return to the main menu*" - ) - - full_response = ai_response + selection_instructions - - # Translate response if needed - if reply_language == 'ur': - try: - translated_response = GoogleTranslator(source='auto', target='ur').translate(full_response) - send_whatsjet_message(from_number, translated_response) - except Exception as e: - logger.error(f"[AI] Translation error: {e}") - send_whatsjet_message(from_number, full_response) - else: - send_whatsjet_message(from_number, full_response) - - # Store the product list in context for selection handling - context_manager.update_context( - from_number, - current_state='intelligent_products_menu', - current_menu='intelligent_products_menu', - current_menu_options=[f"Product {i+1}" for i in range(len(products))], - available_products=products, - last_query=query - ) - - # Add to conversation history - context_manager.add_to_history(from_number, query, full_response) - return - - except Exception as e: - logger.error(f"[AI] Error generating product summary: {e}") - # Fall back to simple listing if AI fails - pass - - # Fallback: Simple listing without AI - message = f"🔍 *Found {len(products)} products matching '{query}':*\n\n" - - for i, product in enumerate(products, 1): - product_name = product.get('Product Name', 'N/A') - category = product.get('Category', 'N/A') - target_species = product.get('Target Species', 'N/A') - message += f"{format_number_with_emoji(i)} {product_name}\n" - message += f" 📦 {category} ({target_species})\n\n" - - message += ( - f"💬 *To view detailed information about any product, reply with its number (1-{len(products)})*\n" - "💬 *Type 'main' to return to the main menu*" - ) - - # Translate response if needed - if reply_language == 'ur': - try: - translated_message = GoogleTranslator(source='auto', target='ur').translate(message) - send_whatsjet_message(from_number, translated_message) - except Exception as e: - logger.error(f"[AI] Translation error: {e}") - send_whatsjet_message(from_number, message) - else: - send_whatsjet_message(from_number, message) - - # Store the product list in context for selection handling - context_manager.update_context( - from_number, - current_state='intelligent_products_menu', - current_menu='intelligent_products_menu', - current_menu_options=[f"Product {i+1}" for i in range(len(products))], - available_products=products, - last_query=query - ) - - # Add to conversation history - context_manager.add_to_history(from_number, query, message) - - else: - # Single product found - show detailed information with media support - selected_product = products[0] - context_manager.update_context( - from_number, - current_product=selected_product, - current_state='product_inquiry', - current_menu='product_inquiry', - current_menu_options=list(MENU_CONFIG['product_inquiry']['option_descriptions'].values()) - ) - - # Get updated context with last message - context = context_manager.get_context(from_number) - - # Use enhanced response with media support - response_with_media = generate_veterinary_product_response_with_media(selected_product, context) - - # Send the response - send_whatsjet_message(from_number, response_with_media['text']) - - # Add to conversation history - context_manager.add_to_history(from_number, query, response_with_media['text']) - - else: - # Enhanced "not found" response with veterinary suggestions - message = ( - "❌ *Product Not Found*\n\n" - f"🔍 *We couldn't find '{query}' in our veterinary database.*\n\n" - "💡 *Try these alternatives:*\n" - "• Check spelling (e.g., 'Hydropex' not 'Hydro pex')\n" - "• Search by symptoms (e.g., 'respiratory', 'liver support')\n" - "• Search by category (e.g., 'antibiotic', 'vitamin')\n" - "• Search by species (e.g., 'poultry', 'livestock')\n\n" - "🏥 *Popular Veterinary Products:*\n" - "• Hydropex (Electrolyte supplement)\n" - "• Heposel (Liver tonic)\n" - "• Bromacid (Respiratory support)\n" - "• Tribiotic (Antibiotic)\n" - "• Symodex (Multivitamin)\n\n" - "💬 *Type 'main' to return to main menu or try another search.*" - ) - - # Translate response if needed - if reply_language == 'ur': - try: - translated_message = GoogleTranslator(source='auto', target='ur').translate(message) - send_whatsjet_message(from_number, translated_message) - except Exception as e: - logger.error(f"[AI] Translation error: {e}") - send_whatsjet_message(from_number, message) - else: - send_whatsjet_message(from_number, message) - - except Exception as e: - logger.error(f"Error in product inquiry: {e}") - # Instead of sending a generic error, return to main menu - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) - send_whatsjet_message(from_number, welcome_msg) - context_manager.update_context(from_number, current_state='main_menu', current_menu='main_menu', current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values())) - -async def handle_category_selection(selection: str, from_number: str): - """Handle category selection from category_selection_menu state""" - try: - user_context = context_manager.get_context(from_number) - available_categories = user_context.get('available_categories', []) - - logger.info(f"[Category] Handling selection '{selection}' for {from_number}") - logger.info(f"[Category] Available categories: {len(available_categories)}") - - if not available_categories: - logger.warning("[Category] No available categories") - send_whatsjet_message(from_number, "❌ No categories available. Please type 'main' to return to main menu.") - return - - if selection.isdigit() and 1 <= int(selection) <= len(available_categories): - selected_category = available_categories[int(selection) - 1] - logger.info(f"[Category] Selected category: '{selected_category}'") - - products = get_products_by_category(selected_category) - logger.info(f"[Category] Found {len(products)} products in category '{selected_category}'") - - if products: - product_message = f"📦 *Products in {selected_category}*\n\n" - for i, product in enumerate(products[:10], 1): # Show first 10 products - product_message += f"{format_number_with_emoji(i)} {product.get('Product Name', 'Unknown')}\n" - - if len(products) > 10: - product_message += f"\n... and {len(products) - 10} more products" - - product_message += "\n\nSelect a product number or type 'main' to return to main menu." - - logger.info(f"[Category] Sending product message for category '{selected_category}'") - send_whatsjet_message(from_number, product_message) - context_manager.update_context( - from_number, - current_state='category_products_menu', - current_menu='category_products_menu', - current_menu_options=[f"Product {i+1}" for i in range(len(products))], - available_products=products, - current_category=selected_category - ) - logger.info(f"[Category] Updated context to category_products_menu with {len(products)} products") - else: - logger.warning(f"[Category] No products found in category '{selected_category}'") - send_whatsjet_message(from_number, f"❌ No products found in {selected_category}") - else: - logger.warning(f"[Category] Invalid category selection: '{selection}' (valid range: 1-{len(available_categories)})") - send_whatsjet_message(from_number, "❌ Invalid selection. Please choose a valid category number.") - - except Exception as e: - logger.error(f"[Category] Error handling category selection: {e}") - # Get user context before using it - user_context = context_manager.get_context(from_number) - welcome_msg = generate_veterinary_welcome_message(from_number, user_context) - send_whatsjet_message(from_number, welcome_msg) - context_manager.update_context( - from_number, - current_state='main_menu', - current_menu='main_menu', - current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values()) - ) - -def get_menu_validation_message(current_state: str, user_context: dict) -> str: - """Get specific validation message for the current menu state""" - if current_state == 'main_menu': - return "❌ Invalid selection for Main Menu. Please choose:\n1️⃣ Search Veterinary Products\n2️⃣ Browse Categories\n3️⃣ Download Catalog\n4️⃣ Chat with Veterinary AI Assistant\n\n💬 Type 'main' to return to main menu." - - elif current_state == 'all_products_menu': - available_products = user_context.get('available_products', []) - total_products = len(available_products) if available_products else 23 - return f"❌ Invalid selection for Product List. Please choose a number between 1 and {total_products}.\n\n💬 Type 'main' to return to main menu." - - elif current_state == 'category_selection_menu': - available_categories = user_context.get('available_categories', []) - total_categories = len(available_categories) if available_categories else 0 - if total_categories > 0: - return f"❌ Invalid selection for Category List. Please choose a number between 1 and {total_categories}.\n\n💬 Type 'main' to return to main menu." - else: - return "❌ No categories available. Please type 'main' to return to main menu." - - elif current_state == 'category_products_menu': - available_products = user_context.get('available_products', []) - total_products = len(available_products) if available_products else 0 - if total_products > 0: - return f"❌ Invalid selection for Category Products. Please choose a number between 1 and {total_products}.\n\n💬 Type 'main' to return to main menu." - else: - return "❌ No products available in this category. Please type 'main' to return to main menu." - - elif current_state == 'product_inquiry': - return "❌ Invalid selection for Product Options. Please choose:\n1️⃣ Talk to Veterinary Consultant\n2️⃣ Inquire about Product Availability\n3️⃣ Back to Main Menu" - - elif current_state == 'ai_chat_mode': - return "💬 *You're in AI Chat mode. Ask me anything about veterinary care!*\n\nType 'main' to return to main menu." - - else: - return "❌ Invalid selection. Please type 'main' to return to main menu." - -def is_valid_menu_selection(selection: str, current_state: str, user_context: dict) -> bool: - """Check if a selection is valid for the current menu state""" - # Use the intelligent voice command processor for consistent handling - mapped_selection = process_intelligent_voice_command(selection, current_state, user_context) - - if not mapped_selection.isdigit(): - return False - - selection_num = int(mapped_selection) - - if current_state == 'main_menu': - return 1 <= selection_num <= 4 - - elif current_state == 'all_products_menu': - available_products = user_context.get('available_products', []) - total_products = len(available_products) if available_products else 23 - return 1 <= selection_num <= total_products - - elif current_state == 'category_selection_menu': - available_categories = user_context.get('available_categories', []) - total_categories = len(available_categories) if available_categories else 0 - return 1 <= selection_num <= total_categories - - elif current_state == 'category_products_menu': - available_products = user_context.get('available_products', []) - total_products = len(available_products) if available_products else 0 - return 1 <= selection_num <= total_products - - elif current_state == 'product_inquiry': - return 1 <= selection_num <= 3 - - elif current_state == 'ai_chat_mode': - return mapped_selection == 'main' - - return False - -# Load products on startup -load_products_data() - -# Add these functions after the existing imports and before the main functions - -def get_product_image_path(product_name: str) -> str: - """ - Get the public URL for a product image if it exists in the uploads directory. - Returns the public URL if found, otherwise falls back to static/images or None. - """ - try: - # Check uploads directory for exact match (case and spaces preserved) - uploads_dir = "uploads" - image_extensions = ['.jpg', '.jpeg', '.png'] - for ext in image_extensions: - filename = f"{product_name}{ext}" - local_path = os.path.join(uploads_dir, filename) - if os.path.exists(local_path): - # Construct the public URL for Hugging Face Space - # (Assumes the Space is named dreamstream-1-chatbot) - public_url = f"https://dreamstream-1-chatbot.hf.space/uploads/{filename.replace(' ', '%20')}" - logger.info(f"[Image] Found product image in uploads: {public_url}") - return public_url - # Fallback to static/images (old logic) + # Create images directory if it doesn't exist images_dir = "static/images" os.makedirs(images_dir, exist_ok=True) + + # Clean product name for filename safe_name = re.sub(r'[^\w\s-]', '', product_name).replace(' ', '_').lower() + + # Check for common image extensions image_extensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif'] + for ext in image_extensions: image_path = os.path.join(images_dir, f"{safe_name}{ext}") if os.path.exists(image_path): logger.info(f"[Image] Found product image: {image_path}") return image_path + + # If no specific product image found, check for a default image default_image_path = os.path.join(images_dir, "default_product.jpg") if os.path.exists(default_image_path): logger.info(f"[Image] Using default product image: {default_image_path}") return default_image_path + logger.warning(f"[Image] No image found for product: {product_name}") return None + except Exception as e: logger.error(f"[Image] Error getting product image path: {e}") return None @@ -3638,11 +3169,13 @@ def generate_veterinary_product_response_with_media(product_info: Dict[str, Any] if pd.isna(text) or text is None: return "Not specified" return str(text).strip() + product_name = clean_text(product_info.get('Product Name', '')) product_type = clean_text(product_info.get('Type', '')) category = clean_text(product_info.get('Category', '')) indications = clean_text(product_info.get('Indications', '')) pdf_link = "" + try: csv_data = pd.read_csv('Veterinary.csv') product_row = csv_data[csv_data['Product Name'] == product_name] @@ -3652,17 +3185,22 @@ def generate_veterinary_product_response_with_media(product_info: Dict[str, Any] pdf_link = brochure_link.strip() except Exception as e: logger.warning(f"Error checking PDF link for {product_name}: {e}") + response_text = f"""🧪 *Name:* {product_name}\n📦 *Type:* {product_type}\n🏥 *Category:* {category}\n💊 *Used For:* {indications}""" + if pdf_link: response_text += f"\n\n📄 Product Brochure Available\n🔗 {product_name} PDF:\n{pdf_link}" + response_text += f""" \n💬 *Available Actions:* 1️⃣ Talk to Veterinary Consultant 2️⃣ Inquire About Availability 3️⃣ Back to Main Menu \n💬 Select an option or ask about related products""" + image_path = get_product_image_path(product_name) has_image = image_path is not None and os.path.exists(image_path) + return { 'text': response_text, 'has_image': has_image, @@ -3670,11 +3208,11 @@ def generate_veterinary_product_response_with_media(product_info: Dict[str, Any] 'product_name': product_name } -# Ensure static/images directory exists for product images def ensure_images_dir(): - images_dir = os.path.join('static', 'images') + """Ensure the images directory exists""" + images_dir = "static/images" os.makedirs(images_dir, exist_ok=True) - return images_dir + logger.info(f"[Image] Ensured images directory exists: {images_dir}") # New feature: Send product image with caption (product details) async def send_product_image_with_caption(from_number: str, product: Dict[str, Any], user_context: Dict[str, Any]): @@ -3809,246 +3347,126 @@ async def send_product_image_with_caption(from_number: str, product: Dict[str, A logger.info(f"[Product] Falling back to text-only message for: {product_name}") send_whatsjet_message(from_number, details) +# Test endpoint for product image with caption @app.get("/test-product-image-with-caption") async def test_product_image_with_caption(phone: str): - """ - Test endpoint to send Heposel product image and details as caption to the given phone number. - """ - # Load Heposel product from CSV + """Test endpoint for sending product image with caption""" try: - df = pd.read_csv('Veterinary.csv') - row = df[df['Product Name'].str.lower() == 'heposel'] - if row.empty: - return {"error": "Heposel not found in CSV"} - product = row.iloc[0].to_dict() - user_context = context_manager.get_context(phone) + if products_df is None or products_df.empty: + return {"error": "No products loaded"} + + # Get first product for testing + product = products_df.iloc[0].to_dict() + user_context = {} + await send_product_image_with_caption(phone, product, user_context) - return {"status": "sent", "phone": phone} + + return { + "success": True, + "message": f"Test product image sent to {phone}", + "product": product.get('Product Name', 'Unknown') + } + except Exception as e: + logger.error(f"Error in test product image with caption: {e}") return {"error": str(e)} +# Test endpoint for image sending @app.get("/test-image-sending") async def test_image_sending(phone: str, image_url: str = "https://www.w3schools.com/w3images/lights.jpg"): - """ - Test endpoint to send a test image with caption to debug image sending functionality. - """ + """Test endpoint for sending images via WhatsApp""" try: - test_message = f"""🧪 *Test Image Message* - -This is a test message to verify image sending functionality. - -📸 *Image Details:* -• URL: {image_url} -• Type: JPEG -• Purpose: Testing WhatsJet API - -💬 *Test Options:* -1️⃣ Send another test image -2️⃣ Test with different URL -3️⃣ Back to main menu - -Please confirm if you received both the image and this text message.""" + filename = "test_image.jpg" success = send_whatsjet_message( phone, - test_message, + "🖼️ *Test Image*\n\nThis is a test image sent via WhatsApp API.", media_type="image/jpeg", media_path=image_url, - filename="test_image.jpg" + filename=filename ) if success: return { - "status": "success", - "phone": phone, - "image_url": image_url, - "message": "Test image and caption sent successfully" + "success": True, + "message": f"Test image sent successfully to {phone}", + "image_url": image_url } else: return { - "status": "failed", - "phone": phone, - "image_url": image_url, - "message": "Failed to send test image" + "success": False, + "message": f"Failed to send test image to {phone}", + "image_url": image_url } + except Exception as e: - return {"error": str(e), "phone": phone, "image_url": image_url} + logger.error(f"Error in test image sending: {e}") + return {"error": str(e)} +# Debug endpoint for WhatsJet @app.get("/debug-whatsjet") async def debug_whatsjet(): - """ - Debug endpoint to check WhatsJet configuration and test basic functionality. - """ + """Debug endpoint to check WhatsJet configuration""" try: - # Check environment variables - config_status = { - "WHATSJET_API_URL": bool(WHATSJET_API_URL), - "WHATSJET_VENDOR_UID": bool(WHATSJET_VENDOR_UID), - "WHATSJET_API_TOKEN": bool(WHATSJET_API_TOKEN), - "all_configured": all([WHATSJET_API_URL, WHATSJET_VENDOR_UID, WHATSJET_API_TOKEN]) + config = { + "api_url": WHATSJET_API_URL, + "vendor_uid": WHATSJET_VENDOR_UID, + "api_token": "***" if WHATSJET_API_TOKEN else None, + "server_url": SERVER_URL, + "openai_key": "***" if OPENAI_API_KEY else None } - # Test basic text message if configured - test_result = None - if config_status["all_configured"]: - try: - # Test with a simple text message - test_phone = "1234567890" # Dummy phone for testing - test_success = send_whatsjet_message( - test_phone, - "🧪 WhatsJet API Test Message\n\nThis is a test to verify API connectivity.", - ) - test_result = { - "success": test_success, - "message": "API test completed (dummy phone number used)" - } - except Exception as e: - test_result = { - "success": False, - "error": str(e) - } - return { - "timestamp": datetime.now().isoformat(), - "config_status": config_status, - "test_result": test_result, - "api_url": WHATSJET_API_URL if config_status["all_configured"] else "Not configured", - "vendor_uid": WHATSJET_VENDOR_UID if config_status["all_configured"] else "Not configured" + "status": "success", + "config": config, + "timestamp": datetime.now().isoformat() } + except Exception as e: - return {"error": str(e), "timestamp": datetime.now().isoformat()} + return { + "status": "error", + "error": str(e), + "timestamp": datetime.now().isoformat() + } +# Test endpoint for WhatsJet payloads @app.get("/test-whatsjet-payloads") async def test_whatsjet_payloads(phone: str): - """ - Test different WhatsJet API payload formats to identify the correct one for image sending. - """ - if not all([WHATSJET_API_URL, WHATSJET_VENDOR_UID, WHATSJET_API_TOKEN]): - return {"error": "WhatsJet not configured"} - - url = f"{WHATSJET_API_URL}/{WHATSJET_VENDOR_UID}/contact/send-message?token={WHATSJET_API_TOKEN}" - test_image_url = "https://www.w3schools.com/w3images/lights.jpg" - - # Download test image + """Test endpoint to check WhatsJet payloads""" try: - response = requests.get(test_image_url, timeout=10) - response.raise_for_status() - image_content = response.content - image_b64 = base64.b64encode(image_content).decode('utf-8') + # Test basic message sending + test_message = "🧪 *WhatsJet Test*\n\nThis is a test message to verify WhatsJet integration." + success = send_whatsjet_message(phone, test_message) + + return { + "status": "success" if success else "failed", + "message": f"WhatsJet test message sent to {phone}", + "success": success, + "timestamp": datetime.now().isoformat() + } + except Exception as e: - return {"error": f"Failed to download test image: {e}"} - - results = {} - - # Test different payload formats - test_payloads = [ - { - "name": "Format 1: Standard with media_content", - "payload": { - "phone_number": phone, - "media_type": "image/jpeg", - "media_content": image_b64, - "media_filename": "test.jpg", - "message_body": "" - } - }, - { - "name": "Format 2: With media_url instead of media_content", - "payload": { - "phone_number": phone, - "media_type": "image/jpeg", - "media_url": test_image_url, - "media_filename": "test.jpg", - "message_body": "" - } - }, - { - "name": "Format 3: With file field", - "payload": { - "phone_number": phone, - "file": image_b64, - "file_type": "image/jpeg", - "filename": "test.jpg", - "message_body": "" - } - }, - { - "name": "Format 4: With attachment field", - "payload": { - "phone_number": phone, - "attachment": image_b64, - "attachment_type": "image/jpeg", - "attachment_name": "test.jpg", - "message_body": "" - } - }, - { - "name": "Format 5: With image field", - "payload": { - "phone_number": phone, - "image": image_b64, - "image_type": "image/jpeg", - "image_name": "test.jpg", - "message_body": "" - } + return { + "status": "error", + "error": str(e), + "timestamp": datetime.now().isoformat() } - ] - - for test in test_payloads: - try: - logger.info(f"[WhatsJet] Testing payload format: {test['name']}") - headers = {"Authorization": f"Bearer {WHATSJET_API_TOKEN}"} - response = httpx.post(url, json=test['payload'], headers=headers, timeout=30) - - results[test['name']] = { - "status_code": response.status_code, - "success": response.status_code == 200, - "response_body": response.text[:200] if response.text else "No response body" - } - - logger.info(f"[WhatsJet] {test['name']} - Status: {response.status_code}") - - except Exception as e: - results[test['name']] = { - "status_code": "Error", - "success": False, - "error": str(e) - } - logger.error(f"[WhatsJet] {test['name']} - Error: {e}") - - return { - "timestamp": datetime.now().isoformat(), - "phone": phone, - "test_image_url": test_image_url, - "results": results - } +# Test endpoint for cPanel image access @app.get("/test-cpanel-image-access") async def test_cpanel_image_access(): - """ - Test endpoint to check if cPanel image URLs are now accessible with browser-like headers. - """ + """Test endpoint to check cPanel image access""" try: - image_url = "https://amgocus.com/uploads/images/Respira%20Aid%20Plus.jpg" - - # Test with browser-like headers - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br', - 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1' - } + # Test image URL that should be accessible via cPanel + image_url = "https://your-cpanel-domain.com/public_html/static/images/test.jpg" - logger.info(f"[Test] Testing cPanel image URL with browser headers: {image_url}") - response = requests.head(image_url, headers=headers, timeout=10, allow_redirects=True) + # Test if the URL is accessible + response = requests.head(image_url, timeout=10) result = { "image_url": image_url, - "status_code": response.status_code, - "headers": dict(response.headers), "accessible": response.status_code == 200, + "status_code": response.status_code, "timestamp": datetime.now().isoformat() } @@ -4067,7 +3485,284 @@ async def test_cpanel_image_access(): "timestamp": datetime.now().isoformat() } +# Load products on startup +def load_products_data(): + """Load products data from CSV file""" + global products_df + try: + if os.path.exists(CSV_FILE): + products_df = pd.read_csv(CSV_FILE) + logger.info(f"✅ Loaded {len(products_df)} products from {CSV_FILE}") + else: + logger.warning(f"⚠️ CSV file {CSV_FILE} not found") + products_df = pd.DataFrame() + except Exception as e: + logger.error(f"❌ Error loading products data: {e}") + products_df = pd.DataFrame() + +def convert_drive_link(link: str) -> str: + """Convert Google Drive link to direct download link""" + if 'drive.google.com' in link: + file_id = link.split('/')[-2] if '/d/' in link else link.split('/')[-1] + return f"https://drive.google.com/uc?export=download&id={file_id}" + return link + +def format_number_with_emoji(number: int) -> str: + """Format number with emoji""" + emoji_map = { + 1: "1️⃣", 2: "2️⃣", 3: "3️⃣", 4: "4️⃣", 5: "5️⃣", + 6: "6️⃣", 7: "7️⃣", 8: "8️⃣", 9: "9️⃣", 10: "🔟", + 11: "1️⃣1️⃣", 12: "1️⃣2️⃣", 13: "1️⃣3️⃣", 14: "1️⃣4️⃣", 15: "1️⃣5️⃣", + 16: "1️⃣6️⃣", 17: "1️⃣7️⃣", 18: "1️⃣8️⃣", 19: "1️⃣9️⃣", 20: "2️⃣0️⃣", + 21: "2️⃣1️⃣", 22: "2️⃣2️⃣", 23: "2️⃣3️⃣" + } + return emoji_map.get(number, f"{number}.") + +async def display_all_products(from_number: str): + """Display all products in multiple messages and update menu context""" + try: + user_context = context_manager.get_context(from_number) + current_state = user_context.get('current_state', 'main_menu') + logger.info(f"[Display] display_all_products called for {from_number} in state: {current_state}") + if current_state == 'all_products_menu': + logger.warning(f"[Display] Already in all_products_menu state for {from_number}, skipping display") + return + if products_df is None or products_df.empty: + send_whatsjet_message(from_number, "❌ No products available at the moment.") + return + # Set state to all_products_menu and store menu context + products = products_df.to_dict('records') + context_manager.update_context( + from_number, + current_state='all_products_menu', + current_menu='all_products_menu', + current_menu_options=[p.get('Product Name', 'Unknown') for p in products], + available_products=products + ) + logger.info(f"[Display] Set state to all_products_menu for {from_number}") + # Send products in chunks + chunk_size = 5 + for i in range(0, len(products), chunk_size): + chunk = products[i:i + chunk_size] + message = f"📋 *Products ({i+1}-{min(i+chunk_size, len(products))} of {len(products)})*\n\n" + for j, product in enumerate(chunk, i+1): + message += f"{format_number_with_emoji(j)} {product.get('Product Name', 'Unknown')}\n" + if product.get('Category'): + message += f" Category: {product.get('Category')}\n" + message += "\n" + send_whatsjet_message(from_number, message) + send_whatsjet_message(from_number, + "💬 Type a product name to get detailed information, or type 'main' to return to main menu.") + except Exception as e: + logger.error(f"[Display] Error displaying products: {e}") + send_whatsjet_message(from_number, "❌ Error displaying products. Please try again.") + +def get_all_categories(): + """Return a list of all unique categories from the products DataFrame""" + if products_df is not None and not products_df.empty: + return list(products_df['Category'].unique()) + return [] + +def get_products_by_category(category: str): + """Get products by category""" + if products_df is None or products_df.empty: + return [] + category_products = products_df[products_df['Category'] == category] + return category_products.to_dict('records') + +async def handle_category_selection(selection: str, from_number: str): + """Handle category selection""" + try: + user_context = context_manager.get_context(from_number) + available_categories = user_context.get('available_categories', []) + + if selection.isdigit() and 1 <= int(selection) <= len(available_categories): + selected_category = available_categories[int(selection) - 1] + products = get_products_by_category(selected_category) + + if products: + # Update context with category products + context_manager.update_context( + from_number, + current_category=selected_category, + current_state='category_products_menu', + current_menu='category_products_menu', + current_menu_options=[p.get('Product Name', 'Unknown') for p in products], + available_products=products + ) + + # Send category products + message = f"📦 *Products in {selected_category}*\n\n" + for i, product in enumerate(products, 1): + message += f"{format_number_with_emoji(i)} {product.get('Product Name', 'Unknown')}\n" + if product.get('Target Species'): + message += f" Target: {product.get('Target Species')}\n" + message += "\n" + + message += "💬 Select a product number or type 'main' to return to main menu." + send_whatsjet_message(from_number, message) + else: + send_whatsjet_message(from_number, f"❌ No products found in {selected_category} category.") + else: + send_whatsjet_message(from_number, "❌ Invalid selection. Please choose a valid category number.") + except Exception as e: + logger.error(f"[Category] Error handling category selection: {e}") + send_helpful_guidance(from_number, 'category_selection_menu') + +def get_menu_validation_message(current_state: str, user_context: dict) -> str: + """Get appropriate validation message for current menu state""" + if current_state == 'main_menu': + return ( + "❌ *Invalid Selection*\n\n" + "Please choose from the main menu:\n" + "1️⃣ Search Veterinary Products\n" + "2️⃣ Browse Categories\n" + "3️⃣ Download Catalog\n" + "4️⃣ Chat with Veterinary AI Assistant\n\n" + "💬 *You can also:*\n" + "• Type a product name (e.g., 'hydropex', 'respira aid plus')\n" + "• Type 'main' to refresh the menu" + ) + elif current_state == 'all_products_menu': + if products_df is not None and not products_df.empty: + total_products = len(products_df) + return ( + f"❌ *Invalid Product Selection*\n\n" + f"Please choose a product number between 1 and {total_products}.\n\n" + "💬 *You can also:*\n" + "• Type a product name (e.g., 'hydropex', 'respira aid plus')\n" + "• Type 'main' to return to main menu" + ) + else: + return "❌ No products available. Type 'main' to return to main menu." + elif current_state == 'category_products_menu': + available_products = user_context.get('available_products', []) + if available_products: + return ( + f"❌ *Invalid Product Selection*\n\n" + f"Please choose a product number between 1 and {len(available_products)}.\n\n" + "💬 *You can also:*\n" + "• Type a product name (e.g., 'hydropex', 'respira aid plus')\n" + "• Type 'main' to return to main menu" + ) + else: + return "❌ No products available in this category. Type 'main' to return to main menu." + elif current_state == 'category_selection_menu': + available_categories = user_context.get('available_categories', []) + if available_categories: + return ( + f"❌ *Invalid Category Selection*\n\n" + f"Please choose a category number between 1 and {len(available_categories)}.\n\n" + "💬 *You can also:*\n" + "• Type a product name (e.g., 'hydropex', 'respira aid plus')\n" + "• Type 'main' to return to main menu" + ) + else: + return "❌ No categories available. Type 'main' to return to main menu." + elif current_state == 'product_inquiry': + return ( + "❌ *Invalid Selection*\n\n" + "Please choose an option:\n" + "1️⃣ Talk to Veterinary Consultant\n" + "2️⃣ Inquire About Availability\n" + "3️⃣ Back to Main Menu\n\n" + "💬 *You can also:*\n" + "• Type a product name (e.g., 'hydropex', 'respira aid plus')\n" + "• Type 'main' to return to main menu" + ) + else: + return ( + "❌ *Invalid Selection*\n\n" + "Please choose a valid option or type 'main' to return to main menu.\n\n" + "💬 *You can also:*\n" + "• Type a product name (e.g., 'hydropex', 'respira aid plus')" + ) + +def is_valid_menu_selection(selection: str, current_state: str, user_context: dict) -> bool: + """Check if selection is valid for current menu state""" + is_valid, _ = validate_menu_selection(selection, current_state, user_context) + return is_valid + +def generate_veterinary_welcome_message(phone_number=None, user_context=None): + """Generate veterinary welcome message""" + return ( + "🏥 *Welcome to Apex Biotical Veterinary Bot*\n\n" + "I'm your intelligent veterinary assistant. How can I help you today?\n\n" + "📋 *Main Menu:*\n" + "1️⃣ Search Veterinary Products\n" + "2️⃣ Browse Categories\n" + "3️⃣ Download Catalog\n" + "4️⃣ Chat with Veterinary AI Assistant\n\n" + "💬 *Quick Actions:*\n" + "• Type a product name (e.g., 'hydropex', 'respira aid plus')\n" + "• Ask about symptoms (e.g., 'respiratory problems', 'liver support')\n" + "• Search by category (e.g., 'antibiotics', 'vitamins')\n\n" + "🎤 *Voice messages are supported!*\n" + "You can speak product names, menu numbers, or ask questions." + ) + +async def handle_veterinary_product_followup(selection: str, from_number: str) -> None: + """ + Handle product follow-up selections with enhanced veterinary domain support + """ + try: + user_context = context_manager.get_context(from_number) + current_product = user_context.get('current_product') + + if not current_product: + send_whatsjet_message(from_number, "❌ No product selected. Please search for a product first.") + return + + if selection == '1': + # Talk to Veterinary Consultant + product_name = current_product.get('Product Name', 'the selected product') + consultant_msg = ( + f"📞 Contact Veterinary Consultant\n\n" + f"Product: {product_name}\n\n" + "Please provide your details:\n" + "* Name and location\n" + "* Specific inquiry\n\n" + "💬 Example: Dr. Ali - Multan - Need consultation for respiratory problems\n\n" + "Type main at any time to go to main menu." + ) + send_whatsjet_message(from_number, consultant_msg) + context_manager.update_context( + from_number, + current_state='contact_request', + current_menu='contact_request', + current_menu_options=['Provide contact details'] + ) + + elif selection == '2': + # Inquire about Product Availability + await handle_availability_inquiry(from_number, user_context) + + elif selection == '3': + welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + send_whatsjet_message(from_number, welcome_msg) + context_manager.update_context( + from_number, + current_state='main_menu', + current_menu='main_menu', + current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values()) + ) + return + else: + send_whatsjet_message(from_number, "❌ Invalid selection. Please choose 1, 2, or 3.") + return + except Exception as e: + logger.error(f"Error in product follow-up: {e}") + user_context = context_manager.get_context(from_number) + welcome_msg = generate_veterinary_welcome_message(from_number, user_context) + send_whatsjet_message(from_number, welcome_msg) + context_manager.update_context( + from_number, + current_state='main_menu', + current_menu='main_menu', + current_menu_options=list(MENU_CONFIG['main_menu']['option_descriptions'].values()) + ) + if __name__ == "__main__": # Launch FastAPI app import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7860) + uvicorn.run(app, host="0.0.0.0", port=7860)