Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |