Nguyen Trong Lap
Recreate history without binary blobs
eeb0f9c
"""
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