my-gradio-app / agents /specialized /nutrition_agent.py
Nguyen Trong Lap
Recreate history without binary blobs
eeb0f9c
"""
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]}{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? 💙"""