Spaces:
Runtime error
Runtime error
| """ | |
| Symptom Agent - Specialized agent for symptom assessment using OPQRST method | |
| """ | |
| from config.settings import client, MODEL | |
| from health_data import HealthContext | |
| from health_analysis import HealthAnalyzer | |
| from rag.rag_integration import get_rag_integration | |
| from agents.core.base_agent import BaseAgent | |
| from agents.core.context_analyzer import ContextAnalyzer | |
| from agents.core.response_validator import ResponseValidator | |
| from typing import Dict, Any, List, Optional | |
| from datetime import datetime | |
| import re | |
| class SymptomAgent(BaseAgent): | |
| def __init__(self, memory=None): | |
| super().__init__(memory) | |
| self.health_context = None | |
| self.analyzer = None | |
| self.rag = get_rag_integration() | |
| # Configure handoff triggers for symptom agent | |
| self.handoff_triggers = { | |
| 'nutrition_agent': ['ăn gì', 'thực đơn', 'dinh dưỡng'], | |
| 'exercise_agent': ['tập luyện', 'vận động', 'phục hồi chức năng'], | |
| 'mental_health_agent': ['lo âu', 'stress', 'mất ngủ do lo'], | |
| 'general_health_agent': ['khám tổng quát', 'xét nghiệm', 'kiểm tra sức khỏe'] | |
| } | |
| self.system_prompt = """Bạn là bác sĩ tư vấn chuyên nghiệp. | |
| 🩺 NHIỆM VỤ: | |
| Thu thập thông tin triệu chứng một cách có hệ thống và chuyên nghiệp. | |
| 📋 PHƯƠNG PHÁP OPQRST (Hỏi tự nhiên, KHÔNG dùng template): | |
| **Onset (Khởi phát):** | |
| - Khi nào bắt đầu? Đột ngột hay từ từ? | |
| - Ví dụ tự nhiên: | |
| * Đau đầu: "Đau đầu từ khi nào rồi bạn? Đột ngột hay từ từ?" | |
| * Đầy bụng: "Cảm giác đầy bụng này xuất hiện từ bao giờ? Sau khi ăn hay suốt ngày?" | |
| **Quality (Đặc điểm):** | |
| - Mô tả cảm giác như thế nào? | |
| - Ví dụ tự nhiên: | |
| * Đau đầu: "Đau kiểu gì? Đau nhói, tức, đập thình thình, hay nặng nề?" | |
| * Đầy bụng: "Cảm giác đầy như thế nào? Căng cứng, khó tiêu, hay đau tức?" | |
| **Region (Vị trí):** | |
| - Ở đâu? Có lan ra không? | |
| - Ví dụ tự nhiên: | |
| * Đau đầu: "Đau ở đâu? Trán, thái dương, sau gáy, hay cả đầu?" | |
| * Đầy bụng: "Đầy ở vùng nào? Trên rốn, dưới rốn, hay toàn bộ bụng?" | |
| **Provocation/Palliation (Yếu tố ảnh hưởng):** | |
| - Gì làm tệ/đỡ hơn? | |
| - Ví dụ tự nhiên: | |
| * Đau đầu: "Có gì làm đau nhiều hơn không? Ánh sáng, tiếng ồn, stress? Nghỉ ngơi có đỡ không?" | |
| * Đầy bụng: "Ăn gì làm nặng hơn? Có loại thức ăn nào làm đỡ không?" | |
| **Severity (Mức độ):** | |
| - Mức độ và triệu chứng kèm theo? | |
| - Ví dụ tự nhiên: | |
| * Đau đầu: "Đau nhiều không? Có buồn nôn, nhìn mờ, hoặc sợ ánh sáng không?" | |
| * Đầy bụng: "Đầy nhiều không? Có ợ hơi, buồn nôn, hoặc khó thở không?" | |
| **Timing (Thời gian):** | |
| - Khi nào xuất hiện? Liên tục hay từng đợt? | |
| - Ví dụ tự nhiên: | |
| * Đau đầu: "Đau suốt hay từng cơn? Thường xuất hiện lúc nào trong ngày?" | |
| * Đầy bụng: "Đầy suốt ngày hay chỉ sau ăn? Kéo dài bao lâu?" | |
| 🎯 NGUYÊN TẮC QUAN TRỌNG: | |
| 1. **HỎI TỐI ĐA 3-4 CÂU:** | |
| - Không hỏi mãi theo template OPQRST | |
| - Hỏi 3-4 câu quan trọng nhất | |
| - Nếu user không biết/không rõ → Chuyển sang đưa khuyến nghị | |
| 2. **ƯU TIÊN THÔNG TIN:** | |
| - Câu 1: Thời gian xuất hiện (khi nào?) | |
| - Câu 2: Đặc điểm (đau như thế nào?) | |
| - Câu 3: Mức độ (có triệu chứng kèm theo?) | |
| - Câu 4 (nếu cần): Yếu tố ảnh hưởng | |
| 3. **KHI USER KHÔNG BIẾT:** | |
| - User nói "không biết", "không rõ", "không chắc" | |
| - → DỪNG hỏi, chuyển sang đưa khuyến nghị | |
| - Dựa trên thông tin ĐÃ CÓ để tư vấn | |
| 4. **ĐƯA KHUYẾN NGHỊ:** | |
| - Tổng hợp thông tin đã thu thập | |
| - Đưa ra các biện pháp tự chăm sóc phù hợp | |
| - Khuyên gặp bác sĩ nếu cần | |
| - KHÔNG hỏi thêm nữa | |
| 🚨 RED FLAGS - Khuyên gặp bác sĩ NGAY: | |
| - Đau ngực + khó thở → Nghi ngờ tim | |
| - Đau đầu dữ dội đột ngột + cứng gáy + sốt → Nghi ngờ màng não | |
| - Yếu đột ngột một bên → Nghi ngờ đột quỵ | |
| - Đau bụng dữ dội → Nghi ngờ ruột thừa/cấp cứu | |
| - Ho/nôn ra máu | |
| - Ý định tự tử | |
| ⚠️ AN TOÀN & GIỚI HẠN: | |
| - KHÔNG chẩn đoán bệnh | |
| - KHÔNG kê đơn thuốc | |
| - KHÔNG tạo giáo án tập luyện (đó là việc của exercise_agent) | |
| - KHÔNG tư vấn dinh dưỡng chi tiết (đó là việc của nutrition_agent) | |
| - CHỈ tập trung vào ĐÁNH GIÁ TRIỆU CHỨNG | |
| - Luôn khuyên gặp bác sĩ với triệu chứng nghiêm trọng | |
| - Với red flags → khuyên đi cấp cứu NGAY | |
| 💬 PHONG CÁCH: | |
| - Tự nhiên, conversational - như đang nói chuyện | |
| - KHÔNG formal, KHÔNG "Dựa trên thông tin bạn cung cấp" | |
| - Emoji tối thiểu (chỉ khi thật sự cần) | |
| - Ngắn gọn, đi thẳng vấn đề | |
| - KHÔNG vừa hỏi vừa khuyên trong cùng 1 response | |
| 🏥 KHI USER HỎI ĐỊA CHỈ BỆNH VIỆN: | |
| - ĐỪNG lặp lại triệu chứng nếu đã nói rồi! | |
| - Nếu user hỏi "tôi muốn đi khám", "bệnh viện nào tốt", "cho tôi địa chỉ" | |
| → ĐI THẲNG VÀO ĐỊA CHỈ, không cần nhắc lại "Triệu chứng đau đầu và mất ngủ..." | |
| - Format địa chỉ bệnh viện: | |
| **Bệnh viện/Phòng khám gần [địa điểm]:** | |
| 1. **Tên bệnh viện** | |
| - Địa chỉ: [địa chỉ đầy đủ] | |
| - Chuyên khoa: [chuyên khoa liên quan] | |
| - SĐT: [nếu có] | |
| **Khi nào cần đi khám:** [điều kiện] | |
| - KHÔNG dùng "Giải pháp:" cho danh sách bệnh viện | |
| - KHÔNG mix địa chỉ với home remedies (thiền, yoga) trong cùng list | |
| 📝 VÍ DỤ WORKFLOW: | |
| **Tình huống: User đau bụng** | |
| Turn 1: | |
| User: "Tôi đau bụng" | |
| Bot: "Bạn bắt đầu bị đau từ khi nào vậy?" | |
| Turn 2: | |
| User: "Mới xuất hiện, đau âm ỉ" | |
| Bot: "Đau ở vị trí nào? Trên rốn, dưới rốn, hay toàn bộ bụng?" | |
| Turn 3: | |
| User: "Phía trên rốn" | |
| Bot: "Có triệu chứng kèm theo như buồn nôn, ợ hơi, hoặc đầy bụng không?" | |
| Turn 4: | |
| User: "Không biết, giờ tôi muốn làm sao cho hết đau" | |
| Bot: "Dựa trên thông tin bạn cung cấp (đau âm ỉ vùng thượng vị, mới xuất hiện), | |
| đây có thể là triệu chứng của viêm dạ dày hoặc khó tiêu. Khuyến nghị: | |
| 1. Nghỉ ngơi, tránh căng thẳng | |
| 2. Ăn nhẹ, tránh thức ăn cay nóng, cà phê, rượu | |
| 3. Có thể dùng thuốc giảm acid (theo chỉ định) | |
| Nếu đau không giảm sau 24h hoặc xuất hiện triệu chứng nặng hơn | |
| (nôn ra máu, đau dữ dội), hãy đến bệnh viện ngay." | |
| → DỪNG hỏi, đưa khuyến nghị dựa trên thông tin có! | |
| 🎯 NGUYÊN TẮC QUAN TRỌNG: | |
| 1. **ƯU TIÊN GIẢI PHÁP KHI USER CẦN:** | |
| - Nếu user nói "đau quá", "khó chịu", "làm sao" → Đưa giải pháp NGAY | |
| - Không hỏi thêm khi user đang cần giúp đỡ khẩn cấp | |
| - Cấu trúc: Giải pháp ngay → Thuốc (nếu cần) → Cảnh báo → Phòng ngừa | |
| 2. **CHỈ HỎI KHI CẦN THIẾT:** | |
| - Tối đa 1-2 câu hỏi trong toàn bộ conversation | |
| - Nếu đã có đủ info cơ bản → Đưa lời khuyên luôn | |
| - Nếu user không muốn trả lời → Đưa lời khuyên chung | |
| 3. **EMOJI - Dùng tiết kiệm:** | |
| - KHÔNG dùng 😔 cho mọi triệu chứng | |
| - Chỉ dùng khi thực sự cần (trấn an, động viên) | |
| - Có thể không dùng emoji nếu câu đã đủ ấm áp | |
| 4. **PHÂN TÍCH CONTEXT:** | |
| - Nếu là câu hỏi ĐẦU TIÊN → có thể đồng cảm | |
| - Nếu đang FOLLOW-UP → đi thẳng vào câu hỏi, không cần lặp lại đồng cảm | |
| - Nếu user đã trả lời nhiều câu → cảm ơn họ, không cần đồng cảm nữa | |
| VÍ DỤ CÁCH HỎI ĐA DẠNG: | |
| ❌ SAI (Lặp lại pattern): | |
| Turn 1: "Đau đầu khó chịu lắm nhỉ 😔 Cho mình hỏi..." | |
| Turn 2: "Đau nhói khó chịu quá 😔 Mà này..." | |
| Turn 3: "Sợ ánh sáng khó chịu lắm 😔 Còn về..." | |
| → LẶP LẠI "khó chịu" + 😔 = MÁY MÓC! | |
| ✅ ĐÚNG (Đa dạng, tự nhiên): | |
| Turn 1: "Mình hiểu rồi. Cho mình hỏi, bạn bị đau từ khi nào?" | |
| Turn 2: "À, đau nhói từ 2 ngày trước nhỉ. Vậy đau ở đâu? Trán, thái dương, hay cả đầu?" | |
| Turn 3: "Được rồi. Có gì làm đau nhiều hơn không? Ví dụ ánh sáng, tiếng ồn, hay stress?" | |
| → BIẾN ĐỔI, TỰ NHIÊN! | |
| VÍ DỤ THEO TRIỆU CHỨNG: | |
| **Đau đầu - Variations:** | |
| - "Cho mình hỏi, bạn bị đau từ khi nào?" | |
| - "Mình hiểu. Đau đầu xuất hiện đột ngột hay từ từ?" | |
| - "Để mình giúp bạn tìm hiểu. Đau kiểu gì? Nhói, tức, hay đập thình thình?" | |
| **Đầy bụng - Variations:** | |
| - "Cảm giác đầy này xuất hiện từ bao giờ?" | |
| - "Liên quan đến ăn uống không? Sau khi ăn hay suốt ngày?" | |
| - "Có ợ hơi hoặc khó tiêu không?" | |
| **Đau lưng - Variations:** | |
| - "Bạn bị đau lưng từ khi nào?" | |
| - "Có bị chấn thương hay làm gì nặng không?" | |
| - "Đau ở vị trí nào? Lưng trên, giữa, hay dưới?" | |
| QUAN TRỌNG: | |
| - Mỗi triệu chứng cần cách hỏi KHÁC NHAU | |
| - Mỗi TURN trong conversation cần cách diễn đạt KHÁC NHAU | |
| - KHÔNG lặp lại patterns - hãy TỰ NHIÊN như người thật!""" | |
| def set_health_context(self, health_context: HealthContext): | |
| """Inject health context and initialize health analyzer""" | |
| self.health_context = health_context | |
| self.analyzer = HealthAnalyzer(health_context) | |
| def handle(self, parameters, chat_history=None): | |
| """ | |
| Handle symptom assessment request using LLM for natural conversation | |
| Args: | |
| parameters (dict): {"user_query": str} | |
| chat_history (list): Conversation history | |
| Returns: | |
| str: Response message | |
| """ | |
| user_query = parameters.get("user_query", "") | |
| # Check for red flags first | |
| red_flag_response = self._check_red_flags(user_query, chat_history) | |
| if red_flag_response: | |
| # Persist red flag alert | |
| if self.health_context: | |
| self.health_context.add_health_record('symptom', { | |
| 'query': user_query, | |
| 'type': 'red_flag_alert', | |
| 'response': red_flag_response, | |
| 'timestamp': datetime.now().isoformat() | |
| }) | |
| return red_flag_response | |
| # Use LLM to naturally assess symptoms and ask questions | |
| response = self._natural_symptom_assessment(user_query, chat_history) | |
| # Analyze health risks if analyzer is available | |
| if self.analyzer: | |
| risks = self.analyzer.identify_health_risks() | |
| predictions = self.analyzer.predict_disease_risk() | |
| else: | |
| risks = [] | |
| predictions = {} | |
| # Persist symptom data to health context | |
| if self.health_context: | |
| self.health_context.add_health_record('symptom', { | |
| 'query': user_query, | |
| 'response': response, | |
| 'risks': risks, | |
| 'predictions': predictions, | |
| 'timestamp': datetime.now().isoformat() | |
| }) | |
| return response | |
| def _build_context_instruction(self, context, chat_history, user_query=""): | |
| """ | |
| Build clear instruction based on conversation stage | |
| """ | |
| stage = context.get('conversation_stage', 0) | |
| urgency = context.get('urgency', 'medium') | |
| is_vague = context.get('is_vague', False) | |
| # PRIORITY: Handle vague/unclear queries first | |
| if is_vague and stage == 0: | |
| return """\n\nPHASE: LÀM RÕ Ý ĐỊNH (VỚI GỢI Ý) | |
| User query không rõ ràng. Giúp user bằng GỢI Ý CỤ THỂ: | |
| 1. ACKNOWLEDGE + HỎI VỚI GỢI Ý: | |
| Format: "Mình thấy bạn [cảm giác user nói]. Bạn có thể cho mình biết rõ hơn không? Ví dụ như: | |
| • [Gợi ý 1 liên quan] | |
| • [Gợi ý 2 liên quan] | |
| • [Gợi ý 3 liên quan] | |
| • Hoặc vấn đề khác?" | |
| 2. GỢI Ý DỰA VÀO TỪ KHÓA: | |
| - "mệt" → gợi ý: mệt cơ thể, mệt tinh thần, mất ngủ, stress | |
| - "không khỏe" → gợi ý: đau đầu, buồn nôn, chóng mặt, sốt | |
| - "khó chịu" → gợi ý: đau bụng, khó tiêu, lo âu, căng thẳng | |
| - "không ổn" → gợi ý: sức khỏe thể chất, tinh thần, dinh dưỡng | |
| 3. VÍ DỤ CỤ THỂ: | |
| User: "tôi mệt" | |
| Bot: "Mình thấy bạn đang cảm thấy mệt. Bạn có thể nói rõ hơn không? Ví dụ: | |
| • Mệt cơ thể, không có sức? | |
| • Mệt tinh thần, stress? | |
| • Mất ngủ, ngủ không ngon? | |
| • Hay vấn đề khác?" | |
| QUAN TRỌNG: | |
| - Dùng từ khóa user nói để tạo gợi ý phù hợp | |
| - 3-4 gợi ý cụ thể | |
| - Luôn có "hoặc vấn đề khác" để mở rộng | |
| - Tự nhiên, không formal""" | |
| # Assessment phase (first 1-2 turns) | |
| if stage <= 1: | |
| return """\n\nPHASE: ĐÁNH GIÁ TRIỆU CHỨNG | |
| Hỏi 1-2 câu ngắn để hiểu rõ: | |
| - Thời gian xuất hiện | |
| - Vị trí đau | |
| - Mức độ đau | |
| CHỈ HỎi, KHÔNG đưa lời khuyên.""" | |
| # High urgency - skip to solutions | |
| if urgency == 'high': | |
| return """\n\nPHASE: GIẢI PHÁP KHẨN CẤP | |
| User cần giúp NGAY. Đưa ra: | |
| 1. Giải pháp tức thời (2-3 điềm) | |
| 2. Thuốc có thể dùng + disclaimer | |
| 3. Cảnh báo khi nào đi khám | |
| KHÔNG hỏi thêm.""" | |
| # Check if user is answering self-assessment questions | |
| if chat_history and len(chat_history) > 0: | |
| last_bot_msg = chat_history[-1][1] if len(chat_history[-1]) > 1 else "" | |
| # More specific detection - must have "Câu hỏi tự kiểm tra" section | |
| if ("Câu hỏi tự kiểm tra" in last_bot_msg or "### Câu hỏi tự kiểm tra" in last_bot_msg) and len(user_query) > 30: | |
| return """\n\nPHASE: PHÂN TÍCH KẾT QUẢ TỰ KIỂM TRA | |
| User vừa trả lời self-assessment. Phân tích THÔNG MINH dựa trên ĐÚNG CONTEXT: | |
| QUAN TRỌNG: | |
| - CHỈ phân tích dựa trên triệu chứng user VỪA NÓI | |
| - KHÔNG dùng thông tin từ RAG không liên quan | |
| - KHÔNG nhầm lẫn với các bệnh khác | |
| 1. NHẬN DIỆN PATTERN (dựa vào RAG knowledge): | |
| - Đọc kỹ triệu chứng user mô tả | |
| - So sánh với các bệnh có thể có (từ RAG) | |
| - Tìm điểm KHÁC BIỆT quan trọng | |
| - Đưa ra 1-2 khả năng phù hợp nhất | |
| 2. ĐÁNH GIÁ MỨC ĐỘ NGHIÊM TRỌNG: | |
| - Có red flags? → "CẦN KHÁM NGAY" | |
| - Triệu chứng nặng? → "Nên đi khám sớm" | |
| - Triệu chứng nhẹ? → "Thử giải pháp, không đỡ thì khám" | |
| 3. LUÔN DISCLAIMER: | |
| "Đây chỉ là đánh giá sơ bộ dựa trên triệu chứng. Bác sĩ sẽ chẩn đoán chính xác qua khám lâm sàng và xét nghiệm." | |
| 4. NEXT STEPS: | |
| - Giải pháp tạm thời (nếu không nguy hiểm) | |
| - Khi nào cần đi khám | |
| - "Bạn muốn biết thêm về [bệnh nghi ngờ] không?" | |
| QUAN TRỌNG: Phân tích GENERIC cho MỌI triệu chứng, KHÔNG hard-code.""" | |
| # Check if user asking "how to know" / differential diagnosis | |
| if any(phrase in user_query.lower() for phrase in [ | |
| 'làm sao biết', 'làm sao để biết', 'phân biệt', | |
| 'khác nhau thế nào', 'hay', 'hoặc' | |
| ]): | |
| return """\n\nPHASE: HƯỚNG DẪN TỰ KIỂM TRA (GENERIC) | |
| User muốn phân biệt các bệnh/tình trạng. Sử dụng RAG để: | |
| 1. XÁC ĐỊNH các bệnh có thể (từ user query): | |
| - Trích xuất các bệnh user đề cập | |
| - Hoặc tìm các bệnh liên quan đến triệu chứng | |
| 2. TẠO BẢNG SO SÁNH: | |
| Format: | |
| **[Bệnh A]:** | |
| • Triệu chứng đặc trưng 1 | |
| • Triệu chứng đặc trưng 2 | |
| • Đặc điểm riêng | |
| **[Bệnh B]:** | |
| • Triệu chứng đặc trưng 1 | |
| • Triệu chứng đặc trưng 2 | |
| • Đặc điểm riêng | |
| **Điểm khác biệt chính:** [Highlight key differences] | |
| 3. CÂU HỎI TỰ KIỂM TRA: | |
| Tạo 3-5 câu hỏi giúp user tự đánh giá: | |
| • Về thời gian xuất hiện | |
| • Về đặc điểm triệu chứng | |
| • Về yếu tố kích hoạt | |
| • Về triệu chứng kèm theo | |
| 4. LUÔN DISCLAIMER: | |
| "Tuy nhiên, chỉ bác sĩ mới chẩn đoán chính xác qua khám lâm sàng và xét nghiệm." | |
| 5. Kết thúc: "Sau khi tự kiểm tra, bạn có thể cho mình biết kết quả để mình phân tích nhé!" | |
| QUAN TRỌNG: Dùng RAG knowledge, KHÔNG hard-code bệnh cụ thể.""" | |
| # Advice phase (have enough info) | |
| return """\n\nPHASE: TƯ VẤN & PHÒNG NGỪÀ | |
| Đưa ra: | |
| 1. Đánh giá ngắn (1 câu): Triệu chứng có thể là gì | |
| 2. Giải pháp (3-4 điểm cụ thể) | |
| 3. Khi nào cần đi khám | |
| 4. Kết thúc: "Có gì thắc mắc cứ hỏi mình nhé!" | |
| KHÔNG nói "Dựa trên thông tin".""" | |
| def _validate_response(self, response, context): | |
| """ | |
| Validate if LLM response follows instructions | |
| Returns: (is_valid, list_of_issues) | |
| """ | |
| issues = [] | |
| stage = context.get('conversation_stage', 0) | |
| # Check for bad formal phrases | |
| bad_phrases = [ | |
| "Dựa trên thông tin bạn cung cấp", | |
| "Dựa vào thông tin", | |
| "Theo thông tin bạn đưa ra" | |
| ] | |
| for phrase in bad_phrases: | |
| if phrase.lower() in response.lower(): | |
| issues.append(f"Dùng cụm từ formal: '{phrase}'") | |
| break | |
| # Assessment phase: should ask, not advise | |
| if stage <= 1: | |
| advice_indicators = [ | |
| "khuyến nghị", "nên", "hãy", "bạn thử", | |
| "giải pháp", "cách xử lý" | |
| ] | |
| has_advice = any(ind in response.lower() for ind in advice_indicators) | |
| has_question = '?' in response | |
| if has_advice and not has_question: | |
| issues.append("Đưa lời khuyên quá sớm (phase assessment)") | |
| # Check if both asking and advising (bad) | |
| if '?' in response: | |
| advice_count = sum(1 for ind in ["khuyến nghị", "nên", "hãy thử"] if ind in response.lower()) | |
| if advice_count >= 2: | |
| issues.append("Vừa hỏi vừa khuyên trong cùng response") | |
| is_valid = len(issues) == 0 | |
| return is_valid, issues | |
| def _post_process_response(self, response, context): | |
| """ | |
| Clean up LLM response to ensure quality | |
| """ | |
| # Remove formal phrases | |
| bad_phrases = [ | |
| "Dựa trên thông tin bạn cung cấp", | |
| "Dựa vào thông tin", | |
| "Theo thông tin bạn đưa ra", | |
| "Từ thông tin trên" | |
| ] | |
| for phrase in bad_phrases: | |
| response = response.replace(phrase, "") | |
| response = response.replace(phrase.lower(), "") | |
| # Clean up extra whitespace | |
| response = "\n".join(line.strip() for line in response.split("\n") if line.strip()) | |
| return response | |
| def _check_red_flags(self, user_query, chat_history): | |
| """Check for dangerous symptoms that need immediate medical attention""" | |
| all_text = user_query.lower() | |
| if chat_history: | |
| all_text += " " + " ".join([msg[0].lower() for msg in chat_history if msg[0]]) | |
| red_flags = { | |
| "heart_attack": { | |
| "keywords": ["đau ngực", "khó thở", "chest pain", "đau tim"], | |
| "message": """🚨 **CẢNH BÁO KHẨN CẤP** | |
| Triệu chứng của bạn có thể liên quan đến **cơn đau tim**. Đây là tình huống khẩn cấp! | |
| ⚠️ **HÃY LÀM NGAY:** | |
| 1. **Gọi cấp cứu 115** hoặc đến bệnh viện GẤP | |
| 2. Ngồi nghỉ, không vận động | |
| 3. Nếu có aspirin, nhai 1 viên (nếu không dị ứng) | |
| 4. Thông báo cho người thân | |
| 🚑 **KHÔNG TỰ LÁI XE** - Gọi xe cấp cứu hoặc nhờ người khác đưa đi | |
| Sức khỏe của bạn là ưu tiên số 1. Hãy đi khám NGAY nhé! 💙""" | |
| }, | |
| "stroke": { | |
| "keywords": ["yếu một bên", "méo miệng", "nói khó", "tê nửa người"], | |
| "message": """🚨 **CẢNH BÁO KHẨN CẤP - NGUY CƠ ĐỘT QUỴ** | |
| Triệu chứng của bạn có thể là **đột quỵ não**. Mỗi phút đều quan trọng! | |
| ⚠️ **HÃY LÀM NGAY:** | |
| 1. **Gọi cấp cứu 115 NGAY LẬP TỨC** | |
| 2. Ghi nhớ thời gian triệu chứng bắt đầu | |
| 3. Nằm nghỉ, đầu hơi cao | |
| 4. KHÔNG cho ăn uống gì | |
| 🚑 Đây là cấp cứu y tế. Hãy đi bệnh viện NGAY! Thời gian vàng chỉ có 3-4 giờ!""" | |
| }, | |
| "meningitis": { | |
| "keywords": ["đau đầu dữ dội", "cứng gáy", "sốt cao", "buồn nôn"], | |
| "message": """🚨 **CẢNH BÁO - CẦN KHÁM NGAY** | |
| Triệu chứng đau đầu dữ dội + cứng gáy + sốt có thể là **viêm màng não** - rất nguy hiểm! | |
| ⚠️ **HÃY LÀM NGAY:** | |
| 1. Đi bệnh viện hoặc gọi cấp cứu 115 | |
| 2. Không trì hoãn | |
| 3. Thông báo bác sĩ về tất cả triệu chứng | |
| Đây là tình huống nghiêm trọng. Hãy đi khám NGAY nhé! 🏥""" | |
| }, | |
| "severe_abdominal": { | |
| "keywords": ["đau bụng dữ dội", "đau bụng không chịu nổi", "đau bụng cấp"], | |
| "message": """⚠️ **CẦN KHÁM BÁC SĨ NGAY** | |
| Đau bụng dữ dội có thể là nhiều nguyên nhân nghiêm trọng (viêm ruột thừa, sỏi mật, thủng dạ dày...). | |
| 🏥 **Hãy đi khám ngay nếu:** | |
| - Đau không giảm sau 1-2 giờ | |
| - Kèm sốt, nôn, tiêu chảy | |
| - Bụng cứng, đau khi ấn | |
| - Có máu trong phân | |
| Đừng chần chừ, hãy đi bệnh viện để được khám và xử lý kịp thời nhé!""" | |
| } | |
| } | |
| for flag_type, flag_data in red_flags.items(): | |
| if any(keyword in all_text for keyword in flag_data["keywords"]): | |
| return flag_data["message"] | |
| return None | |
| def _needs_rag_query(self, user_query, chat_history): | |
| """Determine if RAG query is needed for this question""" | |
| # Simple questions don't need RAG | |
| simple_patterns = [ | |
| 'đau', 'bị', 'khó tiêu', 'mệt', 'chóng mặt', 'buồn nôn', | |
| 'sốt', 'ho', 'cảm', 'đau đầu', 'đau bụng', 'đau lưng' | |
| ] | |
| # Check if it's a simple symptom report (first turn) | |
| if not chat_history or len(chat_history) == 0: | |
| # First message - usually just symptom report | |
| if any(pattern in user_query.lower() for pattern in simple_patterns): | |
| return False # Don't need RAG for initial symptom report | |
| # Need RAG for complex questions or specific medical info | |
| complex_patterns = [ | |
| 'nguyên nhân', 'tại sao', 'làm sao', 'điều trị', 'thuốc', | |
| 'phòng ngừa', 'biến chứng', 'triệu chứng của', 'bệnh gì' | |
| ] | |
| if any(pattern in user_query.lower() for pattern in complex_patterns): | |
| return True | |
| # Default: don't use RAG for conversational turns | |
| return False | |
| def _natural_symptom_assessment(self, user_query, chat_history): | |
| """Use LLM to naturally assess symptoms with context awareness""" | |
| try: | |
| # Analyze user context and intent | |
| context = ContextAnalyzer.analyze_user_intent(user_query, chat_history) | |
| response_structure = ContextAnalyzer.determine_response_structure(context) | |
| # Smart RAG - only query when needed | |
| rag_answer = '' | |
| rag_sources = [] | |
| if self._needs_rag_query(user_query, chat_history): | |
| # Build context-aware RAG query | |
| # Include recent conversation context to get relevant results | |
| if chat_history and len(chat_history) > 0: | |
| last_exchange = chat_history[-1] | |
| last_bot_msg = last_exchange[1] if len(last_exchange) > 1 else "" | |
| # If answering self-assessment, include the diseases mentioned | |
| if "Câu hỏi tự kiểm tra" in last_bot_msg: | |
| # Extract diseases from last bot message | |
| import re | |
| diseases = re.findall(r'\*\*\[(.*?)\]:\*\*', last_bot_msg) | |
| if diseases: | |
| # Query specifically about those diseases | |
| enhanced_query = f"{user_query} (liên quan đến: {', '.join(diseases)})" | |
| rag_result = self.rag.query_health(enhanced_query) | |
| else: | |
| rag_result = self.rag.query_health(user_query) | |
| else: | |
| rag_result = self.rag.query_health(user_query) | |
| else: | |
| rag_result = self.rag.query_health(user_query) | |
| rag_answer = rag_result.get('answer', '') | |
| rag_sources = rag_result.get('source_docs', []) | |
| # Build conversation context | |
| messages = [{"role": "system", "content": self.system_prompt}] | |
| # Add RAG context if available with explicit filtering instruction | |
| if rag_answer: | |
| # Add warning about context relevance | |
| rag_instruction = f"""Thông tin tham khảo từ cơ sở dữ liệu:\n{rag_answer} | |
| ⚠️ QUAN TRỌNG: | |
| - CHỈ sử dụng thông tin LIÊN QUAN đến triệu chứng user đang nói | |
| - KHÔNG dùng thông tin về bệnh khác không liên quan | |
| - Nếu thông tin RAG không match với triệu chứng user → BỎ QUA""" | |
| messages.append({"role": "system", "content": rag_instruction}) | |
| # Add chat history (last 5 exchanges for context) | |
| 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}) | |
| # Build context-aware instruction | |
| context_prompt = self._build_context_instruction(context, chat_history, user_query) | |
| messages.append({"role": "user", "content": user_query + context_prompt}) | |
| # 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 | |
| # CRITICAL: Check for context mismatch (e.g., talking about brain when discussing stomach) | |
| if chat_history and len(chat_history) > 0: | |
| # Get recent symptoms mentioned | |
| recent_symptoms = [] | |
| for msg, _ in chat_history[-3:]: | |
| if msg: | |
| recent_symptoms.extend([ | |
| 'đau bụng', 'dạ dày', 'tiêu hóa', 'ăn', 'buồn nôn', 'ợ' | |
| ] if any(w in msg.lower() for w in ['bụng', 'dạ dày', 'ăn', 'nôn', 'ợ']) else []) | |
| # Check if response mentions completely unrelated conditions | |
| unrelated_keywords = { | |
| 'stomach': ['viêm màng não', 'cứng gáy', 'não'], | |
| 'head': ['đau bụng', 'tiêu hóa', 'dạ dày'], | |
| 'respiratory': ['đau bụng', 'dạ dày'] | |
| } | |
| # If discussing stomach but response mentions brain → REJECT | |
| if recent_symptoms and any('bụng' in s or 'dạ dày' in s for s in recent_symptoms): | |
| if any(keyword in llm_response.lower() for keyword in ['viêm màng não', 'cứng gáy', 'não', 'đầu dữ dội']): | |
| print("⚠️ CONTEXT MISMATCH DETECTED: Response about brain when discussing stomach!") | |
| # Force retry with explicit instruction | |
| messages[-1]['content'] += "\n\n🚨 LỖI NGHIÊM TRỌNG: User đang nói về BỤng/DẠ DÀY, KHÔNG phải đầu/não! Phân tích lại ĐÚNG triệu chứng!" | |
| retry_response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=messages, | |
| temperature=0.3, # Very low temp for accuracy | |
| max_tokens=500 | |
| ) | |
| llm_response = retry_response.choices[0].message.content | |
| # Validate response quality using shared validator | |
| is_valid, issues = ResponseValidator.validate_response( | |
| llm_response, | |
| agent_type='symptom', | |
| context=context, | |
| chat_history=chat_history | |
| ) | |
| # Retry if invalid (max 1 retry) | |
| if not is_valid: | |
| print(f"Response validation failed: {issues}. Retrying...") | |
| # Add stronger instruction | |
| messages[-1]['content'] += f"\n\nLỖI TRƯỚC: {', '.join(issues)}. HÃY SỬA LẠI!" | |
| retry_response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=messages, | |
| temperature=0.5, # Lower temp for more control | |
| max_tokens=500 | |
| ) | |
| llm_response = retry_response.choices[0].message.content | |
| # Post-process to ensure quality | |
| llm_response = self._post_process_response(llm_response, context) | |
| # 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 nếu triệu chứng nghiêm trọng, hãy gặp bác sĩ ngay nhé 🙏 | |
| Lỗi: {str(e)[:100]}""" | |
| def _assess_opqrst_progress(self, chat_history): | |
| """Assess how much OPQRST data has been collected""" | |
| if not chat_history: | |
| return {'complete': False, 'next_step': 'onset', 'data': {}} | |
| # Analyze conversation to see what's been asked | |
| all_bot_messages = " ".join([msg[1].lower() for msg in chat_history if msg[1]]) | |
| all_user_messages = " ".join([msg[0].lower() for msg in chat_history if msg[0]]) | |
| opqrst_data = { | |
| 'onset': None, | |
| 'provocation': None, | |
| 'quality': None, | |
| 'region': None, | |
| 'severity': None, | |
| 'timing': None | |
| } | |
| # Check what's been asked | |
| if "khi nào" in all_bot_messages or "bắt đầu" in all_bot_messages: | |
| opqrst_data['onset'] = 'asked' | |
| if "làm tệ hơn" in all_bot_messages or "làm đỡ" in all_bot_messages: | |
| opqrst_data['provocation'] = 'asked' | |
| if "mô tả cảm giác" in all_bot_messages or "đau kiểu gì" in all_bot_messages: | |
| opqrst_data['quality'] = 'asked' | |
| if "vị trí" in all_bot_messages or "ở đâu" in all_bot_messages: | |
| opqrst_data['region'] = 'asked' | |
| if "mức độ" in all_bot_messages or "1-10" in all_bot_messages: | |
| opqrst_data['severity'] = 'asked' | |
| if "lúc nào xuất hiện" in all_bot_messages or "liên tục" in all_bot_messages: | |
| opqrst_data['timing'] = 'asked' | |
| # Determine next step | |
| for step, value in opqrst_data.items(): | |
| if value is None: | |
| return {'complete': False, 'next_step': step, 'data': opqrst_data} | |
| # All steps completed | |
| return {'complete': True, 'next_step': None, 'data': opqrst_data} | |
| def _ask_next_opqrst_question(self, next_step, user_query): | |
| """Ask the next OPQRST question""" | |
| questions = { | |
| 'onset': """Mình hiểu rồi. Để đánh giá chính xác hơn, cho mình hỏi thêm nhé: | |
| - Triệu chứng này bắt đầu từ khi nào? (hôm nay, mấy ngày, mấy tuần?) | |
| - Nó xuất hiện đột ngột hay từ từ?""", | |
| 'quality': """À được rồi. Mà này: | |
| - Bạn mô tả cảm giác đó như thế nào? (đau nhói, tức, nóng rát, tê, đập thình thình...) | |
| - Mức độ từ 1-10 thì bao nhiêu? (1 = nhẹ, 10 = không chịu nổi)""", | |
| 'region': """Ừm, để mình hỏi thêm: | |
| - Vị trí chính xác ở đâu? (chỉ rõ vùng cơ thể) | |
| - Có lan ra chỗ khác không?""", | |
| 'provocation': """Bạn có nhận thấy: | |
| - Có gì làm nó tệ hơn không? (vận động, ăn uống, stress, tư thế...) | |
| - Có gì làm nó đỡ hơn không? (nghỉ ngơi, thuốc, chườm...)""", | |
| 'timing': """Quan trọng nhé: | |
| - Nó xuất hiện lúc nào trong ngày? (sáng, chiều, tối, đêm?) | |
| - Liên tục hay từng đợt? Mỗi đợt kéo dài bao lâu?""", | |
| 'severity': """Cuối cùng: | |
| - Có kèm theo triệu chứng nào khác không? (sốt, buồn nôn, chóng mặt, mệt mỏi...) | |
| - Có ảnh hưởng đến ăn uống, ngủ nghỉ, sinh hoạt không? | |
| - Bạn có bệnh nền gì không? Đang uống thuốc gì không?""" | |
| } | |
| return questions.get(next_step, "Cho mình biết thêm về triệu chứng của bạn nhé?") | |
| def _provide_assessment(self, opqrst_data, user_query): | |
| """Provide symptom assessment after collecting OPQRST data""" | |
| # Use LLM to analyze symptoms with OPQRST context | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": self.system_prompt}, | |
| {"role": "user", "content": f"""Dựa vào thông tin OPQRST đã thu thập, hãy đánh giá triệu chứng và đưa ra lời khuyên. | |
| Triệu chứng ban đầu: {user_query} | |
| Thông tin đã thu thập: | |
| - Onset: {opqrst_data.get('onset', 'chưa rõ')} | |
| - Quality: {opqrst_data.get('quality', 'chưa rõ')} | |
| - Region: {opqrst_data.get('region', 'chưa rõ')} | |
| - Provocation: {opqrst_data.get('provocation', 'chưa rõ')} | |
| - Timing: {opqrst_data.get('timing', 'chưa rõ')} | |
| - Severity: {opqrst_data.get('severity', 'chưa rõ')} | |
| Hãy đưa ra: | |
| 1. Phân tích triệu chứng | |
| 2. Nguyên nhân có thể | |
| 3. Lời khuyên xử lý tại nhà (nếu phù hợp) | |
| 4. Khi nào cần gặp bác sĩ | |
| 5. Lời động viên, trấn an"""} | |
| ], | |
| temperature=0.7, | |
| max_tokens=1500 | |
| ) | |
| return response.choices[0].message.content | |
| except Exception as e: | |
| return f"""Xin lỗi, mình gặp chút vấn đề khi phân tích triệu chứng. | |
| Dựa vào những gì bạn chia sẻ, mình khuyên bạn nên: | |
| - Theo dõi triệu chứng thêm 24-48 giờ | |
| - Nghỉ ngơi đầy đủ | |
| - Uống đủ nước | |
| - Nếu triệu chứng tệ hơn hoặc không giảm → đi khám bác sĩ | |
| Với các triệu chứng bất thường, tốt nhất là được bác sĩ khám trực tiếp nhé! 🏥""" | |