File size: 17,575 Bytes
eeb0f9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
"""
Exercise Agent - Specialized agent for exercise and fitness advice
"""

from config.settings import client, MODEL
from modules.exercise.exercise import generate_exercise_plan
from health_data import HealthContext
from fitness_tracking import FitnessTracker
from rag.rag_integration import get_rag_integration
from agents.core.base_agent import BaseAgent
from typing import Dict, Any, List, Optional
from datetime import datetime
import re

class ExerciseAgent(BaseAgent):
    def __init__(self, memory=None):
        super().__init__(memory)
        self.health_context = None
        self.fitness_tracker = None
        self.rag = get_rag_integration()
        
        # Configure handoff triggers for exercise agent
        self.handoff_triggers = {
            'nutrition_agent': ['ăn gì', 'thực đơn', 'calo', 'dinh dưỡng', 'giảm cân nhanh', 'tăng cân'],
            'symptom_agent': ['đau', 'chấn thương', 'bị thương', 'sưng', 'viêm'],
            'mental_health_agent': ['stress', 'lo âu', 'không có động lực', 'chán'],
            'general_health_agent': ['khám', 'bác sĩ', 'xét nghiệm']
        }
        self.system_prompt = """Bạn là huấn luyện viên cá nhân chuyên nghiệp, nhiệt huyết và động viên.

💪 CHUYÊN MÔN:
- Tạo kế hoạch tập luyện cá nhân hóa
- Tư vấn bài tập phù hợp với thể trạng, mục tiêu
- Hướng dẫn kỹ thuật tập an toàn
- Tư vấn tập cho người có bệnh nền
- Lịch tập gym, tập tại nhà, cardio, yoga...

🎯 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 30 tuổi, nam, muốn giảm cân, có thể tập 45 phút/ngày" → ĐỪNG HỎI LẠI!
   - Chỉ hỏi thông tin THỰC SỰ còn thiếu
   - Nếu đã đủ thông tin cơ bản → TẠO LỊCH TẬP NGAY!

2. **THÔNG TIN CẦN THIẾT:**
   - Cơ bản: Tuổi, giới tính, mục tiêu, thời gian rảnh
   - Bổ sung: Thể lực, dụng cụ có sẵn, bệnh nền
   - Nếu thiếu → Hỏi ngắn gọn, không hỏi mãi

3. **TẠO LỊCH TẬP:**
   - Lịch tập cụ thể theo ngày
   - Giải thích TẠI SAO tập bài này
   - Hướng dẫn progression (tuần 1, 2, 3...)
   - Lưu ý an toàn, tránh chấn thương

⚠️ AN TOÀN:
- Người có bệnh tim, huyết áp → khuyên gặp bác sĩ trước
- Người có chấn thương → tập nhẹ, tránh vùng bị thương
- Người mới bắt đầu → từ từ, không quá sức

💬 PHONG CÁCH:
- Động viên, khích lệ 💪🔥
- Thực tế, không lý thuyết suông
- Dễ hiểu, dễ làm theo
- Hài hước nhẹ nhà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ĩ, không phải PT!)"""

    def set_health_context(self, health_context: HealthContext):
        """Inject health context and initialize fitness tracker"""
        self.health_context = health_context
        self.fitness_tracker = FitnessTracker(health_context)

    def handle(self, parameters, chat_history=None):
        """
        Handle exercise request

        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 exercise data for next agent
                self.save_agent_data('last_exercise_advice', {
                    'query': user_query,
                    'user_profile': self.get_user_profile(),
                    'timestamp': datetime.now().isoformat()
                })
                
                # Check if nutrition agent shared data with us
                nutrition_data = self.get_other_agent_data('nutrition_agent', 'nutrition_plan')
                context = self._generate_exercise_summary(nutrition_data)
                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,
                'fitness_level': profile.fitness_level,
                'activity_level': profile.activity_level,
                'health_conditions': profile.health_conditions
            }
        # 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 we have enough data - check shared memory first
        profile = self.get_user_profile()
        for field in ['age', 'gender', 'weight', 'height']:
            if not user_data.get(field) and profile.get(field):
                user_data[field] = profile[field]
        
        missing_fields = self._check_missing_data(user_data)

        if missing_fields:
            return self._ask_for_missing_data(missing_fields, user_data)

        # Generate exercise plan
        try:
            plan = generate_exercise_plan(user_data)

            # Adjust difficulty based on fitness tracker
            if self.fitness_tracker:
                metrics = self.fitness_tracker.calculate_progress_metrics()
                if metrics.get('adherence', 0) > 0.8:
                    plan = self.fitness_tracker.adjust_difficulty(plan, 'increase')
                elif metrics.get('adherence', 0) < 0.5:
                    plan = self.fitness_tracker.adjust_difficulty(plan, 'decrease')

            response = plan

            # Persist workout plan to health context
            if self.health_context:
                self.health_context.add_health_record('exercise', {
                    'query': user_query,
                    'plan': response,
                    'user_data': user_data,
                    'timestamp': datetime.now().isoformat()
                })

            return response
        except Exception as e:
            return self._handle_error(e, user_query)
    
    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,
            'fitness_level': 'beginner',
            'goal': 'health_improvement',
            'available_time': 30,
            '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 fitness level
        if re.search(r'mới bắt đầu|beginner|chưa tập', all_messages.lower()):
            user_data['fitness_level'] = 'beginner'
        elif re.search(r'trung bình|intermediate|tập được', all_messages.lower()):
            user_data['fitness_level'] = 'intermediate'
        elif re.search(r'nâng cao|advanced|tập lâu', all_messages.lower()):
            user_data['fitness_level'] = 'advanced'
        
        # Extract goal
        if re.search(r'giảm cân|weight loss|slim', all_messages.lower()):
            user_data['goal'] = 'weight_loss'
        elif re.search(r'tăng cân|weight gain|bulk', all_messages.lower()):
            user_data['goal'] = 'weight_gain'
        elif re.search(r'tập gym|muscle|cơ bắp|tăng cơ', all_messages.lower()):
            user_data['goal'] = 'muscle_building'
        elif re.search(r'khỏe mạnh|health|sức khỏe', all_messages.lower()):
            user_data['goal'] = 'health_improvement'
        
        # Extract available time
        time_match = re.search(r'(\d+)\s*phút|(\d+)\s*tiếng', all_messages.lower())
        if time_match:
            time_val = int([g for g in time_match.groups() if g][0])
            if 'tiếng' in all_messages.lower():
                time_val *= 60
            user_data['available_time'] = time_val
        
        return user_data
    
    def _check_missing_data(self, user_data):
        """Check what data is missing"""
        required = ['age', 'gender', 'fitness_level', 'goal']
        return [field for field in required if not user_data.get(field)]
    
    def _ask_for_missing_data(self, missing_fields, current_data):
        """Ask for missing data"""
        questions = {
            'age': "bạn bao nhiêu tuổi",
            'gender': "bạn là nam hay nữ",
            'fitness_level': "thể lực hiện tại của bạn thế nào (mới bắt đầu/trung bình/nâng cao)",
            'goal': "mục tiêu của bạn là gì (giảm cân/tăng cơ/khỏe mạnh hơn)"
        }
        
        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ạo lịch tập phù hợp, mình cần biết thêm:**

Cho mình biết {question} nhé?

💡 **Ví dụ:** "Tôi 30 tuổi, nam, mới bắt đầu tập, muốn giảm cân, có thể tập 45 phút mỗi ngày"

Sau khi có đủ thông tin, mình sẽ tạo kế hoạch tập luyện 7 ngày chi tiết cho bạn! 🔥"""
    
    def _handle_general_exercise_query(self, user_query, chat_history):
        """Handle general exercise questions using LLM + RAG"""
        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_exercise(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
            messages.append({"role": "user", "content": user_query})

            # 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. Hoặc hỏi mình về chủ đề sức khỏe khác nhé! 💙

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 exercise agent:
        - User asks about nutrition/diet
        - User mentions pain/injury
        - User asks about mental health
        """
        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 exercise planning
                if chat_history and self._is_mid_planning(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"""
        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('nutrition_agent', [])):
            return 'nutrition_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_planning(self, chat_history: List) -> bool:
        """Check if we're in the middle of exercise planning"""
        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 [
            "tuổi", "giới tính", "mục tiêu", "thời gian", "dụng cụ"
        ]):
            return True
        
        return False
    
    def _generate_exercise_summary(self, nutrition_data=None) -> str:
        """Generate summary of exercise advice for handoff"""
        exercise_data = self.get_agent_data('exercise_plan')
        user_profile = self.get_user_profile()
        
        # Natural summary without robotic prefix
        summary_parts = []
        
        if exercise_data and isinstance(exercise_data, dict):
            if 'goal' in exercise_data:
                summary_parts.append(f"Mục tiêu: {exercise_data['goal']}")
            if 'frequency' in exercise_data:
                summary_parts.append(f"Tần suất: {exercise_data['frequency']}")
        
        # Include nutrition data if available (agent-to-agent communication)
        if nutrition_data and isinstance(nutrition_data, dict):
            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('fitness_level'):
            summary_parts.append(f"Thể lực: {user_profile['fitness_level']}")
        
        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 lịch tập. 😅

Lỗi: {str(error)}

Bạn có thể thử:
1. Cung cấp lại thông tin: tuổi, giới tính, thể lực, mục tiêu
2. Hỏi câu hỏi cụ thể hơn về tập luyện
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? 💙"""