Spaces:
Sleeping
Sleeping
File size: 21,508 Bytes
3bc8336 279184d 3bc8336 df60532 3bc8336 12f849f 3bc8336 12f849f 3bc8336 279184d d409128 927ef83 3bc8336 927ef83 3bc8336 6bd6ff8 3bc8336 1ac8620 279184d 6bd6ff8 3bc8336 279184d da32c1e 279184d 1426f0f 3bc8336 4c3e7af 12f849f 3bc8336 8a55f94 279184d 3bc8336 1426f0f 3bc8336 1426f0f 3bc8336 1426f0f 3bc8336 279184d 1426f0f 3bc8336 4c3e7af 3bc8336 1426f0f 3bc8336 1426f0f 3bc8336 927ef83 3bc8336 3504b37 3bc8336 3504b37 3bc8336 df60532 3bc8336 279184d 3bc8336 3504b37 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 4c3e7af 3bc8336 12f849f 927ef83 3bc8336 927ef83 3bc8336 927ef83 3f22f1a d409128 3bc8336 d409128 3bc8336 4f16805 12f849f 3bc8336 1426f0f 3bc8336 279184d 3bc8336 279184d 3bc8336 1ac8620 3bc8336 927ef83 3bc8336 927ef83 3bc8336 927ef83 3bc8336 927ef83 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 3504b37 3bc8336 3504b37 3bc8336 279184d 3bc8336 1abbebe 3bc8336 927ef83 1abbebe 3bc8336 d409128 3bc8336 279184d 3bc8336 1426f0f 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 279184d 3bc8336 5154b50 3bc8336 d409128 |
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 |
"""
DeepMed AI - Trợ lý Dược lâm sàng (Phiên bản Tra cứu)
Hugging Face Spaces Ready
- Chức năng: Chỉ tra cứu (Read-only), không cho phép upload từ UI.
- Dữ liệu: Tự động tải từ Hugging Face Dataset khi khởi động.
"""
# ---------- FIX SQLITE3 CHO CHROMA (QUAN TRỌNG TRÊN HF) ----------
try:
__import__("pysqlite3")
import sys
sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
except ImportError:
pass
# ---------- THƯ VIỆN ----------
import os
import logging
import pickle
import traceback
from typing import List
import gradio as gr
import pandas as pd
import docx2txt
from huggingface_hub import login, snapshot_download, upload_file, upload_folder
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.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
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
# ---------- CẤU HÌNH ----------
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
# Lấy Key từ Environment Variables (Cài đặt trong Settings của Space)
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
HF_TOKEN = os.getenv("HF_TOKEN")
DATA_PATH = "medical_data" # Thư mục chứa dữ liệu gốc (nếu cần rebuild)
DB_PATH = "chroma_db" # Thư mục chứa Vector DB
SPLITS_CACHE = "splits_cache.pkl" # Cache file đã chunk
FORCE_REBUILD_DB = False # Set True nếu muốn buộc tạo lại DB từ đầu
MAX_HISTORY_TURNS = 4 # Số lượt hội thoại được nhớ
# ---------- CẤU HÌNH DATASET PERSISTENT ----------
# 🔴 THAY ĐỔI DÒNG DƯỚI ĐÂY THÀNH USERNAME CỦA BẠN
HF_USERNAME = "PBThuong96"
DATASET_NAME = "deepmed-db"
DATASET_REPO = f"{HF_USERNAME}/{DATASET_NAME}"
# ---------- XÁC THỰC HUGGING FACE ----------
if HF_TOKEN:
login(token=HF_TOKEN)
logging.info("✅ Logged into Hugging Face Hub")
else:
logging.warning("⚠️ HF_TOKEN not found. Dataset persistence disabled.")
# ---------- HÀM PERSISTENT STORAGE (Sync Dữ Liệu) ----------
def download_persistent_data():
"""Tải Chroma DB và splits cache từ Dataset về local (gọi khi khởi động)"""
# 1. Kiểm tra Username (Lỗi phổ biến nhất)
if HF_USERNAME == "your-username":
logging.error("❌ CẤU HÌNH LỖI: Bạn chưa đổi 'HF_USERNAME' trong code (dòng 55). Vui lòng sửa lại tên tài khoản HF của bạn.")
return False
if not HF_TOKEN:
logging.warning("⚠️ CHƯA CÓ TOKEN: Biến môi trường 'HF_TOKEN' chưa được cài đặt. Không thể tải dữ liệu.")
return False
logging.info(f"🔄 Bắt đầu đồng bộ dữ liệu từ Dataset: {DATASET_REPO}")
try:
# Tải Chroma DB
if not os.path.exists(DB_PATH):
logging.info("📥 Đang tải Chroma DB từ Dataset...")
snapshot_download(
repo_id=DATASET_REPO,
repo_type="dataset",
# Sử dụng pattern ** để tìm đệ quy, tránh lỗi cấu trúc folder
allow_patterns=f"{DB_PATH}/**",
local_dir=".",
local_dir_use_symlinks=False,
token=HF_TOKEN,
ignore_patterns=["*.gitattributes", "README.md"]
)
# Tải splits cache
if not os.path.exists(SPLITS_CACHE):
logging.info("📥 Đang tải splits_cache.pkl...")
snapshot_download(
repo_id=DATASET_REPO,
repo_type="dataset",
allow_patterns=SPLITS_CACHE,
local_dir=".",
local_dir_use_symlinks=False,
token=HF_TOKEN,
ignore_patterns=["*.gitattributes", "README.md"]
)
# Kiểm tra kết quả sau khi tải
if os.path.exists(DB_PATH) and os.listdir(DB_PATH):
logging.info("✅ Tải dữ liệu thành công!")
return True
else:
logging.warning("⚠️ Đã chạy lệnh tải nhưng thư mục DB vẫn rỗng hoặc không tồn tại. Có thể Dataset của bạn đang trống?")
return False
except Exception as e:
error_msg = str(e)
logging.error(f"❌ LỖI TẢI DỮ LIỆU: {error_msg}")
if "404" in error_msg:
logging.error(f"👉 Không tìm thấy Dataset '{DATASET_REPO}'. Hãy kiểm tra:\n 1. Bạn đã tạo Dataset trên Hugging Face chưa?\n 2. Tên Username và Dataset Name trong code có đúng không?")
elif "401" in error_msg or "403" in error_msg:
logging.error("👉 Lỗi quyền truy cập (Auth). Hãy kiểm tra HF_TOKEN trong Settings của Space.")
return False
def upload_persistent_data():
"""Upload Chroma DB và splits cache lên Dataset (gọi sau khi rebuild DB)"""
if not HF_TOKEN:
return
try:
# Upload Chroma DB
if os.path.exists(DB_PATH):
logging.info("📤 Đang upload Chroma DB lên Dataset...")
upload_folder(
folder_path=DB_PATH,
repo_id=DATASET_REPO,
repo_type="dataset",
path_in_repo=DB_PATH,
token=HF_TOKEN,
ignore_patterns=[".gitattributes", "README.md"]
)
logging.info("✅ Upload Chroma DB thành công.")
# Upload splits cache
if os.path.exists(SPLITS_CACHE):
logging.info("📤 Đang upload splits_cache.pkl...")
upload_file(
path_or_fileobj=SPLITS_CACHE,
path_in_repo=SPLITS_CACHE,
repo_id=DATASET_REPO,
repo_type="dataset",
token=HF_TOKEN
)
logging.info("✅ Upload splits_cache.pkl thành công.")
except Exception as e:
logging.error(f"❌ Upload thất bại: {e}")
# Gọi download ngay khi khởi chạy app
download_persistent_data()
# ---------- XỬ LÝ DOCUMENTS (Hỗ trợ Rebuild Local) ----------
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"--- Quét thư mục: {folder_path} ---")
documents = []
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 số tài liệu gốc: {len(documents)}")
return documents
def load_or_create_splits(raw_docs):
if os.path.exists(SPLITS_CACHE) and not FORCE_REBUILD_DB:
logging.info("--- Load splits từ cache ---")
with open(SPLITS_CACHE, "rb") as f:
return pickle.load(f)
logging.info("--- Tạo splits mới (chunk_size=800, overlap=150) ---")
text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=150)
splits = text_splitter.split_documents(raw_docs)
with open(SPLITS_CACHE, "wb") as f:
pickle.dump(splits, f)
return splits
# ---------- RETRIEVERS & CORE LOGIC ----------
def get_retrievers():
logging.info("--- Tải Embedding Model ---")
embedding_model = HuggingFaceEmbeddings(
model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
)
vectorstore = None
splits = []
# 1. Thử load DB từ Disk (đã được download từ Dataset)
if os.path.exists(DB_PATH) and os.listdir(DB_PATH) and not FORCE_REBUILD_DB:
logging.info("--- Phát hiện Chroma DB cũ, đang tải... ---")
try:
vectorstore = Chroma(
persist_directory=DB_PATH,
embedding_function=embedding_model,
)
if os.path.exists(SPLITS_CACHE):
with open(SPLITS_CACHE, "rb") as f:
splits = pickle.load(f)
else:
# Nếu mất cache, load lại từ raw folder (nếu có)
raw_docs = load_documents_from_folder(DATA_PATH)
if raw_docs:
splits = load_or_create_splits(raw_docs)
except Exception as e:
logging.error(f"Lỗi load Chroma DB: {e}. Tiến hành tạo mới.")
vectorstore = None
# 2. Nếu không load được DB, tạo mới từ folder 'medical_data'
if vectorstore is None:
logging.info("--- Tạo Index dữ liệu mới ---")
raw_docs = load_documents_from_folder(DATA_PATH)
if not raw_docs:
logging.warning("⚠️ Không có tài liệu nào trong thư mục data & không tải được DB từ Dataset.")
return None, None
splits = load_or_create_splits(raw_docs)
vectorstore = Chroma.from_documents(
documents=splits,
embedding=embedding_model,
persist_directory=DB_PATH
)
# Upload dữ liệu mới lên Dataset để lần sau dùng
upload_persistent_data()
# === FAST RETRIEVER (Ensemble, k=8) ===
bm25_fast = BM25Retriever.from_documents(splits)
bm25_fast.k = 8
vector_fast = vectorstore.as_retriever(search_kwargs={"k": 8})
fast_retriever = EnsembleRetriever(
retrievers=[bm25_fast, vector_fast],
weights=[0.5, 0.5]
)
# === DEEP RETRIEVER (bge-reranker-v2-m3) ===
bm25_deep = BM25Retriever.from_documents(splits)
bm25_deep.k = 12
vector_deep = vectorstore.as_retriever(search_kwargs={"k": 12})
ensemble_deep = EnsembleRetriever(
retrievers=[bm25_deep, vector_deep],
weights=[0.5, 0.5]
)
logging.info("--- Tải CrossEncoderReranker (bge-reranker-v2-m3) ---")
reranker_model = HuggingFaceCrossEncoder(
model_name="BAAI/bge-reranker-v2-m3",
model_kwargs={'device': 'cpu', 'low_cpu_mem_usage': True}
)
compressor = CrossEncoderReranker(model=reranker_model, top_n=3)
deep_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=ensemble_deep
)
return fast_retriever, deep_retriever
# ---------- DEEPMED BOT ----------
class DeepMedBot:
def __init__(self):
self.fast_chain = None
self.deep_chain = None
self.ready = False
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
)
if self.fast_retriever and self.deep_retriever:
self._build_chains()
self.ready = True
logging.info("✅ Bot DeepMed đã sẵn sàng!")
else:
logging.warning("⚠️ Không có retriever (Chưa có dữ liệu).")
self.ready = True
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. "
"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ụ: Tư vấn điều trị CHỈ DỰA TRÊN Dữ liệu nội bộ (Context) được cung cấp.\n\n"
"QUY TẮC AN TOÀN:\n"
"1. Nếu thông tin không có trong Context, 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ộ'.\n"
"2. Chỉ đề xuất thuốc có trong danh sách Context.\n"
"3. Mọi khẳng định phải 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 chưa có dữ liệu..."
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:
chat_history.append(HumanMessage(content=str(u)))
chat_history.append(AIMessage(content=str(b)))
active_chain = self.deep_chain if "Chuyên sâu" in mode else self.fast_chain
if not active_chain:
try:
resp = self.llm.invoke([HumanMessage(content=message)])
yield f"⚠️ (Chế độ kiến thức chung - Chưa có DB) {resp.content}"
return
except:
yield "Lỗi kết nối AI hoặc thiếu dữ liệu."
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}")
yield full_response + f"\n\n[Lỗ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)
# Khởi tạo bot
bot = DeepMedBot()
# ---------- GRADIO UI ----------
theme = gr.themes.Soft(
primary_hue="blue",
secondary_hue="emerald",
neutral_hue="gray",
font=gr.themes.GoogleFont("Inter")
)
css = """
footer {visibility: hidden}
.gr-chatbot .user-message {background-color: #e6f7ff}
.gr-chatbot .bot-message {background-color: #f0f2f6}
"""
with gr.Blocks(theme=theme, css=css, title="DeepMed AI") as demo:
gr.Markdown("""
# 🏥 DeepMed AI - Trợ lý Dược lâm sàng
**Hệ thống tra cứu phác đồ, thuốc, bệnh án nội bộ** Chạy trên nền tảng Gemini + RAG với reranker BGE-M3.
""")
with gr.Row():
with gr.Column(scale=4):
mode_select = gr.Radio(
choices=["⚡ Tốc độ (Nhanh)", "🔍 Chuyên sâu"],
value="⚡ Tốc độ (Nhanh)",
label="Chế độ tra cứu",
)
with gr.Column(scale=1):
clear_btn = gr.ClearButton(value="🗑️ Xoá chat", size="sm")
chatbot = gr.Chatbot(
avatar_images=("🧑⚕️", "🤖"),
height=600,
show_copy_button=True,
bubble_full_width=False,
layout="panel"
)
with gr.Row():
msg = gr.Textbox(
placeholder="Nhập câu hỏi (VD: 'Phác đồ điều trị tăng huyết áp?', 'Thuốc Paracetamol giá bao nhiêu?')",
scale=9,
container=False
)
submit = gr.Button("📨 Gửi", variant="primary", scale=1, min_width=100)
with gr.Accordion("📚 Nguồn tham khảo chi tiết", open=False):
ref_markdown = gr.Markdown("_Chưa có nguồn trích dẫn._")
# Xử lý sự kiện
def respond(message, chat_history, mode):
bot_response = ""
for chunk in bot.chat_stream(message, chat_history, mode):
bot_response = chunk
chat_history.append((message, bot_response))
refs = ""
if "📚 **Nguồn tham khảo**" in bot_response:
parts = bot_response.split("---\n📚 **Nguồn tham khảo**")
bot_response = parts[0].strip()
refs = "📚 **Nguồn tham khảo**" + parts[1]
return chat_history, bot_response, refs
submit.click(
respond,
inputs=[msg, chatbot, mode_select],
outputs=[chatbot, msg, ref_markdown]
).then(lambda: "", None, msg)
msg.submit(
respond,
inputs=[msg, chatbot, mode_select],
outputs=[chatbot, msg, ref_markdown]
).then(lambda: "", None, msg)
clear_btn.click(lambda: ([], "", "_Chưa có nguồn trích dẫn._"), None, [chatbot, msg, ref_markdown])
# ---------- KHỞI CHẠY APP ----------
if __name__ == "__main__":
demo.launch() |