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