""" Nutrition Agent - Specialized agent for nutrition advice """ from config.settings import client, MODEL from modules.nutrition import NutritionAdvisor from health_data import HealthContext from personalization import PersonalizationEngine from rag.rag_integration import get_rag_integration from agents.core.base_agent import BaseAgent from agents.core.context_analyzer import ContextAnalyzer from agents.core.response_validator import ResponseValidator from typing import Dict, Any, List, Optional from datetime import datetime import re class NutritionAgent(BaseAgent): def __init__(self, memory=None): super().__init__(memory) self.advisor = NutritionAdvisor() self.health_context = None self.personalization = None self.rag = get_rag_integration() # Configure handoff triggers for nutrition agent self.handoff_triggers = { 'exercise_agent': ['tập', 'gym', 'cardio', 'yoga', 'chạy bộ', 'thể dục', 'vận động'], 'symptom_agent': ['đau bụng', 'buồn nôn', 'tiêu chảy', 'dị ứng', 'ngộ độc'], 'mental_health_agent': ['stress', 'lo âu', 'mất ngủ', 'ăn không ngon'], 'general_health_agent': ['khám', 'xét nghiệm', 'bác sĩ'] } self.system_prompt = """Bạn là chuyên gia dinh dưỡng chuyên nghiệp. 🥗 CHUYÊN MÔN: - Tư vấn dinh dưỡng cá nhân hóa dựa trên BMI, tuổi, giới tính, mục tiêu - Tính toán calo, macro (protein/carb/fat) - Gợi ý thực đơn phù hợp - Tư vấn thực phẩm bổ sung - Hướng dẫn ăn uống cho các bệnh lý (tiểu đường, huyết áp, tim mạch...) 🎯 CÁCH TƯ VẤN: 1. **KIỂM TRA THÔNG TIN TRƯỚC KHI HỎI:** - ĐỌC KỸ chat history - user có thể đã cung cấp thông tin rồi! - Nếu user đã nói "tôi 25 tuổi, nam, 70kg, 175cm" → ĐỪNG HỎI LẠI! - Chỉ hỏi thông tin THỰC SỰ còn thiếu - Nếu đã đủ (tuổi, giới tính, cân nặng, chiều cao) → ĐƯA KHUYẾN NGHỊ NGAY! 2. **ƯU TIÊN THÔNG TIN:** - Câu 1: Mục tiêu (giảm cân/tăng cân/duy trì?) - Câu 2: Cân nặng, chiều cao (để tính BMI) - Câu 3: Mức độ hoạt động (ít/vừa/nhiều) - Câu 4 (nếu cần): Bệnh nền, dị ứng 3. **KHI USER KHÔNG MUỐN CUNG CẤP:** - User nói "không biết", "không muốn nói", "tư vấn chung thôi" - → DỪNG hỏi, đưa khuyến nghị chung - Dựa trên thông tin ĐÃ CÓ để tư vấn 4. **ĐƯA KHUYẾN NGHỊ:** - Nếu có đủ thông tin: Tính calo, macro cụ thể - Nếu thiếu thông tin: Đưa khuyến nghị chung (400g rau củ, protein đủ, etc.) - Gợi ý thực đơn mẫu - KHÔNG hỏi thêm nữa ⚠️ AN TOÀN: - Luôn khuyên gặp bác sĩ dinh dưỡng cho các vấn đề phức tạp - Cảnh báo về các chế độ ăn kiêng cực đoan - Lưu ý về dị ứng, bệnh nền 💬 PHONG CÁCH: - Chuyên nghiệp, rõ ràng, súc tích - Dùng "tôi" để thể hiện tính chuyên môn - KHÔNG dùng emoji - Đưa ra con số cụ thể khi có thể - Thực tế, không lý thuyết suông - TỰ NHIÊN, MẠCH LẠC - không lặp lại ý, không copy-paste câu từ context khác - Nếu hỏi thông tin → Hỏi NGẮN GỌN, TRỰC TIẾP - KHÔNG dùng câu như "Bạn thử làm theo xem có đỡ không" (đây là câu của bác sĩ chữa bệnh!)""" def set_health_context(self, health_context: HealthContext): """Inject health context and initialize personalization engine""" self.health_context = health_context self.personalization = PersonalizationEngine(health_context) def handle(self, parameters, chat_history=None): """ Handle nutrition request using LLM for natural conversation Args: parameters (dict): { "user_query": str, "user_data": dict (optional) } chat_history (list): Conversation history Returns: str: Response message """ user_query = parameters.get("user_query", "") user_data = parameters.get("user_data", {}) # Extract and save user info from current message immediately self.extract_and_save_user_info(user_query) # Update memory from chat history if chat_history: self.update_memory_from_history(chat_history) # Check if we should hand off to another agent if self.should_handoff(user_query, chat_history): next_agent = self.suggest_next_agent(user_query) if next_agent: # Save current nutrition data for next agent self.save_agent_data('last_nutrition_advice', { 'query': user_query, 'user_profile': self.get_user_profile(), 'timestamp': datetime.now().isoformat() }) # Create handoff message with context context = self._generate_nutrition_summary() return self.create_handoff_message(next_agent, context, user_query) # Use health context if available if self.health_context: profile = self.health_context.get_user_profile() user_data = { 'age': profile.age, 'gender': profile.gender, 'weight': profile.weight, 'height': profile.height, 'activity_level': profile.activity_level, 'health_conditions': profile.health_conditions, 'dietary_restrictions': profile.dietary_restrictions } # Extract user data from chat history if not provided elif not user_data and chat_history: user_data = self._extract_user_data_from_history(chat_history) # Save extracted data to shared memory for other agents for key, value in user_data.items(): if value is not None: self.update_user_profile(key, value) # Check if user needs personalized advice (BMI, calories, meal plan) needs_personalization = self._needs_personalized_advice(user_query, chat_history) if needs_personalization: # Check if we have enough data missing_fields = self._check_missing_data(user_data) if missing_fields: return self._ask_for_missing_data(missing_fields, user_data, user_query) # Generate personalized nutrition advice try: result = self.advisor.generate_nutrition_advice(user_data) # Adapt recommendations using personalization engine if self.personalization: adapted_result = self.personalization.adapt_nutrition_plan(result) else: adapted_result = result response = self._format_nutrition_response(adapted_result, user_data) # Persist data to health context if self.health_context: self.health_context.add_health_record('nutrition', { 'query': user_query, 'advice': response, 'user_data': user_data, 'timestamp': datetime.now().isoformat() }) return response except Exception as e: return self._handle_error(e, user_query) else: # General nutrition question - use LLM directly response = self._handle_general_nutrition_query(user_query, chat_history) # Persist general query if self.health_context: self.health_context.add_health_record('nutrition', { 'query': user_query, 'response': response, 'type': 'general', 'timestamp': datetime.now().isoformat() }) return response def _extract_user_data_from_history(self, chat_history): """Extract user data from conversation history""" user_data = { 'age': None, 'gender': None, 'weight': None, 'height': None, 'goal': 'maintenance', 'activity_level': 'moderate', 'dietary_restrictions': [], 'health_conditions': [] } all_messages = " ".join([msg[0] for msg in chat_history if msg[0]]) # Extract age age_match = re.search(r'(\d+)\s*tuổi|tuổi\s*(\d+)|tôi\s*(\d+)', all_messages.lower()) if age_match: user_data['age'] = int([g for g in age_match.groups() if g][0]) # Extract gender if re.search(r'\bnam\b|male|đàn ông', all_messages.lower()): user_data['gender'] = 'male' elif re.search(r'\bnữ\b|female|đàn bà', all_messages.lower()): user_data['gender'] = 'female' # Extract weight - improved patterns weight_match = re.search(r'(?:nặng|cân|weight)?\s*(\d+(?:\.\d+)?)\s*kg|(\d+(?:\.\d+)?)\s*kg', all_messages.lower()) if weight_match: user_data['weight'] = float([g for g in weight_match.groups() if g][0]) # Extract height - improved patterns height_cm_match = re.search(r'(?:cao|chiều cao|height)?\s*(\d+(?:\.\d+)?)\s*cm', all_messages.lower()) if height_cm_match: user_data['height'] = float(height_cm_match.group(1)) else: height_m_match = re.search(r'(?:cao|chiều cao|height)?\s*(\d+\.?\d*)\s*m\b', all_messages.lower()) if height_m_match: height = float(height_m_match.group(1)) if height < 3: # Convert meters to cm height = height * 100 user_data['height'] = height # Extract goal if re.search(r'giảm cân|weight loss', all_messages.lower()): user_data['goal'] = 'weight_loss' elif re.search(r'tăng cân|weight gain', all_messages.lower()): user_data['goal'] = 'weight_gain' elif re.search(r'tập gym|muscle|cơ bắp', all_messages.lower()): user_data['goal'] = 'muscle_building' return user_data def _needs_personalized_advice(self, user_query, chat_history): """ Determine if user needs personalized advice (BMI, calories, meal plan) or just general nutrition info """ # Keywords that indicate need for personalization personalization_keywords = [ 'giảm cân', 'tăng cân', 'bmi', 'calo', 'calorie', 'thực đơn', 'meal plan', 'chế độ ăn cá nhân', 'tôi nên ăn gì', 'tư vấn cho tôi', 'phù hợp với tôi' ] query_lower = user_query.lower() # Check if user explicitly asks for personalized advice if any(kw in query_lower for kw in personalization_keywords): return True # Check chat history - if user already provided personal info if chat_history: all_messages = " ".join([msg[0] for msg in chat_history if msg[0]]).lower() if any(kw in all_messages for kw in personalization_keywords): return True # Default: general question return False def _check_missing_data(self, user_data): """Check what data is missing - check shared memory first""" required = ['age', 'gender', 'weight', 'height'] # Check shared memory for missing fields profile = self.get_user_profile() for field in required: if not user_data.get(field) and profile.get(field): user_data[field] = profile[field] return [field for field in required if not user_data.get(field)] def _ask_for_missing_data(self, missing_fields, current_data, user_query): """Ask for missing data""" questions = { 'age': "bạn bao nhiêu tuổi", 'gender': "bạn là nam hay nữ", 'weight': "bạn nặng bao nhiêu kg", 'height': "bạn cao bao nhiêu cm" } # Build friendly question q_list = [questions[f] for f in missing_fields] if len(q_list) == 1: question = q_list[0] elif len(q_list) == 2: question = f"{q_list[0]} và {q_list[1]}" else: question = ", ".join(q_list[:-1]) + f" và {q_list[-1]}" return f"""🥗 **Để tư vấn dinh dưỡng chính xác, mình cần biết thêm:** Cho mình biết {question} nhé? 💡 **Ví dụ:** "Tôi 25 tuổi, nam, nặng 70kg, cao 175cm" Sau khi có đủ thông tin, mình sẽ tính BMI và đưa ra lời khuyên dinh dưỡng cá nhân hóa cho bạn! 😊""" def _format_nutrition_response(self, result, user_data): """Format nutrition advice into friendly response""" bmi_info = result['bmi_analysis'] targets = result['daily_targets'] meals = result['meal_suggestions'] supplements = result['supplement_recommendations'] response = f"""🥗 **Tư Vấn Dinh Dưỡng Cá Nhân Hóa** 👤 **Thông tin của bạn:** - {user_data['age']} tuổi, {user_data['gender']}, {user_data['weight']}kg, {user_data['height']}cm 📊 **Phân tích BMI:** - BMI: **{bmi_info['bmi']}** ({bmi_info['category']}) - Lời khuyên: {bmi_info['advice']} 🎯 **Mục tiêu hàng ngày:** - 🔥 Calo: **{targets['daily_calories']} kcal** - 🥩 Protein: **{targets['protein']}** - 🍚 Carb: **{targets['carbs']}** - 🥑 Chất béo: **{targets['fats']}** - 💧 Nước: **{targets['water']}** 🍽️ **Gợi ý thực đơn:** **Sáng:** - {meals['breakfast'][0]} - {meals['breakfast'][1]} **Trưa:** - {meals['lunch'][0]} - {meals['lunch'][1]} **Tối:** - {meals['dinner'][0]} - {meals['dinner'][1]} **Snack:** - {meals['snacks'][0]} - {meals['snacks'][1]} """ if supplements: response += f"\n💊 **Thực phẩm bổ sung gợi ý:**\n" response += "\n".join([f"- {s}" for s in supplements[:4]]) response += f""" 🤖 **Lời khuyên chuyên gia:** {result['personalized_advice'][:600]}... --- ⚠️ *Đây là tư vấn tham khảo. Với các vấn đề phức tạp, hãy gặp bác sĩ dinh dưỡng nhé!* 💬 Bạn có câu hỏi gì về chế độ ăn này không? Hoặc muốn mình điều chỉnh gì không? 😊""" return response def _build_nutrition_context_instruction(self, user_query, chat_history): """ Build context instruction for nutrition queries """ # Check if user is answering comparison self-assessment if chat_history and len(chat_history) > 0: last_bot_msg = chat_history[-1][1] if len(chat_history[-1]) > 1 else "" if "TỰ KIỂM TRA" in last_bot_msg or "Bạn trả lời" in last_bot_msg: return """\n\nPHASE: PHÂN TÍCH LỰA CHỌN DINH DƯỠNG User vừa trả lời các câu hỏi. Phân tích: 1. NHẬN DIỆN PHÙ HỢP (dựa vào RAG): - Đọc kỹ mục tiêu, lifestyle, sở thích - So sánh với đặc điểm của từng lựa chọn - Đưa ra lựa chọn PHÙ HỢP NHẤT 2. GIẢI THÍCH: - Vì sao lựa chọn này phù hợp - Lợi ích cụ thể cho user - Lưu ý khi thực hiện 3. HƯỚNG DẪN BẮT ĐẦU: - Cách bắt đầu cụ thể - Thực đơn mẫu (nếu cần) - Tips để duy trì 4. Kết thúc: "Bạn cần hướng dẫn chi tiết hơn không?" KHÔNG nói "Dựa trên thông tin".""" # Check if asking comparison question if any(phrase in user_query.lower() for phrase in [ 'nên ăn', 'hay', 'hoặc', 'khác nhau thế nào', 'chọn', 'so sánh', 'tốt hơn' ]): return """\n\nPHASE: SO SÁNH DINH DƯỠNG (GENERIC) User muốn so sánh các lựa chọn dinh dưỡng. Sử dụng RAG để: 1. XÁC ĐỊNH các lựa chọn (từ user query): - Trích xuất diets/foods user đề cập - Hoặc tìm các lựa chọn liên quan 2. TẠO BẢNG SO SÁNH: Format: **[Lựa chọn A]:** • Macros: [protein/carb/fat] • Ưu điểm: [benefits] • Nhược điểm: [drawbacks] • Phù hợp cho: [who] **[Lựa chọn B]:** • Macros: [protein/carb/fat] • Ưu điểm: [benefits] • Nhược điểm: [drawbacks] • Phù hợp cho: [who] **Điểm khác biệt chính:** [key differences] 3. CÂU HỊI TỰ KIỂM TRA: Tạo 3-5 câu hỏi giúp user tự đánh giá: • Mục tiêu của bạn? • Lifestyle như thế nào? • Có hạn chế gì không? • Thời gian chuẩn bị? 4. Kết thúc: "Bạn trả lời giúp mình để recommend phù hợp nhé!" QUAN TRỌNG: Dùng RAG knowledge, KHÔNG hard-code.""" # Normal advice return """\n\nĐưa ra lời khuyên dinh dưỡng cụ thể, thực tế. KHÔNG quá lý thuyết. KHÔNG nói "Dựa trên thông tin".""" def _handle_general_nutrition_query(self, user_query, chat_history): """Handle general nutrition questions using LLM + RAG with comparison support""" from config.settings import client, MODEL try: # Smart RAG - only query when needed (inherit from BaseAgent) rag_answer = '' rag_sources = [] if self.should_use_rag(user_query, chat_history): rag_result = self.rag.query_nutrition(user_query) rag_answer = rag_result.get('answer', '') rag_sources = rag_result.get('source_docs', []) # Build conversation context with RAG context rag_context = f"Dựa trên kiến thức từ cơ sở dữ liệu:\n{rag_answer}\n\n" if rag_answer else "" messages = [{"role": "system", "content": self.system_prompt}] # Add RAG context if available if rag_context: messages.append({"role": "system", "content": f"Thông tin tham khảo từ cơ sở dữ liệu:\n{rag_context}"}) # Add chat history (last 5 exchanges) if chat_history: recent_history = chat_history[-5:] if len(chat_history) > 5 else chat_history for user_msg, bot_msg in recent_history: if user_msg: messages.append({"role": "user", "content": user_msg}) if bot_msg: messages.append({"role": "assistant", "content": bot_msg}) # Add current query with context instruction context_prompt = self._build_nutrition_context_instruction(user_query, chat_history) messages.append({"role": "user", "content": user_query + context_prompt}) # Get LLM response response = client.chat.completions.create( model=MODEL, messages=messages, temperature=0.7, max_tokens=500 ) llm_response = response.choices[0].message.content # Add sources using RAG integration formatter (FIXED!) if rag_sources: formatted_response = self.rag.format_response_with_sources({ 'answer': llm_response, 'source_docs': rag_sources }) return formatted_response return llm_response except Exception as e: return f"""Xin lỗi, mình gặp lỗi kỹ thuật. Bạn có thể: 1. Thử lại câu hỏi 2. Hỏi cách khác 3. Liên hệ hỗ trợ Chi tiết lỗi: {str(e)}""" def should_handoff(self, user_query: str, chat_history: Optional[List] = None) -> bool: """ Override base method - Determine if should hand off to another agent Specific triggers for nutrition agent: - User asks about exercise/workout - User mentions symptoms (stomach pain, nausea) - User asks about mental health affecting eating """ query_lower = user_query.lower() # Check each agent's triggers for agent, triggers in self.handoff_triggers.items(): if any(trigger in query_lower for trigger in triggers): # Don't handoff if we're in the middle of nutrition consultation if chat_history and self._is_mid_consultation(chat_history): return False return True return False def suggest_next_agent(self, user_query: str) -> Optional[str]: """Override base method - Suggest which agent to hand off to based on query""" query_lower = user_query.lower() # Priority order for handoff if any(trigger in query_lower for trigger in self.handoff_triggers.get('symptom_agent', [])): return 'symptom_agent' if any(trigger in query_lower for trigger in self.handoff_triggers.get('exercise_agent', [])): return 'exercise_agent' if any(trigger in query_lower for trigger in self.handoff_triggers.get('mental_health_agent', [])): return 'mental_health_agent' if any(trigger in query_lower for trigger in self.handoff_triggers.get('general_health_agent', [])): return 'general_health_agent' return None def _is_mid_consultation(self, chat_history: List) -> bool: """Check if we're in the middle of nutrition consultation""" if not chat_history or len(chat_history) < 2: return False # Check last bot response last_bot_response = chat_history[-1][1] if len(chat_history[-1]) > 1 else "" # If we just asked for user data, don't handoff if any(phrase in last_bot_response for phrase in [ "cân nặng", "chiều cao", "tuổi", "giới tính", "mục tiêu" ]): return True return False def _generate_nutrition_summary(self) -> str: """Generate summary of nutrition advice for handoff""" nutrition_data = self.get_agent_data('nutrition_plan') user_profile = self.get_user_profile() # Natural summary without robotic prefix summary_parts = [] if nutrition_data and isinstance(nutrition_data, dict): if 'bmi_analysis' in nutrition_data: bmi = nutrition_data['bmi_analysis'] summary_parts.append(f"BMI: {bmi.get('bmi', 'N/A')} ({bmi.get('category', 'N/A')})") if 'daily_targets' in nutrition_data: targets = nutrition_data['daily_targets'] summary_parts.append(f"Calo: {targets.get('calories', 'N/A')} kcal/ngày") if user_profile and user_profile.get('goal'): summary_parts.append(f"Mục tiêu: {user_profile['goal']}") return " | ".join(summary_parts)[:100] if summary_parts else "" def _handle_error(self, error, user_query): """Handle errors gracefully""" return f"""Xin lỗi, mình gặp chút vấn đề khi tạo tư vấn dinh dưỡng. 😅 Lỗi: {str(error)} Bạn có thể thử: 1. Cung cấp lại thông tin: tuổi, giới tính, cân nặng, chiều cao 2. Hỏi câu hỏi cụ thể hơn về dinh dưỡng 3. Hoặc mình có thể tư vấn về chủ đề sức khỏe khác Bạn muốn thử lại không? 💙"""