""" Event Recommendation Scenario Handler Recommends events based on user's vibe/mood with RAG integration """ from typing import Dict, Any from .base_handler import BaseScenarioHandler class EventRecommendationHandler(BaseScenarioHandler): """ Handle event recommendation flow Steps: 1. Ask for vibe/mood (Chill, Sôi động, Hài, Workshop) 2. Search events matching vibe → RAG 3. Show event list, ask which to see details 4. Ask what info needed (price, lineup, location, time) 5-8. Show specific info → RAG 9. Ask if want to save event to email 10. Collect email + send summary 11-12. End scenario """ def start(self, initial_data: Dict = None) -> Dict[str, Any]: """Start event recommendation flow""" return { "message": "Hello! 👋 Bạn muốn tìm sự kiện theo vibe gì nè? Chill – Sôi động – Hài – Workshop?", "new_state": { "active_scenario": "event_recommendation", "scenario_step": 1, "scenario_data": initial_data or {} } } def next_step(self, current_step: int, user_input: str, scenario_data: Dict) -> Dict[str, Any]: """Process user input and advance scenario""" # Get expected input type for this step expected_type = self._get_expected_type(current_step) # Check for unexpected input (off-topic questions) unexpected = self.handle_unexpected_input(user_input, expected_type, current_step) if unexpected: return unexpected # ===== STEP 1: Collect interest tag ===== if current_step == 1: scenario_data['interest_tag'] = user_input return { "message": f"Mình hiểu rồi! Để mình tìm sự kiện hợp vibe **{user_input}** nha", "new_state": { "active_scenario": "event_recommendation", "scenario_step": 2, "scenario_data": scenario_data }, "scenario_active": True } # ===== STEP 2: Execute RAG search for events ===== elif current_step == 2: # Search events matching interest_tag query = f"sự kiện phù hợp với {scenario_data.get('interest_tag', 'mọi người')}" print(f"🔍 RAG Search: {query}") results = self._search_rag(query, limit=5) # Get up to 5 formatted_events = self._format_event_list(results) # Save results to scenario data scenario_data['rag_results'] = formatted_events scenario_data['available_events'] = [ r['metadata'].get('id_use', r['metadata'].get('original_id', f'event_{i+1}')) for i, r in enumerate(results) ] return { "message": f"Đây là các event hợp với bạn nè:\n{formatted_events}\n\nBạn có muốn xem chi tiết event nào không?", "new_state": { "active_scenario": "event_recommendation", "scenario_step": 3, "scenario_data": scenario_data }, "scenario_active": True, "loading_message": "⏳ Bạn đợi tôi tìm 1 chút nhé..." } # ===== STEP 3: User picks event ===== elif current_step == 3: scenario_data['event_name'] = user_input return { "message": "Bạn cần xem: giá – line-up – địa điểm – hay thời gian của sự kiện?", "new_state": { "active_scenario": "event_recommendation", "scenario_step": 4, "scenario_data": scenario_data }, "scenario_active": True } # ===== STEP 4: Branch based on info choice ===== elif current_step == 4: choice = self._detect_choice(user_input) event_name = scenario_data.get('event_name', 'sự kiện này') # Build RAG query based on choice query_map = { 'price': f"giá vé {event_name}", 'lineup': f"lineup nghệ sĩ {event_name}", 'location': f"địa điểm tổ chức {event_name}", 'time': f"thời gian lịch diễn {event_name}" } query = query_map.get(choice, query_map['price']) print(f"🔍 RAG Search: {query}") results = self._search_rag(query) formatted_info = self._format_rag_results(results) # Build response message message_map = { 'price': f"Giá vé event {event_name} nè:\n{formatted_info}", 'lineup': f"Lineup / nghệ sĩ của event {event_name} là:\n{formatted_info}", 'location': f"Địa điểm tổ chức event {event_name}:\n{formatted_info}", 'time': f"Thời gian / lịch diễn của event {event_name}:\n{formatted_info}" } return { "message": message_map.get(choice, message_map['price']), "new_state": { "active_scenario": "event_recommendation", "scenario_step": 9, # Skip to email step "scenario_data": scenario_data }, "scenario_active": True, "loading_message": "⏳ Bạn đợi tôi tìm 1 chút nhé..." } # ===== STEP 9: Ask if want to save event to email ===== elif current_step == 9: choice = self._detect_yes_no(user_input) if choice == 'yes': return { "message": "Cho mình xin email để gửi bản tóm tắt event kèm link mua vé?", "new_state": { "active_scenario": "event_recommendation", "scenario_step": 10, "scenario_data": scenario_data }, "scenario_active": True } else: return { "message": "Okie, bạn cần event theo vibe khác không nè? 😄", "new_state": None, "scenario_active": False, "end_scenario": True } # ===== STEP 10: Collect email and send summary ===== elif current_step == 10: email = user_input.strip() if not self._validate_email(email): return { "message": "Email này có vẻ không đúng định dạng. Bạn nhập lại giúp mình nhé? (Ví dụ: name@example.com)", "new_state": None, # Stay at same step "scenario_active": True } # Save lead scenario_data['email'] = email try: self.lead_storage.save_lead( event_name=scenario_data.get('event_name', 'Unknown Event'), email=email, interests={ "vibe": scenario_data.get('interest_tag'), "wants_event_summary": True }, session_id=scenario_data.get('session_id') ) print(f"📧 Lead saved: {email}") except Exception as e: print(f"⚠️ Error saving lead: {e}") return { "message": "Đã gửi email cho bạn nha! ✨", "new_state": None, "scenario_active": False, "end_scenario": True, "action": "send_event_summary_email" } # Fallback - unknown step return { "message": "Xin lỗi, có lỗi xảy ra. Bạn muốn bắt đầu lại không?", "new_state": None, "scenario_active": False, "end_scenario": True } def _get_expected_type(self, step: int) -> str: """Get expected input type for each step""" type_map = { 1: 'interest_tag', 2: None, # Auto-advance after RAG 3: 'event_name', 4: 'choice', 9: 'choice', 10: 'email' } return type_map.get(step, 'text') def _format_event_list(self, results: list) -> str: """Format event search results - IMPROVED to show actual event count""" print(f"🔍 DEBUG: RAG returned {len(results)} results") if not results or len(results) == 0: print("⚠️ DEBUG: No results found") return "Hiện tại chưa có event phù hợp 😢\nBạn thử vibe khác nhé!" # Debug: Print first result to see structure print(f"🔍 DEBUG: First result metadata: {results[0].get('metadata', {})}") events = [] # FIXED: Use actual length instead of hardcoded [:3] num_events = min(len(results), 5) # Show max 5 events print(f"🔍 DEBUG: Formatting {num_events} events") for i, r in enumerate(results[:num_events], 1): metadata = r.get('metadata', {}) # Extract event info from Qdrant metadata event_id = metadata.get('id_use', metadata.get('original_id')) texts = metadata.get('texts', []) text = texts[0] if texts and len(texts) > 0 else metadata.get('text', '') print(f"🔍 DEBUG Event {i}:") print(f" - ID: {event_id}") print(f" - Text length: {len(text)} chars") print(f" - Text preview: {text[:100]}") # Extract event name from text # Try to find event name pattern in text name = self._extract_event_name_from_text(text) if not name: # Fallback to shortened text name = text[:60].strip() if len(text) > 60: name += "..." print(f" - Extracted name: {name}") # Format event string event_str = f"{i}. **{name}**" events.append(event_str) result = "\n".join(events) print(f"✅ DEBUG: Formatted {len(events)} events successfully") return result def _extract_event_name_from_text(self, text: str) -> str: """ Try to extract event name from description text Patterns to match: - "Sự kiện văn hóa hài kịch" → "Văn hóa hài kịch" - "Stand-up Comedy Night - ..." → "Stand-up Comedy Night" """ if not text: return "" # Remove "Sự kiện" prefix if exists clean_text = text if clean_text.startswith("Sự kiện "): clean_text = clean_text[8:].strip() # Take first sentence or first 80 chars sentences = clean_text.split('.') if sentences and len(sentences) > 0: first_sentence = sentences[0].strip() if len(first_sentence) > 80: return first_sentence[:77] + "..." return first_sentence # Fallback if len(clean_text) > 80: return clean_text[:77] + "..." return clean_text def _detect_choice(self, user_input: str) -> str: """Detect what info user wants to see""" input_lower = user_input.lower() if any(k in input_lower for k in ['giá', 'price', 'vé', 'ticket', 'bao nhiêu']): return 'price' elif any(k in input_lower for k in ['lineup', 'line-up', 'nghệ sĩ', 'artist', 'performer']): return 'lineup' elif any(k in input_lower for k in ['địa điểm', 'location', 'ở đâu', 'where', 'chỗ']): return 'location' elif any(k in input_lower for k in ['thời gian', 'time', 'khi nào', 'when', 'lịch', 'date']): return 'time' else: return 'price' # Default def _detect_yes_no(self, user_input: str) -> str: """Detect yes/no response""" input_lower = user_input.lower() if any(k in input_lower for k in ['có', 'yes', 'ok', 'được', 'ừ', 'oke']): return 'yes' else: return 'no'