File size: 15,116 Bytes
b256b14 |
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 |
try:
__import__("pysqlite3")
import sys
sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
except ImportError:
pass
import os
import logging
import traceback
import gradio as gr
import pandas as pd
import docx2txt
import chromadb
from chromadb.config import Settings
from shutil import rmtree
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_chroma import Chroma
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers.ensemble import EnsembleRetriever
from langchain.chains import create_retrieval_chain, create_history_aware_retriever
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.documents import Document
from langchain_huggingface import HuggingFaceEmbeddings
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
DATA_PATH = "medical_data"
DB_PATH = "chroma_db"
MAX_HISTORY_TURNS = 4
FORCE_REBUILD_DB = False
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
def process_excel_file(file_path: str, filename: str) -> list[Document]:
docs = []
try:
if file_path.endswith(".csv"):
df = pd.read_csv(file_path)
else:
df = pd.read_excel(file_path)
df.dropna(how='all', inplace=True)
df.fillna("Không có thông tin", inplace=True)
for idx, row in df.iterrows():
content_parts = []
for col_name, val in row.items():
clean_val = str(val).strip()
if clean_val and clean_val.lower() != "nan":
content_parts.append(f"{col_name}: {clean_val}")
if content_parts:
page_content = f"Dữ liệu từ file {filename} (Dòng {idx+1}):\n" + "\n".join(content_parts)
metadata = {"source": filename, "row": idx+1, "type": "excel_record"}
docs.append(Document(page_content=page_content, metadata=metadata))
except Exception as e:
logging.error(f"Lỗi xử lý Excel {filename}: {e}")
return docs
def load_documents_from_folder(folder_path: str) -> list[Document]:
logging.info(f"--- Bắt đầu quét thư mục: {folder_path} ---")
documents: list[Document] = []
if not os.path.exists(folder_path):
os.makedirs(folder_path, exist_ok=True)
return []
for root, _, files in os.walk(folder_path):
for filename in files:
file_path = os.path.join(root, filename)
filename_lower = filename.lower()
try:
if filename_lower.endswith(".pdf"):
loader = PyPDFLoader(file_path)
docs = loader.load()
for d in docs: d.metadata["source"] = filename
documents.extend(docs)
elif filename_lower.endswith(".docx"):
text = docx2txt.process(file_path)
if text.strip():
documents.append(Document(page_content=text, metadata={"source": filename}))
elif filename_lower.endswith((".xlsx", ".xls", ".csv")):
excel_docs = process_excel_file(file_path, filename)
documents.extend(excel_docs)
elif filename_lower.endswith((".txt", ".md")):
with open(file_path, "r", encoding="utf-8") as f: text = f.read()
if text.strip():
documents.append(Document(page_content=text, metadata={"source": filename}))
except Exception as e:
logging.error(f"Lỗi đọc file {filename}: {e}")
logging.info(f"Tổng cộng đã load: {len(documents)} tài liệu gốc.")
return documents
def get_retrievers():
logging.info("--- Tải Embedding Model ---")
embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
vectorstore = None
splits = []
# Tắt telemetry
chroma_settings = Settings(anonymized_telemetry=False)
# ... (Giữ nguyên đoạn xử lý FORCE_REBUILD_DB và load DB cũ/mới) ...
# LƯU Ý: Nếu bạn copy đè, hãy đảm bảo giữ lại logic if/else check DB ở đoạn này nhé
# Hoặc đơn giản là copy đoạn cấu hình Retriever bên dưới:
if not vectorstore:
logging.info("--- Tạo Index dữ liệu mới ---")
raw_docs = load_documents_from_folder(DATA_PATH)
if not raw_docs: return None, None
# SỬA 1: Chunk lớn hơn
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=200)
splits = text_splitter.split_documents(raw_docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=embedding_model, persist_directory=DB_PATH, client_settings=chroma_settings)
# === CẤU HÌNH LẠI FAST MODE (TỐI ƯU HÓA) ===
# SỬA 2: Tăng k lên 15. Gemini Flash đọc rất nhanh, đừng ngại đưa nhiều context.
# Việc này giúp giảm tỉ lệ sót thông tin.
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 15})
ensemble_retriever = vector_retriever
if splits:
bm25_retriever = BM25Retriever.from_documents(splits)
bm25_retriever.k = 15
# SỬA 3: Chỉnh trọng số 0.5 - 0.5.
# Code cũ ưu tiên Vector (0.6) dễ bị sai tên thuốc.
# Tăng BM25 giúp bắt chính xác tên biệt dược/hoạt chất hơn.
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5]
)
fast_retriever = ensemble_retriever
# === CẤU HÌNH DEEP MODE (Giữ nguyên hoặc tăng k nhẹ) ===
vector_retriever_deep = vectorstore.as_retriever(search_kwargs={"k": 25}) # Tăng nhẹ để Rerank có nhiều lựa chọn hơn
ensemble_retriever_deep = vector_retriever_deep
if splits:
bm25_retriever_deep = BM25Retriever.from_documents(splits)
bm25_retriever_deep.k = 25
ensemble_retriever_deep = EnsembleRetriever(
retrievers=[bm25_retriever_deep, vector_retriever_deep],
weights=[0.5, 0.5]
)
logging.info("--- Tải Reranker Model (BGE-M3) ---")
reranker_model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
compressor = CrossEncoderReranker(model=reranker_model, top_n=5)
deep_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=ensemble_retriever_deep
)
return fast_retriever, deep_retriever
class DeepMedBot:
def __init__(self):
self.fast_chain = None
self.deep_chain = None
self.ready = False
self.fallback_llm = None
if not GOOGLE_API_KEY:
logging.error("⚠️ Thiếu GOOGLE_API_KEY!")
return
try:
self.fast_retriever, self.deep_retriever = get_retrievers()
self.llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
temperature=0.2,
google_api_key=GOOGLE_API_KEY,
convert_system_message_to_human=True # Fix lỗi system prompt với Gemini
)
self.fallback_llm = self.llm # Dùng dự phòng nếu RAG lỗi
if self.fast_retriever and self.deep_retriever:
self._build_chains()
self.ready = True
logging.info("✅ Bot DeepMed đã sẵn sàng với 2 chế độ!")
else:
logging.warning("⚠️ Không có dữ liệu. Bot sẽ chỉ dùng kiến thức nền.")
self.ready = True # Vẫn cho chạy nhưng không có RAG
except Exception as e:
logging.error(f"🔥 Lỗi khởi tạo bot: {e}")
logging.debug(traceback.format_exc())
def _build_chains(self):
context_system_prompt = (
"Dựa trên lịch sử chat và câu hỏi mới nhất, hãy viết lại câu hỏi "
"thành một câu hoàn chỉnh để tìm kiếm thông tin. "
"Ví dụ: Nếu user hỏi 'thuốc đó dùng sao', hãy viết lại thành 'cách dùng thuốc [tên thuốc trước đó]'. "
"CHỈ TRẢ VỀ CÂU HỎI ĐÃ VIẾT LẠI, KHÔNG TRẢ LỜI."
)
context_prompt = ChatPromptTemplate.from_messages([
("system", context_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
qa_system_prompt = (
"Bạn là 'DeepMed-AI' - Trợ lý Dược lâm sàng chuyên nghiệp.\n"
"Nhiệm vụ của bạn là tư vấn điều trị CHỈ DỰA TRÊN Dữ liệu nội bộ (Context) được cung cấp bên dưới.\n\n"
"QUY TẮC AN TOÀN (BẮT BUỘC):\n"
"1. **Trung thực tuyệt đối:** Nếu thông tin không có trong Context, hãy trả lời: 'Xin lỗi, tôi không tìm thấy thông tin này trong dữ liệu nội bộ'. KHÔNG tự bịa ra phác đồ.\n"
"2. **Kiểm tra thuốc:** Khi đề xuất thuốc, chỉ nêu tên các thuốc có trong danh sách Context (có kèm giá/số lượng là tốt nhất).\n"
"3. **Trích dẫn:** Mọi khẳng định y khoa phải được trích dẫn từ Context.\n\n"
"Context:\n{context}"
)
qa_prompt = ChatPromptTemplate.from_messages([
("system", qa_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
question_answer_chain = create_stuff_documents_chain(self.llm, qa_prompt)
history_aware_fast = create_history_aware_retriever(self.llm, self.fast_retriever, context_prompt)
self.fast_chain = create_retrieval_chain(history_aware_fast, question_answer_chain)
history_aware_deep = create_history_aware_retriever(self.llm, self.deep_retriever, context_prompt)
self.deep_chain = create_retrieval_chain(history_aware_deep, question_answer_chain)
def chat_stream(self, message: str, history: list, mode: str):
if not self.ready:
yield "Hệ thống đang khởi động hoặc gặp lỗi cấu hình..."
return
chat_history = []
if history:
for turn in history[-MAX_HISTORY_TURNS:]:
if isinstance(turn, (list, tuple)) and len(turn) == 2:
u, b = turn
if u and b and str(u).strip() and str(b).strip():
chat_history.append(HumanMessage(content=str(u)))
chat_history.append(AIMessage(content=str(b)))
active_chain = self.deep_chain if mode == "Chuyên sâu (Chậm & Chính xác)" else self.fast_chain
if not active_chain:
try:
resp = self.llm.invoke([HumanMessage(content=message)])
yield f"⚠️ (Chế độ kiến thức chung) {resp.content}"
return
except:
yield "Lỗi: Không thể kết nối với AI. Vui lòng kiểm tra API Key."
return
full_response = ""
retrieved_docs = []
try:
for chunk in active_chain.stream({"input": message, "chat_history": chat_history}):
if "answer" in chunk:
full_response += chunk["answer"]
yield full_response
elif "context" in chunk:
retrieved_docs = chunk["context"]
if retrieved_docs:
refs = self._build_references_text(retrieved_docs)
if refs:
full_response += f"\n\n---\n📚 **Nguồn tham khảo ({mode}):**\n{refs}"
yield full_response
except Exception as e:
logging.error(f"Lỗi khi chat: {e}")
logging.error(traceback.format_exc())
if not full_response:
try:
yield "⚠️ Gặp lỗi khi truy xuất dữ liệu. Đang chuyển sang chế độ trả lời nhanh...\n\n"
fallback_resp = self.llm.invoke([HumanMessage(content=message)])
yield fallback_resp.content
except:
yield f"Đã xảy ra lỗi hệ thống. Vui lòng nhấn nút 'Clear' để xóa lịch sử chat và thử lại. (Lỗi: {str(e)})"
else:
yield full_response + f"\n\n[Lỗi ngắt kết nối: {str(e)}]"
@staticmethod
def _build_references_text(docs) -> str:
lines = []
seen = set()
for doc in docs:
src = doc.metadata.get("source", "Tài liệu")
row_info = f"(Dòng {doc.metadata['row']})" if "row" in doc.metadata else ""
type_info = " [Kho thuốc]" if doc.metadata.get("type") == "excel_record" else ""
ref_str = f"- {src}{type_info} {row_info}"
if ref_str not in seen:
lines.append(ref_str)
seen.add(ref_str)
return "\n".join(lines)
bot = DeepMedBot()
def gradio_chat_handler(message, history, mode):
yield from bot.chat_stream(message, history, mode)
css = """
.gradio-container {min_height: 600px !important;}
h1 {text-align: center; color: #2E86C1;}
.ref-box {font-size: 0.8em; color: gray;}
"""
with gr.Blocks(title="DeepMed AI") as demo:
gr.HTML(f"<style>{css}</style>")
gr.Markdown("# 🏥 DeepMed AI - Hệ Thống Hỗ Trợ Lâm Sàng")
with gr.Row():
with gr.Column(scale=4):
gr.Markdown("Nhập câu hỏi về phác đồ, thuốc hoặc tra cứu bệnh án.")
with gr.Column(scale=1):
mode_select = gr.Radio(
choices=["Tốc độ (Nhanh)", "Chuyên sâu (Chậm & Chính xác)"],
value="Tốc độ (Nhanh)",
label="Chế độ hoạt động",
info="Chọn 'Tốc độ' để trả lời nhanh, 'Chuyên sâu' để lọc kỹ dữ liệu."
)
chat_interface = gr.ChatInterface(
fn=gradio_chat_handler,
additional_inputs=[mode_select],
)
if __name__ == "__main__":
demo.launch() |