""" 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 ===== "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 as numbered list""" print(f"🔍 DEBUG: RAG returned {len(results)} results") if not results or len(results) == 0: 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 if len(results) > 0: print(f"🔍 DEBUG: First result metadata: {results[0].get('metadata', {})}") events = [] for i, r in enumerate(results[:3], 1): metadata = r.get('metadata', {}) # Extract event info from metadata # Your Qdrant has: {'texts': [...], 'id_use': '...'} 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', '') # Use first 60 chars of text as event name name = text[:60].strip() + "..." if len(text) > 60 else text.strip() print(f"🔍 DEBUG: Event {i}: id={event_id}, name={name[:50]}") # Simple format for now (can enhance with API call later) event_str = f"{i}. **{name}**" # Store event_id for later API call if needed if event_id: event_str += f" (ID: {event_id[:8]}...)" events.append(event_str) return "\n".join(events) async def _format_event_list_with_api(self, results: list) -> str: """ Format event search results by calling API for full details """ print(f"🔍 DEBUG: RAG returned {len(results)} results") if not results or len(results) == 0: return "Hiện tại chưa có event phù hợp 😢\nBạn thử vibe khác nhé!" # Import event service from event_service import EventService event_service = EventService() events = [] for i, r in enumerate(results[:3], 1): metadata = r.get('metadata', {}) event_id = metadata.get('id_use', metadata.get('original_id')) print(f"🔍 DEBUG: Fetching event {i} with ID: {event_id}") # Try to get full event data from API event_data = None if event_id: event_data = await event_service.get_event_by_id(event_id) if event_data: # Use API data name = event_data.get("eventName", "Sự kiện") start = event_data.get("eventStartTime", "") date_str = start[:10] if start else "TBA" location = event_data.get("eventAddress", "") event_str = f"{i}. **{name}**" if date_str != "TBA": event_str += f" ({date_str})" if location: event_str += f" - {location}" print(f"✅ Event {i}: {name} ({date_str})") else: # Fallback to text from Qdrant texts = metadata.get('texts', []) text = texts[0] if texts and len(texts) > 0 else "" name = text[:60].strip() + "..." if len(text) > 60 else text.strip() event_str = f"{i}. **{name}**" print(f"⚠️ Event {i}: Fallback to text (API failed)") events.append(event_str) await event_service.close() return "\n".join(events) 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'