Spaces:
Runtime error
Runtime error
File size: 24,014 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 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 |
"""
Base Agent - Parent class for all specialized agents
Provides shared functionality: memory access, handoff logic, coordination
"""
from typing import Dict, Any, Optional, List
from utils.memory import ConversationMemory
class BaseAgent:
"""
Base class for all agents
Provides common functionality and interface
"""
def __init__(self, memory: Optional[ConversationMemory] = None):
"""
Initialize base agent
Args:
memory: Shared conversation memory (optional)
"""
self.memory = memory or ConversationMemory()
self.agent_name = self.__class__.__name__.replace('Agent', '').lower()
self.system_prompt = ""
# Handoff configuration
self.can_handoff = True
self.handoff_triggers = []
# ===== Core Interface =====
def handle(self, parameters: Dict[str, Any], chat_history: Optional[List] = None) -> str:
"""
Handle user request (must be implemented by subclasses)
Args:
parameters: Request parameters from router
chat_history: Conversation history
Returns:
str: Response message
"""
raise NotImplementedError("Subclasses must implement handle()")
# ===== Memory Access Helpers =====
def get_user_profile(self) -> Dict[str, Any]:
"""Get complete user profile from memory"""
return self.memory.get_full_profile()
# ===== Smart RAG Helper =====
def should_use_rag(self, user_query: str, chat_history: Optional[List] = None) -> bool:
"""
Smart RAG Decision - Skip RAG for simple queries to improve performance
Performance Impact:
- Simple queries: 2-3s (was 8-10s) - 3x faster
- Complex queries: 6-8s (was 8-10s) - 1.3x faster
Args:
user_query: User's message
chat_history: Conversation history
Returns:
bool: True if RAG needed, False for simple conversational queries
"""
query_lower = user_query.lower().strip()
# 1. Greetings & acknowledgments (no RAG needed)
greetings = [
'xin chào', 'hello', 'hi', 'chào', 'hey',
'cảm ơn', 'thanks', 'thank you', 'tks',
'ok', 'được', 'vâng', 'ừ', 'uhm', 'uh huh',
'bye', 'tạm biệt', 'hẹn gặp lại'
]
if any(g in query_lower for g in greetings):
return False
# 2. Very short responses (usually conversational)
if len(query_lower) < 10:
short_responses = ['có', 'không', 'rồi', 'ạ', 'dạ', 'yes', 'no', 'nope', 'yep']
if any(r == query_lower or query_lower.startswith(r + ' ') for r in short_responses):
return False
# 3. Meta questions about the bot (no RAG needed)
meta_questions = [
'bạn là ai', 'bạn tên gì', 'bạn có thể', 'bạn làm gì',
'who are you', 'what can you', 'what do you'
]
if any(m in query_lower for m in meta_questions):
return False
# 4. Complex medical/health questions (NEED RAG)
complex_patterns = [
# Medical terms
'nguyên nhân', 'tại sao', 'why', 'how', 'làm sao',
'cách nào', 'phương pháp', 'điều trị', 'chữa',
'thuốc', 'medicine', 'phòng ngừa', 'prevention',
'biến chứng', 'complication', 'nghiên cứu', 'research',
# Specific diseases
'bệnh', 'disease', 'viêm', 'ung thư', 'cancer',
'tiểu đường', 'diabetes', 'huyết áp', 'blood pressure',
# Detailed questions
'chi tiết', 'cụ thể', 'specific', 'detail',
'khoa học', 'scientific', 'evidence', 'hướng dẫn',
'guideline', 'recommendation', 'chuyên gia', 'expert'
]
if any(p in query_lower for p in complex_patterns):
return True
# 5. Default: Simple first-turn questions don't need RAG
# Agent can ask clarifying questions first
if not chat_history or len(chat_history) == 0:
# Simple initial statements
simple_starts = [
'tôi muốn', 'tôi cần', 'giúp tôi', 'tôi bị',
'i want', 'i need', 'help me', 'i have', 'i feel'
]
if any(s in query_lower for s in simple_starts):
# Let agent gather info first, use RAG later
return False
# 6. Default: Use RAG for safety (medical context)
return True
def update_user_profile(self, key: str, value: Any) -> None:
"""Update user profile in shared memory"""
self.memory.update_profile(key, value)
def get_missing_profile_fields(self, required_fields: List[str]) -> List[str]:
"""Check what profile fields are missing"""
return self.memory.get_missing_fields(required_fields)
def save_agent_data(self, key: str, value: Any) -> None:
"""Save agent-specific data to memory"""
self.memory.add_agent_data(self.agent_name, key, value)
def get_agent_data(self, key: str = None) -> Any:
"""Get agent-specific data from memory"""
return self.memory.get_agent_data(self.agent_name, key)
def get_other_agent_data(self, agent_name: str, key: str = None) -> Any:
"""Get data from another agent"""
return self.memory.get_agent_data(agent_name, key)
# ===== Context Awareness =====
def get_context_summary(self) -> str:
"""Get summary of current conversation context"""
return self.memory.get_context_summary()
def get_previous_agent(self) -> Optional[str]:
"""Get name of previous agent"""
return self.memory.get_previous_agent()
def get_current_topic(self) -> Optional[str]:
"""Get current conversation topic"""
return self.memory.get_current_topic()
def set_current_topic(self, topic: str) -> None:
"""Set current conversation topic"""
self.memory.set_current_topic(topic)
def generate_natural_opening(self, user_query: str, chat_history: Optional[List] = None) -> str:
"""
Generate natural conversation opening based on context
Avoids robotic prefixes like "Thông tin đã tư vấn:"
Args:
user_query: Current user query
chat_history: Conversation history
Returns:
str: Natural opening phrase (empty if not needed)
"""
# Check if this is a topic transition
previous_agent = self.get_previous_agent()
is_new_topic = previous_agent and previous_agent != self.agent_name
# If continuing same topic, no special opening needed
if not is_new_topic:
return ""
# Generate natural transition based on agent type
query_lower = user_query.lower()
# Enthusiastic transitions for new requests
if any(word in query_lower for word in ['muốn', 'cần', 'giúp', 'tư vấn']):
openings = [
"Ah, bây giờ bạn đang cần",
"Được rồi, để mình",
"Tuyệt! Mình sẽ",
"Ok, cùng",
]
import random
return random.choice(openings) + " "
# Default: no prefix, just natural response
return ""
# ===== Handoff Logic =====
def should_handoff(self, user_query: str, chat_history: Optional[List] = None) -> bool:
"""
Determine if this agent should hand off to another agent
Args:
user_query: User's current query
chat_history: Conversation history
Returns:
bool: True if handoff is needed
"""
if not self.can_handoff:
return False
# Check for handoff trigger keywords
query_lower = user_query.lower()
for trigger in self.handoff_triggers:
if trigger in query_lower:
return True
return False
def suggest_next_agent(self, user_query: str) -> Optional[str]:
"""
Suggest which agent to hand off to
Args:
user_query: User's current query
Returns:
str: Name of suggested agent, or None
"""
query_lower = user_query.lower()
# Symptom keywords
symptom_keywords = ['đau', 'sốt', 'ho', 'buồn nôn', 'chóng mặt', 'mệt']
if any(kw in query_lower for kw in symptom_keywords):
return 'symptom_agent'
# Nutrition keywords
nutrition_keywords = ['ăn', 'thực đơn', 'calo', 'giảm cân', 'tăng cân']
if any(kw in query_lower for kw in nutrition_keywords):
return 'nutrition_agent'
# Exercise keywords
exercise_keywords = ['tập', 'gym', 'cardio', 'yoga', 'chạy bộ']
if any(kw in query_lower for kw in exercise_keywords):
return 'exercise_agent'
# Mental health keywords
mental_keywords = ['stress', 'lo âu', 'trầm cảm', 'mất ngủ', 'burnout']
if any(kw in query_lower for kw in mental_keywords):
return 'mental_health_agent'
return None
def create_handoff_message(self, next_agent: str, context: str = "", user_query: str = "") -> str:
"""
Create a SEAMLESS topic transition (not explicit handoff)
Args:
next_agent: Name of agent to hand off to
context: Additional context for handoff
user_query: User's query to understand intent
Returns:
str: Natural transition message (NOT "chuyển sang chuyên gia")
"""
# Map agents to topic areas
topic_map = {
'symptom_agent': {
'topic': 'triệu chứng',
'action': 'đánh giá',
'info_needed': ['triệu chứng cụ thể', 'thời gian xuất hiện']
},
'nutrition_agent': {
'topic': 'dinh dưỡng',
'action': 'tư vấn chế độ ăn',
'info_needed': ['mục tiêu', 'cân nặng', 'chiều cao', 'tuổi']
},
'exercise_agent': {
'topic': 'tập luyện',
'action': 'lên lịch tập',
'info_needed': ['mục tiêu', 'thời gian có thể tập', 'thiết bị']
},
'mental_health_agent': {
'topic': 'sức khỏe tinh thần',
'action': 'hỗ trợ',
'info_needed': ['cảm giác hiện tại', 'thời gian kéo dài']
}
}
topic_info = topic_map.get(next_agent, {
'topic': 'vấn đề này',
'action': 'tư vấn',
'info_needed': []
})
# SEAMLESS transition - acknowledge topic change naturally
message = f"{context}\n\n" if context else ""
# Natural acknowledgment based on query
if 'tập' in user_query.lower() or 'gym' in user_query.lower():
message += f"Ah, bây giờ bạn đang cần về {topic_info['topic']}! "
elif 'ăn' in user_query.lower() or 'thực đơn' in user_query.lower():
message += f"Okii, giờ chuyển sang {topic_info['topic']} nhé! "
else:
message += f"Được, mình giúp bạn về {topic_info['topic']}! "
# Ask for info if needed (natural, not formal)
if topic_info['info_needed']:
info_list = ', '.join(topic_info['info_needed'][:2]) # Max 2 items
message += f"Để {topic_info['action']} phù hợp, cho mình biết thêm về {info_list} nhé!"
return message
# ===== Multi-Agent Coordination =====
def needs_collaboration(self, user_query: str) -> List[str]:
"""
Determine if multiple agents are needed
Args:
user_query: User's query
Returns:
List[str]: List of agent names needed
"""
agents_needed = []
query_lower = user_query.lower()
# Check for each agent's keywords
if any(kw in query_lower for kw in ['đau', 'sốt', 'ho', 'triệu chứng']):
agents_needed.append('symptom_agent')
if any(kw in query_lower for kw in ['ăn', 'thực đơn', 'calo', 'dinh dưỡng']):
agents_needed.append('nutrition_agent')
if any(kw in query_lower for kw in ['tập', 'gym', 'cardio', 'exercise']):
agents_needed.append('exercise_agent')
if any(kw in query_lower for kw in ['stress', 'lo âu', 'trầm cảm', 'mental']):
agents_needed.append('mental_health_agent')
return agents_needed
# ===== Utility Methods =====
def extract_user_data_from_history(self, chat_history: List) -> Dict[str, Any]:
"""
Extract user data from conversation history
(Can be overridden by subclasses for specific extraction)
Args:
chat_history: List of [user_msg, bot_msg] pairs
Returns:
Dict: Extracted user data
"""
import re
if not chat_history:
return {}
all_messages = " ".join([msg[0] for msg in chat_history if msg[0]])
extracted = {}
# 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:
extracted['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()):
extracted['gender'] = 'male'
elif re.search(r'\bnữ\b|female|đàn bà|phụ nữ', all_messages.lower()):
extracted['gender'] = 'female'
# Extract weight
weight_match = re.search(r'(\d+)\s*kg|nặng\s*(\d+)|cân\s*(\d+)', all_messages.lower())
if weight_match:
extracted['weight'] = float([g for g in weight_match.groups() if g][0])
# Extract height
height_match = re.search(r'(\d+)\s*cm|cao\s*(\d+)|chiều cao\s*(\d+)', all_messages.lower())
if height_match:
extracted['height'] = float([g for g in height_match.groups() if g][0])
return extracted
def update_memory_from_history(self, chat_history: List) -> None:
"""Extract and update memory from chat history"""
extracted = self.extract_user_data_from_history(chat_history)
for key, value in extracted.items():
# Always update with latest info (user may correct themselves)
self.memory.update_profile(key, value)
def extract_and_save_user_info(self, user_message: str) -> Dict[str, Any]:
"""
Extract user info from a single message using LLM (flexible, handles typos)
Saves to memory immediately
Args:
user_message: Single user message (any format, any order)
Returns:
Dict: Extracted data
"""
from config.settings import client, MODEL
import json
# Use LLM to extract structured data (handles typos, any order, extra info)
extraction_prompt = f"""Extract health information from this user message. Handle typos and variations.
User message: "{user_message}"
Extract these fields if present (return null if not found):
- age: integer (tuổi, age, years old)
- gender: "male" or "female" (nam, nữ, male, female, đàn ông, phụ nữ)
- weight: float in kg (nặng, cân, weight, kg)
- height: float in cm (cao, chiều cao, height, cm, m)
IMPORTANT: Height MUST be in cm (50-300 range)
- If user says "1.75m" or "1.78m" → convert to cm (175, 178)
- If user says "175cm" or "178cm" → use as is (175, 178)
- NEVER return values like 1.0, 1.5, 1.75 for height!
- body_fat_percentage: float (tỉ lệ mỡ, body fat, %, optional)
Return ONLY valid JSON with these exact keys. Example:
{{"age": 30, "gender": "male", "weight": 70.5, "height": 175, "body_fat_percentage": 25}}
CRITICAL: Height must be 50-300 (in cm). If user says "1.78m", return 178, not 1.78!
If a field is not found, use null. Be flexible with typos and word order."""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "You are a data extraction assistant. Extract structured health data from user messages. Handle typos and variations. Return only valid JSON."},
{"role": "user", "content": extraction_prompt}
],
temperature=0.1, # Low temp for consistent extraction
max_tokens=150
)
result_text = response.choices[0].message.content.strip()
# Parse JSON response
# Remove markdown code blocks if present
if "```json" in result_text:
result_text = result_text.split("```json")[1].split("```")[0].strip()
elif "```" in result_text:
result_text = result_text.split("```")[1].split("```")[0].strip()
extracted = json.loads(result_text)
# Auto-correct obvious errors before saving
extracted = self._auto_correct_health_data(extracted)
# Save to memory (only non-null values)
allowed_fields = ['age', 'gender', 'weight', 'height', 'body_fat_percentage']
for key, value in extracted.items():
if value is not None and key in allowed_fields:
self.update_user_profile(key, value)
return {k: v for k, v in extracted.items() if v is not None}
except Exception as e:
# Fallback to regex if LLM fails
print(f"LLM extraction failed: {e}, using regex fallback")
return self._extract_with_regex_fallback(user_message)
def _auto_correct_health_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Auto-correct obvious errors in health data (typos, wrong units)
Examples:
- height: 200 → 200cm ✅ (likely meant 200cm, not 200m)
- height: 1.75 → 175cm ✅ (convert m to cm)
- weight: 75 → 75kg ✅ (assume kg if reasonable)
- weight: 75000 → 75kg ✅ (likely meant 75kg, not 75000g)
"""
corrected = data.copy()
# Auto-correct height
if 'height' in corrected and corrected['height'] is not None:
height = float(corrected['height'])
# If height is very small (< 10), likely in meters → convert to cm
if 0 < height < 10:
corrected['height'] = height * 100
print(f"Auto-corrected height: {height}m → {corrected['height']}cm")
# If height is reasonable (50-300), assume cm
elif 50 <= height <= 300:
corrected['height'] = height
# If height is very large (> 1000), likely in mm → convert to cm
elif height > 1000:
corrected['height'] = height / 10
print(f"Auto-corrected height: {height}mm → {corrected['height']}cm")
# Otherwise invalid, set to None
else:
print(f"Invalid height: {height}, setting to None")
corrected['height'] = None
# Auto-correct weight
if 'weight' in corrected and corrected['weight'] is not None:
weight = float(corrected['weight'])
# If weight is very large (> 500), likely in grams → convert to kg
if weight > 500:
corrected['weight'] = weight / 1000
print(f"Auto-corrected weight: {weight}g → {corrected['weight']}kg")
# If weight is reasonable (20-300), assume kg
elif 20 <= weight <= 300:
corrected['weight'] = weight
# If weight is very small (< 20), might be wrong unit
elif 0 < weight < 20:
# Could be in different unit or child weight
# Keep as is but flag
corrected['weight'] = weight
# Otherwise invalid
else:
print(f"Invalid weight: {weight}, setting to None")
corrected['weight'] = None
# Auto-correct age
if 'age' in corrected and corrected['age'] is not None:
age = int(corrected['age'])
# Reasonable age range: 1-120
if not (1 <= age <= 120):
print(f"Invalid age: {age}, setting to None")
corrected['age'] = None
# Auto-correct body fat percentage
if 'body_fat_percentage' in corrected and corrected['body_fat_percentage'] is not None:
bf = float(corrected['body_fat_percentage'])
# Reasonable body fat: 3-60%
if not (3 <= bf <= 60):
print(f"Invalid body fat: {bf}%, setting to None")
corrected['body_fat_percentage'] = None
return corrected
def _extract_with_regex_fallback(self, user_message: str) -> Dict[str, Any]:
"""Fallback regex extraction (less flexible but reliable)"""
import re
extracted = {}
msg_lower = user_message.lower()
# Extract age
age_match = re.search(r'(\d+)\s*tuổi|tuổi\s*(\d+)|age\s*(\d+)', msg_lower)
if age_match:
age = int([g for g in age_match.groups() if g][0])
extracted['age'] = age
self.update_user_profile('age', age)
# Extract gender
if re.search(r'\bnam\b|male|đàn ông', msg_lower):
extracted['gender'] = 'male'
self.update_user_profile('gender', 'male')
elif re.search(r'\bnữ\b|female|đàn bà|phụ nữ', msg_lower):
extracted['gender'] = 'female'
self.update_user_profile('gender', 'female')
# Extract weight
weight_match = re.search(r'(?:nặng|cân|weight)?\s*(\d+(?:\.\d+)?)\s*kg', msg_lower)
if weight_match:
weight = float(weight_match.group(1))
extracted['weight'] = weight
self.update_user_profile('weight', weight)
# Extract height
height_cm_match = re.search(r'(?:cao|chiều cao|height)?\s*(\d+(?:\.\d+)?)\s*cm', msg_lower)
if height_cm_match:
height = float(height_cm_match.group(1))
extracted['height'] = height
self.update_user_profile('height', height)
else:
height_m_match = re.search(r'(?:cao|chiều cao|height)?\s*(\d+\.?\d*)\s*m\b', msg_lower)
if height_m_match:
height = float(height_m_match.group(1))
if height < 3:
height = height * 100
extracted['height'] = height
self.update_user_profile('height', height)
return extracted
def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {self.get_context_summary()}>"
|