Spaces:
Running
Running
| """ | |
| 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' | |