Spaces:
Runtime error
Runtime error
File size: 28,731 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 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 |
"""
Agent Router - Routes user requests to appropriate specialized agents
Supports two routing strategies:
1. Embedding-based routing (primary) - Automatic, scalable
2. LLM-based routing (fallback) - Manual, explicit
"""
from config.settings import client, MODEL
from typing import List, Dict, Tuple, Optional
import numpy as np
# Try to import embedding model (optional)
try:
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
EMBEDDINGS_AVAILABLE = True
except ImportError:
EMBEDDINGS_AVAILABLE = False
print("[WARNING] sentence-transformers not installed. Using LLM-based routing only.")
print("Install with: pip install sentence-transformers scikit-learn")
# Define available functions/agents
AVAILABLE_FUNCTIONS = [
{
"name": "nutrition_agent",
"description": """Tư vấn dinh dưỡng và chế độ ăn uống:
- Tính BMI, calo, macro (protein/carb/fat)
- Lập thực đơn, meal plan
- Tư vấn thực phẩm nên ăn/tránh
- Giảm cân, tăng cân, tăng cơ
- Bổ sung dinh dưỡng, vitamin
KHÔNG dùng cho: triệu chứng bệnh (đau bụng, buồn nôn, tiêu chảy)
→ Triệu chứng bệnh → dùng symptom_agent""",
"parameters": {
"type": "object",
"properties": {
"user_query": {
"type": "string",
"description": "Câu hỏi của người dùng về dinh dưỡng"
},
"user_data": {
"type": "object",
"description": "Thông tin người dùng (tuổi, giới tính, cân nặng, chiều cao, mục tiêu)",
"properties": {
"age": {"type": "integer"},
"gender": {"type": "string"},
"weight": {"type": "number"},
"height": {"type": "number"},
"goal": {"type": "string"}
}
}
},
"required": ["user_query"]
}
},
{
"name": "exercise_agent",
"description": "Tư vấn tập luyện, lịch tập gym, bài tập thể dục, kế hoạch tập luyện, yoga, cardio",
"parameters": {
"type": "object",
"properties": {
"user_query": {
"type": "string",
"description": "Câu hỏi của người dùng về tập luyện"
},
"user_data": {
"type": "object",
"description": "Thông tin người dùng (tuổi, giới tính, thể lực, mục tiêu, thời gian)",
"properties": {
"age": {"type": "integer"},
"gender": {"type": "string"},
"fitness_level": {"type": "string"},
"goal": {"type": "string"},
"available_time": {"type": "integer"}
}
}
},
"required": ["user_query"]
}
},
{
"name": "symptom_agent",
"description": """CLINICAL SYMPTOM ASSESSMENT - Đánh giá triệu chứng bệnh CỤ THỂ:
✅ USE FOR (Specific symptoms):
- Pain: đau đầu, đau bụng, đau lưng, đau ngực, đau khớp
- Fever/Infection: sốt, ho, cảm cúm, viêm họng, viêm phổi
- Digestive: buồn nôn, nôn, tiêu chảy, táo bón, đầy hơi
- Neurological: chóng mặt, đau nửa đầu, mất thăng bằng
- Acute symptoms: triệu chứng đột ngột, bất thường, nghiêm trọng
✅ WHEN TO USE:
- User describes SPECIFIC symptom: "Tôi bị đau bụng"
- User feels sick/unwell: "Tôi không khỏe", "Tôi bị ốm"
- Medical concern: "Tôi sợ bị bệnh X"
❌ DO NOT USE FOR:
- General wellness: "Làm sao để khỏe?" → general_health_agent
- Prevention: "Phòng ngừa bệnh" → general_health_agent
- Lifestyle: "Sống khỏe mạnh" → general_health_agent
- Nutrition: "Nên ăn gì?" → nutrition_agent
- Exercise: "Tập gì?" → exercise_agent""",
"parameters": {
"type": "object",
"properties": {
"user_query": {
"type": "string",
"description": "Mô tả triệu chứng của người dùng"
},
"symptom_data": {
"type": "object",
"description": "Thông tin triệu chứng (onset, location, severity, duration)",
"properties": {
"symptom_type": {"type": "string"},
"duration": {"type": "string"},
"severity": {"type": "integer"},
"location": {"type": "string"}
}
}
},
"required": ["user_query"]
}
},
{
"name": "mental_health_agent",
"description": "Tư vấn sức khỏe tinh thần, stress, lo âu, trầm cảm, burnout, giấc ngủ, cảm xúc",
"parameters": {
"type": "object",
"properties": {
"user_query": {
"type": "string",
"description": "Câu hỏi về sức khỏe tinh thần"
},
"context": {
"type": "object",
"description": "Ngữ cảnh (công việc, gia đình, stress level)",
"properties": {
"stress_level": {"type": "string"},
"duration": {"type": "string"},
"triggers": {"type": "array", "items": {"type": "string"}}
}
}
},
"required": ["user_query"]
}
},
{
"name": "general_health_agent",
"description": """GENERAL WELLNESS & LIFESTYLE - Tư vấn sức khỏe TỔNG QUÁT:
✅ USE FOR (General health & wellness):
- Wellness: "Làm sao để khỏe mạnh?", "Sống khỏe"
- Prevention: "Phòng ngừa bệnh", "Tăng sức đề kháng"
- Lifestyle: "Lối sống lành mạnh", "Thói quen tốt"
- General advice: "Tư vấn sức khỏe", "Chăm sóc sức khỏe"
- Health education: "Kiến thức sức khỏe", "Hiểu về cơ thể"
- Check-ups: "Khám sức khỏe định kỳ", "Xét nghiệm gì?"
✅ WHEN TO USE:
- Broad health questions: "Tôi muốn khỏe hơn"
- No specific symptom: "Tư vấn sức khỏe tổng quát"
- Prevention focus: "Làm gì để không bị ốm?"
- Lifestyle optimization: "Cải thiện sức khỏe"
❌ DO NOT USE FOR:
- Specific symptoms: "Tôi bị đau bụng" → symptom_agent
- Nutrition details: "Lập thực đơn" → nutrition_agent
- Exercise plans: "Lịch tập gym" → exercise_agent
- Mental health: "Stress, lo âu" → mental_health_agent""",
"parameters": {
"type": "object",
"properties": {
"user_query": {
"type": "string",
"description": "Câu hỏi chung về sức khỏe"
}
},
"required": ["user_query"]
}
}
]
def route_to_agent(message, chat_history=None):
"""
Route user message to appropriate specialized agent using function calling
Args:
message (str): User's message
chat_history (list): Conversation history for context
Returns:
dict: {
"agent": str, # Agent name
"parameters": dict, # Extracted parameters
"confidence": float # Routing confidence (0-1)
}
"""
# Build context from chat history (increased from 3 to 10 for better context)
context = ""
last_agent = None
if chat_history:
recent_messages = chat_history[-10:] # Last 10 exchanges (was 3)
# Extract last agent from bot response
if recent_messages:
last_bot_msg = recent_messages[-1][1] if len(recent_messages[-1]) > 1 else ""
# Try to detect agent from debug info
if "Agent used:" in last_bot_msg:
import re
match = re.search(r'Agent used: `(\w+)`', last_bot_msg)
if match:
last_agent = match.group(1)
# Build context with turn numbers for clarity
context_lines = []
for i, (user_msg, bot_msg) in enumerate(recent_messages, 1):
# Truncate long messages
user_short = user_msg[:80] + "..." if len(user_msg) > 80 else user_msg
bot_short = bot_msg[:80] + "..." if len(bot_msg) > 80 else bot_msg
context_lines.append(f"Turn {i}:\n User: {user_short}\n Bot: {bot_short}")
context = "\n".join(context_lines)
# Create enhanced routing prompt with context awareness
routing_prompt = f"""Phân tích câu hỏi của người dùng và xác định agent phù hợp nhất.
LỊCH SỬ HỘI THOẠI (10 exchanges gần nhất):
{context if context else "Đây là câu hỏi đầu tiên"}
AGENT TRƯỚC ĐÓ: {last_agent if last_agent else "Chưa có"}
CÂU HỎI HIỆN TẠI: {message}
HƯỚNG DẪN QUAN TRỌNG:
1. **TRIỆU CHỨNG BỆNH CỤ THỂ → symptom_agent (ưu tiên cao nhất)**
- User MÔ TẢ triệu chứng CỤ THỂ: "tôi bị đau...", "tôi bị sốt", "buồn nôn"
- Ví dụ: "đau bụng", "đau đầu", "sốt cao", "ho ra máu", "chóng mặt"
- LUÔN ưu tiên symptom_agent khi có triệu chứng CỤ THỂ!
⚠️ EDGE CASES - KHÔNG PHẢI symptom_agent:
- "Làm sao để KHÔNG bị đau đầu?" → general_health_agent (phòng ngừa)
- "Ăn gì để hết đau bụng?" → nutrition_agent (dinh dưỡng)
- "Tập gì để hết đau lưng?" → exercise_agent (tập luyện)
- "Làm sao để khỏe?" → general_health_agent (tổng quát)
2. **DINH DƯỠNG → nutrition_agent**
- Hỏi về thực phẩm, chế độ ăn, calo, BMI, thực đơn
- KHÔNG phải triệu chứng bệnh
- Ví dụ: "nên ăn gì", "giảm cân", "thực đơn"
3. **TẬP LUYỆN → exercise_agent**
- Hỏi về bài tập, lịch tập, gym, cardio, dụng cụ tập
- Follow-up về giáo án tập: "không có dụng cụ", "tập tại nhà", "không có tạ"
- Ví dụ: "nên tập gì", "lịch tập gym", "không có dụng cụ gym"
- **QUAN TRỌNG:** Nếu đang nói về tập luyện → TIẾP TỤC exercise_agent
4. **SỨC KHỎE TINH THẦN → mental_health_agent**
- Stress, lo âu, trầm cảm, burnout, giấc ngủ
- Ví dụ: "tôi stress", "lo âu", "mất ngủ"
5. **SỨC KHỎE TỔNG QUÁT → general_health_agent**
- Câu hỏi CHUNG về sức khỏe, wellness, lifestyle
- Phòng ngừa, tăng cường sức khỏe
- Ví dụ: "làm sao để khỏe?", "phòng bệnh", "sống khỏe"
⚠️ EDGE CASES - Phân biệt với symptom_agent:
- "Tôi BỊ đau bụng" → symptom_agent (có triệu chứng)
- "Làm sao để KHÔNG bị đau bụng?" → general_health_agent (phòng ngừa)
- "Tôi không khỏe" (mơ hồ) → general_health_agent (chung chung)
- "Tôi bị sốt cao" → symptom_agent (triệu chứng cụ thể)
VÍ DỤ ROUTING (Bao gồm edge cases):
**Symptom Agent (có triệu chứng CỤ THỂ):**
✅ "Tôi bị đau bụng" → symptom_agent
✅ "Đau đầu từ sáng" → symptom_agent
✅ "Buồn nôn, muốn làm sao cho hết" → symptom_agent
✅ "Tôi bị sốt cao 39 độ" → symptom_agent
**General Health Agent (phòng ngừa, tổng quát):**
✅ "Làm sao để khỏe mạnh?" → general_health_agent
✅ "Phòng ngừa đau đầu" → general_health_agent (phòng ngừa!)
✅ "Tôi muốn sống khỏe hơn" → general_health_agent
✅ "Tư vấn sức khỏe tổng quát" → general_health_agent
**Nutrition Agent (dinh dưỡng):**
✅ "Tôi muốn giảm cân" → nutrition_agent
✅ "Nên ăn gì để khỏe?" → nutrition_agent
✅ "Ăn gì để hết đau bụng?" → nutrition_agent (dinh dưỡng!)
**Exercise Agent (tập luyện):**
✅ "Tôi nên tập gì?" → exercise_agent
✅ "Tập gì để hết đau lưng?" → exercise_agent (tập luyện!)
✅ "Không có dụng cụ gym thì sao?" (context: tập) → exercise_agent
**Mental Health Agent:**
✅ "Tôi stress quá" → mental_health_agent
**QUAN TRỌNG - CONTEXT AWARENESS:**
- Nếu last_agent = "exercise_agent" và câu hỏi về "dụng cụ", "tạ", "gym", "tại nhà"
→ TIẾP TỤC exercise_agent (đây là follow-up!)
- Nếu last_agent = "nutrition_agent" và câu hỏi về "món ăn", "thực đơn", "calo"
→ TIẾP TỤC nutrition_agent (đây là follow-up!)
Hãy chọn agent phù hợp nhất dựa trên CẢ câu hỏi hiện tại VÀ ngữ cảnh hội thoại."""
try:
response = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": """Bạn là hệ thống định tuyến thông minh với khả năng HIỂU NGỮ CẢNH hội thoại.
NHIỆM VỤ: Phân tích câu hỏi trong NGỮCẢNH cuộc hội thoại và chọn agent phù hợp.
KỸ NĂNG QUAN TRỌNG:
1. Nhận biết câu hỏi follow-up (vậy, còn, thì sao, nữa)
2. Hiểu context từ lịch sử hội thoại
3. Phát hiện topic switching (chuyển đề rõ ràng)
4. Xử lý câu hỏi mơ hồ bằng cách xem context
NGUYÊN TẮC:
- Câu hỏi RÕ RÀNG → agent trực tiếp
- Câu hỏi MƠ HỒ → xem lịch sử, last agent
- Follow-up question → có thể tiếp tục agent cũ
- Topic switch rõ ràng → agent mới"""
},
{
"role": "user",
"content": routing_prompt
}
],
functions=AVAILABLE_FUNCTIONS,
function_call="auto",
temperature=0.3 # Lower temperature for more consistent routing
)
# Check if function was called
if response.choices[0].message.function_call:
function_call = response.choices[0].message.function_call
import json
parameters = json.loads(function_call.arguments)
return {
"agent": function_call.name,
"parameters": parameters,
"confidence": 0.9, # High confidence when function is called
"raw_response": response
}
else:
# No function called, default to general health agent
return {
"agent": "general_health_agent",
"parameters": {"user_query": message},
"confidence": 0.5,
"raw_response": response
}
except Exception as e:
print(f"Routing error: {e}")
# Fallback to general health agent
return {
"agent": "general_health_agent",
"parameters": {"user_query": message},
"confidence": 0.3,
"error": str(e)
}
def get_agent_description(agent_name):
"""Get description of an agent"""
for func in AVAILABLE_FUNCTIONS:
if func["name"] == agent_name:
return func["description"]
return "Unknown agent"
# ============================================================
# Embedding-Based Router (New, Scalable Approach)
# ============================================================
class EmbeddingRouter:
"""
Embedding-based router that automatically matches queries to agents
without manual rules. More scalable than LLM-based routing.
"""
def __init__(self, use_embeddings=True):
"""
Initialize router
Args:
use_embeddings: If False, falls back to LLM-based routing
"""
self.use_embeddings = use_embeddings and EMBEDDINGS_AVAILABLE
if self.use_embeddings:
# Load embedding model
self.embedder = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
# Agent descriptions for embedding matching
self.agent_descriptions = {
"symptom_agent": """
Đánh giá triệu chứng bệnh khi BỊ ĐAU hoặc KHÔNG KHỎE:
đau đầu, đau bụng, đau lưng, sốt, ho, buồn nôn, chóng mặt,
mệt mỏi bất thường, khó thở, đau ngực, bị bệnh, cảm thấy đau,
đau nhức, triệu chứng bệnh lý, không khỏe, ốm, bệnh,
đang bị gì, bị gì vậy, triệu chứng gì
""",
"nutrition_agent": """
Tư vấn dinh dưỡng, ăn uống healthy, chế độ ăn:
giảm cân, tăng cân, giảm mỡ, muốn gầy, muốn béo,
ăn gì để giảm cân, ăn gì để tăng cân, calo, BMI,
thực đơn, chế độ ăn kiêng, thực phẩm, protein, carb, fat,
vitamin, khoáng chất, dinh dưỡng lành mạnh, ăn uống khoa học,
setup plan ăn uống, kế hoạch dinh dưỡng, healthy eating,
ăn healthy, ăn sạch, clean eating
""",
"exercise_agent": """
Tập luyện, gym, workout, fitness, thể hình:
tập luyện, luyện tập, gym, cardio, bài tập, lịch tập,
dụng cụ tập, tạ, thanh đòn, tập tại nhà, không có dụng cụ,
squat, push-up, plank, chạy bộ, yoga, thể dục, thể hình,
rèn luyện cơ thể, tăng cơ, giảm mỡ, build muscle, lose fat,
setup plan tập luyện, kế hoạch tập luyện, lịch tập 7 ngày,
tập gym, tập thể hình, workout plan, fitness plan
""",
"mental_health_agent": """
Sức khỏe tinh thần, stress, lo âu, trầm cảm, burnout,
mất ngủ, giấc ngủ, căng thẳng, áp lực, tâm lý, cảm xúc,
buồn bã, mệt mỏi tinh thần
""",
"general_health_agent": """
Câu hỏi chung về sức khỏe, lời khuyên sức khỏe,
phòng bệnh, chăm sóc sức khỏe, kiểm tra sức khỏe
"""
}
# Pre-compute agent embeddings
print("[INFO] Pre-computing agent embeddings...")
self.agent_embeddings = {
agent: self.embedder.encode(desc)
for agent, desc in self.agent_descriptions.items()
}
print("[INFO] Embedding router ready!")
else:
print("[INFO] Using LLM-based routing (embeddings not available)")
def route(self, message: str, chat_history: List[Tuple[str, str]] = None) -> Dict:
"""
Route message to appropriate agent
Args:
message: User message
chat_history: Conversation history
Returns:
{
"agent": agent_name,
"parameters": {...},
"confidence": float,
"method": "embedding" or "llm"
}
"""
if self.use_embeddings:
return self._route_with_embeddings(message, chat_history)
else:
# Fallback to LLM-based routing
return route_to_agent(message, chat_history)
def _route_with_embeddings(self, message: str, chat_history: List[Tuple[str, str]]) -> Dict:
"""Route using embedding similarity with topic change detection"""
# Embed query
query_embedding = self.embedder.encode(message)
# Calculate similarity with each agent
similarities = {}
for agent, agent_embedding in self.agent_embeddings.items():
similarity = cosine_similarity(
query_embedding.reshape(1, -1),
agent_embedding.reshape(1, -1)
)[0][0]
similarities[agent] = similarity
# Detect topic change vs follow-up
is_topic_change = self._detect_topic_change(message, chat_history)
# Context boost ONLY for genuine follow-ups (NOT topic changes)
if not is_topic_change and chat_history and len(chat_history) > 0:
# Determine CURRENT context from recent conversation (not just last message)
current_context_agent = self._get_current_context_agent(chat_history)
# Simple heuristic: if query is short and has follow-up indicators
if len(message.split()) < 10 and any(word in message.lower() for word in ["thì sao", "còn", "nữa", "thế", "vậy", "không", "khác", "nếu"]):
# Boost ONLY the current context agent (not all agents)
if current_context_agent and current_context_agent in similarities:
similarities[current_context_agent] += 0.15
print(f"[ROUTER] Boosting current context agent: {current_context_agent}")
# Get best agent
best_agent = max(similarities, key=similarities.get)
confidence = float(similarities[best_agent])
# Debug logging (disabled for cleaner output)
# print(f"\n[ROUTER DEBUG] Message: {message[:50]}...")
# print(f"[ROUTER DEBUG] Topic change detected: {is_topic_change}")
# print(f"[ROUTER DEBUG] Similarities:")
# for agent, score in sorted(similarities.items(), key=lambda x: x[1], reverse=True):
# print(f" - {agent}: {score:.4f}")
# print(f"[ROUTER DEBUG] Selected: {best_agent} (confidence: {confidence:.4f})\n")
return {
"agent": best_agent,
"parameters": {"user_query": message},
"confidence": confidence,
"method": "embedding",
"all_scores": {k: float(v) for k, v in similarities.items()},
"topic_change": is_topic_change
}
def _get_current_context_agent(self, chat_history: List[Tuple[str, str]]) -> Optional[str]:
"""
Determine which agent is handling the CURRENT context
by analyzing recent conversation (last 3-5 turns)
Returns:
Agent name that's currently active, or None
"""
if not chat_history or len(chat_history) == 0:
return None
# Check last 3-5 turns for dominant domain
recent_turns = chat_history[-5:] if len(chat_history) >= 5 else chat_history
domain_keywords = {
'nutrition_agent': ['ăn', 'dinh dưỡng', 'thực đơn', 'calo', 'bmi', 'giảm cân', 'tăng cân', 'protein', 'carb', 'meal', 'bữa'],
'exercise_agent': ['tập', 'luyện', 'gym', 'cardio', 'yoga', 'vận động', 'tăng cơ', 'giảm mỡ', 'workout', 'bài tập'],
'symptom_agent': ['đau', 'bệnh', 'triệu chứng', 'khó chịu', 'buồn nôn', 'sốt', 'ốm'],
'mental_health_agent': ['stress', 'lo âu', 'mất ngủ', 'trầm cảm', 'tâm lý']
}
# Count domain occurrences in recent turns
domain_counts = {agent: 0 for agent in domain_keywords.keys()}
for user_msg, bot_msg in recent_turns:
combined = (user_msg + " " + bot_msg).lower()
for agent, keywords in domain_keywords.items():
for keyword in keywords:
if keyword in combined:
domain_counts[agent] += 1
break # Count once per turn
# Return agent with highest count (if significant)
if domain_counts:
max_agent = max(domain_counts, key=domain_counts.get)
max_count = domain_counts[max_agent]
# Need at least 2 occurrences in recent turns to be considered "current context"
if max_count >= 2:
return max_agent
return None
def _detect_topic_change(self, message: str, chat_history: List[Tuple[str, str]]) -> bool:
"""
Detect if user is changing topics vs following up
Topic change indicators:
- Explicit new requests: "tôi muốn", "giúp tôi", "tư vấn về"
- Different domain keywords: nutrition → exercise, symptom → nutrition
- Long, detailed new questions
Returns:
bool: True if topic change, False if follow-up
"""
msg_lower = message.lower()
# Strong topic change indicators
topic_change_phrases = [
'tôi muốn', 'tôi cần', 'giúp tôi', 'tư vấn về', 'cho tôi',
'bây giờ', 'còn về', 'chuyển sang', 'ngoài ra',
'setup', 'tạo plan', 'lập kế hoạch'
]
if any(phrase in msg_lower for phrase in topic_change_phrases):
# Likely a new request
return True
# Check for domain-specific keywords that indicate topic change
domain_keywords = {
'nutrition': ['ăn', 'dinh dưỡng', 'thực đơn', 'calo', 'bmi', 'giảm cân', 'tăng cân'],
'exercise': ['tập', 'luyện', 'gym', 'cardio', 'yoga', 'vận động', 'tăng cơ', 'giảm mỡ'],
'symptom': ['đau', 'bệnh', 'triệu chứng', 'khó chịu', 'buồn nôn'],
'mental': ['stress', 'lo âu', 'mất ngủ', 'trầm cảm', 'tâm lý']
}
# Detect current message domain
current_domains = []
for domain, keywords in domain_keywords.items():
if any(kw in msg_lower for kw in keywords):
current_domains.append(domain)
# If no chat history, it's a new topic
if not chat_history or len(chat_history) == 0:
return True
# Check last few messages for domain
recent_messages = chat_history[-3:] if len(chat_history) >= 3 else chat_history
previous_domains = []
for user_msg, bot_msg in recent_messages:
combined = (user_msg + " " + bot_msg).lower()
for domain, keywords in domain_keywords.items():
if any(kw in combined for kw in keywords):
if domain not in previous_domains:
previous_domains.append(domain)
# If current domain is different from previous, it's a topic change
if current_domains and previous_domains:
# Check if there's overlap
overlap = set(current_domains) & set(previous_domains)
if not overlap:
# No overlap = topic change
return True
# Long messages (>15 words) with new content are likely topic changes
if len(message.split()) > 15:
# Check if it's not just elaborating on previous topic
follow_up_words = ['vì', 'do', 'bởi vì', 'là do', 'nghĩa là']
if not any(word in msg_lower for word in follow_up_words):
return True
# Default: assume follow-up
return False
# Global router instance (lazy initialization)
_router_instance = None
def get_router(use_embeddings=True, force_reload=False) -> EmbeddingRouter:
"""
Get global router instance
Args:
use_embeddings: Use embedding-based routing
force_reload: Force reload router (useful after updating agent descriptions)
"""
global _router_instance
if _router_instance is None or force_reload:
_router_instance = EmbeddingRouter(use_embeddings=use_embeddings)
return _router_instance
|