PBThuong96 commited on
Commit
3bc8336
·
verified ·
1 Parent(s): fe25486

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +499 -116
app.py CHANGED
@@ -1,162 +1,545 @@
1
- import os
2
- import sys
3
- import logging
4
- import gradio as gr
 
 
 
5
 
6
- # --- 1. SỬA LỖI SQLITE TRÊN HUGGING FACE ---
7
  try:
8
  __import__("pysqlite3")
 
9
  sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
10
  except ImportError:
11
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- import chromadb
14
  from langchain_google_genai import ChatGoogleGenerativeAI
15
  from langchain_chroma import Chroma
16
- from langchain_huggingface import HuggingFaceEmbeddings
17
-
18
- # --- IMPORT ĐƠN GIẢN HÓA (LOẠI BỎ CÁC MODULE GÂY LỖI _type) ---
19
- # Chỉ sử dụng các thành phần cốt lõi ổn định nhất
20
- from langchain.chains import create_retrieval_chain
 
 
 
21
  from langchain.chains.combine_documents import create_stuff_documents_chain
22
- from langchain_core.prompts import ChatPromptTemplate
 
23
  from langchain_core.documents import Document
 
 
 
 
24
 
25
- # --- CẤU HÌNH ---
26
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
 
 
 
27
  DB_PATH = "chroma_db"
 
 
 
28
 
29
- logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
 
 
 
30
 
31
- def get_category_vn_name(cat_code):
32
- return {
33
- "drug_info": "💊 Thuốc Nội Bộ",
34
- "local_regimen": "🏥 Phác Đồ Thanh Ba",
35
- "moh_regimen": "🏛️ Bộ Y Tế",
36
- "association": "🌐 Hiệp Hội"
37
- }.get(cat_code, "Khác")
38
 
39
- # --- 2. LOAD DB (VECTOR SEARCH THUẦN TÚY - ỔN ĐỊNH 100%) ---
40
- def get_retrievers():
41
- if not os.path.exists(DB_PATH):
42
- raise FileNotFoundError(f"❌ LỖI: Không tìm thấy thư mục '{DB_PATH}'. Bạn đã upload folder này vào phần Files chưa?")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- logging.info("--- Đang tải dữ liệu từ ChromaDB... ---")
45
- embedding = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
46
- vectorstore = Chroma(persist_directory=DB_PATH, embedding_function=embedding)
47
-
48
- # Kiểm tra dữ liệu
49
  try:
50
- all_data = vectorstore.get()
51
- if not all_data['documents']:
52
- raise ValueError("Database rỗng")
53
- logging.info(f"✅ Đã tải thành công {len(all_data['documents'])} tài liệu từ Database.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  except Exception as e:
55
- logging.error(f"Lỗi đọc dữ liệu Chroma: {e}")
56
- raise ValueError(f"Không thể đọc dữ liệu từ ChromaDB: {e}")
57
-
58
- # --- TẠO RETRIEVER ĐƠN GIẢN ---
59
- # Thay vì dùng Ensemble/Reranker (dễ lỗi), ta dùng Vector Search trực tiếp.
60
-
61
- # Mode 1: FAST (Tìm kiếm Thuốc - Lấy 5 kết quả sát nhất)
62
- logging.info("--- Khởi tạo Fast Retriever (Vector Only) ---")
63
- fast_retriever = vectorstore.as_retriever(
64
- search_kwargs={
65
- "k": 5,
66
- "filter": {"category": "drug_info"}
67
- }
68
- )
69
 
70
- # Mode 2: DEEP (Tìm kiếm Phác đồ - Lấy 15 kết quả sát nhất)
71
- # Tăng k lên để bù đắp việc thiếu Reranker
72
- logging.info("--- Khởi tạo Deep Retriever (Vector Only) ---")
73
- cats = ["local_regimen", "moh_regimen", "association", "drug_info"]
74
- deep_retriever = vectorstore.as_retriever(
75
- search_kwargs={
76
- "k": 15,
77
- "filter": {"category": {"$in": cats}}
78
- }
79
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  return fast_retriever, deep_retriever
82
 
83
- # --- 3. BOT LOGIC ---
84
  class DeepMedBot:
85
  def __init__(self):
 
 
86
  self.ready = False
87
- self.init_error = "Đang khởi động..."
88
 
89
  if not GOOGLE_API_KEY:
90
- self.init_error = " LỖI: Chưa cấu hình GOOGLE_API_KEY trong Settings."
91
  return
92
-
93
  try:
94
  self.fast_retriever, self.deep_retriever = get_retrievers()
95
- self.llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0.2, google_api_key=GOOGLE_API_KEY)
96
- self._build_chains()
97
- self.ready = True
98
- self.init_error = ""
99
- logging.info("✅ BOT KHỞI ĐỘNG THÀNH CÔNG (CHẾ ĐỘ VECTOR STABLE)!")
 
 
 
 
 
 
 
 
 
 
100
  except Exception as e:
101
- self.init_error = f" LỖI KHỞI TẠO: {str(e)}"
102
- logging.error(self.init_error)
103
-
104
  def _build_chains(self):
105
- # Prompt Nhanh
106
- fast_sys = (
107
- "Bạn Dược Lâm sàng.\n"
108
- "Tra cứu [💊 Thuốc Nội Bộ] trả lời bằng **Bảng Markdown**:\n"
109
- "| Tên thuốc | Hoạt chất | Hàm lượng | Đơn vị | Ghi chú |\n"
110
- "| --- | --- | --- | --- | --- |\n"
111
- "Nếu không thấy, báo: '❌ Không tìm thấy trong kho'."
112
- "Context:\n{context}"
113
  )
114
- fast_chain = create_stuff_documents_chain(self.llm, ChatPromptTemplate.from_messages([("system", fast_sys), ("human", "{input}")]))
115
- self.fast_chain = create_retrieval_chain(self.fast_retriever, fast_chain)
 
 
 
116
 
117
- # Prompt Chuyên sâu
118
- deep_sys = (
119
- "Bạn là Bác Trưởng khoa.\n"
120
- "1. **Tìm phác đồ:** Ưu tiên tuyệt đối [🏥 Phác Đồ Thanh Ba]. Nếu không mới dùng [Bộ Y Tế].\n"
121
- "2. **Đối chiếu thuốc:** Kiểm tra thuốc trong phác đồ có trong [💊 Thuốc Nội Bộ] không.\n"
122
- "3. **Định dạng trả lời:**\n"
123
- " - Chẩn đoán/Nguyên tắc.\n"
124
- " - Phác đồ (Ghi nguồn).\n"
125
- " - **Bảng kê đơn:**\n"
126
- " | Tên thuốc | Liều dùng | Có trong kho? | Thay thế |\n"
127
- " | --- | --- | --- | --- |\n"
128
  "Context:\n{context}"
129
  )
130
- deep_chain = create_stuff_documents_chain(self.llm, ChatPromptTemplate.from_messages([("system", deep_sys), ("human", "{input}")]))
131
- self.deep_chain = create_retrieval_chain(self.deep_retriever, deep_chain)
132
-
133
- def chat(self, msg, history, mode):
134
- if not self.ready:
135
- return f"⚠️ HỆ THỐNG GẶP LỖI.\n\nChi tiết lỗi:\n{self.init_error}\n\nHãy thử Restart Space trong phần Settings."
136
-
137
- chain = self.deep_chain if mode == "Chuyên sâu" else self.fast_chain
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  try:
139
- res = chain.invoke({"input": msg})
140
- ans = res['answer']
141
- if 'context' in res and res['context']:
142
- refs = list(set([f"- [{get_category_vn_name(d.metadata.get('category'))}] {d.metadata.get('source')}" for d in res['context']]))
143
- ans += "\n\n---\n📚 **Nguồn:**\n" + "\n".join(refs)
144
- return ans
 
 
 
 
 
 
 
145
  except Exception as e:
146
- return f"Lỗi khi trả lời: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
 
148
  bot = DeepMedBot()
149
 
150
- def respond(message, history, mode):
151
- return bot.chat(message, history, mode)
152
-
153
- demo = gr.ChatInterface(
154
- fn=respond,
155
- additional_inputs=[gr.Radio(["Tra cứu nhanh (Chỉ thuốc)", "Chuyên sâu"], value="Tra cứu nhanh (Chỉ thuốc)", label="Chế độ")],
156
- title="TTYT Thanh Ba - Hỗ trợ Lâm sàng",
157
- description="Hệ thống tra cứu Phác đồ & Thuốc nội bộ.",
158
- css=".gradio-container {min_height: 600px}"
159
  )
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  if __name__ == "__main__":
162
  demo.launch()
 
1
+ """
2
+ DeepMed AI - Trợ lý Dược lâm sàng
3
+ Hugging Face Spaces Ready (Free Tier)
4
+ - Persistent storage bằng Dataset (không mất DB khi restart)
5
+ - Reranker BGE-M3 (giữ nguyên theo yêu cầu)
6
+ - Giao diện Gradio nâng cao
7
+ """
8
 
9
+ # ---------- FIX SQLITE3 CHO CHROMA (QUAN TRỌNG TRÊN HF) ----------
10
  try:
11
  __import__("pysqlite3")
12
+ import sys
13
  sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
14
  except ImportError:
15
+ pass
16
+
17
+ # ---------- THƯ VIỆN ----------
18
+ import os
19
+ import logging
20
+ import pickle
21
+ import shutil
22
+ import traceback
23
+ from typing import List
24
+
25
+ import gradio as gr
26
+ import pandas as pd
27
+ import docx2txt
28
+ from huggingface_hub import HfApi, login, snapshot_download, upload_file, upload_folder
29
 
 
30
  from langchain_google_genai import ChatGoogleGenerativeAI
31
  from langchain_chroma import Chroma
32
+ from langchain_community.document_loaders import PyPDFLoader
33
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
34
+ from langchain_community.retrievers import BM25Retriever
35
+ from langchain.retrievers.ensemble import EnsembleRetriever
36
+ from langchain.retrievers import ContextualCompressionRetriever
37
+ from langchain.retrievers.document_compressors import CrossEncoderReranker
38
+ from langchain_community.cross_encoders import HuggingFaceCrossEncoder
39
+ from langchain.chains import create_retrieval_chain, create_history_aware_retriever
40
  from langchain.chains.combine_documents import create_stuff_documents_chain
41
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
42
+ from langchain_core.messages import HumanMessage, AIMessage
43
  from langchain_core.documents import Document
44
+ from langchain_huggingface import HuggingFaceEmbeddings
45
+
46
+ # ---------- CẤU HÌNH ----------
47
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
48
 
 
49
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
50
+ HF_TOKEN = os.getenv("HF_TOKEN")
51
+
52
+ DATA_PATH = "medical_data"
53
  DB_PATH = "chroma_db"
54
+ SPLITS_CACHE = "splits_cache.pkl"
55
+ FORCE_REBUILD_DB = False
56
+ MAX_HISTORY_TURNS = 4
57
 
58
+ # ---------- CẤU HÌNH DATASET PERSISTENT (MIỄN PHÍ) ----------
59
+ HF_USERNAME = "your-username" # 🔴 THAY BẰNG TÊN CỦA BẠN
60
+ DATASET_NAME = "deepmed-db"
61
+ DATASET_REPO = f"datasets/{HF_USERNAME}/{DATASET_NAME}"
62
 
63
+ # ---------- XÁC THỰC HUGGING FACE ----------
64
+ if HF_TOKEN:
65
+ login(token=HF_TOKEN)
66
+ logging.info(" Logged into Hugging Face Hub")
67
+ else:
68
+ logging.warning("⚠️ HF_TOKEN not found. Dataset persistence disabled.")
 
69
 
70
+ # ---------- HÀM PERSISTENT STORAGE ----------
71
+ def download_persistent_data():
72
+ """Tải Chroma DB và splits cache từ Dataset về local (gọi khi khởi động)"""
73
+ if not HF_TOKEN:
74
+ return False
75
+ try:
76
+ # Tải Chroma DB
77
+ if not os.path.exists(DB_PATH):
78
+ logging.info("📥 Đang tải Chroma DB từ Dataset...")
79
+ snapshot_download(
80
+ repo_id=DATASET_REPO,
81
+ allow_patterns=f"{DB_PATH}/*",
82
+ local_dir=".",
83
+ local_dir_use_symlinks=False,
84
+ token=HF_TOKEN,
85
+ ignore_patterns=["*.gitattributes", "README.md"]
86
+ )
87
+ # Nếu tải về thư mục tạm (tên là DB_PATH) thì move vào đúng
88
+ if os.path.exists(DB_PATH):
89
+ logging.info("✅ Đã tải Chroma DB.")
90
+
91
+ # Tải splits cache
92
+ if not os.path.exists(SPLITS_CACHE):
93
+ logging.info("📥 Đang tải splits_cache.pkl...")
94
+ snapshot_download(
95
+ repo_id=DATASET_REPO,
96
+ allow_patterns=SPLITS_CACHE,
97
+ local_dir=".",
98
+ local_dir_use_symlinks=False,
99
+ token=HF_TOKEN,
100
+ ignore_patterns=["*.gitattributes", "README.md"]
101
+ )
102
+ if os.path.exists(SPLITS_CACHE):
103
+ logging.info("✅ Đã tải splits_cache.pkl.")
104
+ return True
105
+ except Exception as e:
106
+ logging.warning(f"⚠️ Không tải được dữ liệu từ Dataset (có thể lần đầu chạy): {e}")
107
+ return False
108
 
109
+ def upload_persistent_data():
110
+ """Upload Chroma DB và splits cache lên Dataset (gọi sau khi rebuild DB)"""
111
+ if not HF_TOKEN:
112
+ return
 
113
  try:
114
+ # Upload Chroma DB
115
+ if os.path.exists(DB_PATH):
116
+ logging.info("📤 Đang upload Chroma DB lên Dataset...")
117
+ upload_folder(
118
+ folder_path=DB_PATH,
119
+ repo_id=DATASET_REPO,
120
+ repo_type="dataset",
121
+ path_in_repo=DB_PATH,
122
+ token=HF_TOKEN,
123
+ ignore_patterns=[".gitattributes", "README.md"]
124
+ )
125
+ logging.info("✅ Upload Chroma DB thành công.")
126
+
127
+ # Upload splits cache
128
+ if os.path.exists(SPLITS_CACHE):
129
+ logging.info("📤 Đang upload splits_cache.pkl...")
130
+ upload_file(
131
+ path_or_fileobj=SPLITS_CACHE,
132
+ path_in_repo=SPLITS_CACHE,
133
+ repo_id=DATASET_REPO,
134
+ repo_type="dataset",
135
+ token=HF_TOKEN
136
+ )
137
+ logging.info("✅ Upload splits_cache.pkl thành công.")
138
  except Exception as e:
139
+ logging.error(f" Upload thất bại: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ # Gọi download ngay khi khởi chạy app
142
+ download_persistent_data()
143
+
144
+ # ---------- XỬ DOCUMENTS ----------
145
+ def process_excel_file(file_path: str, filename: str) -> List[Document]:
146
+ docs = []
147
+ try:
148
+ if file_path.endswith(".csv"):
149
+ df = pd.read_csv(file_path)
150
+ else:
151
+ df = pd.read_excel(file_path)
152
+ df.dropna(how='all', inplace=True)
153
+ df.fillna("Không có thông tin", inplace=True)
154
+ for idx, row in df.iterrows():
155
+ content_parts = []
156
+ for col_name, val in row.items():
157
+ clean_val = str(val).strip()
158
+ if clean_val and clean_val.lower() != "nan":
159
+ content_parts.append(f"{col_name}: {clean_val}")
160
+ if content_parts:
161
+ page_content = f"Dữ liệu từ file {filename} (Dòng {idx+1}):\n" + "\n".join(content_parts)
162
+ metadata = {"source": filename, "row": idx+1, "type": "excel_record"}
163
+ docs.append(Document(page_content=page_content, metadata=metadata))
164
+ except Exception as e:
165
+ logging.error(f"Lỗi xử lý Excel {filename}: {e}")
166
+ return docs
167
+
168
+ def load_documents_from_folder(folder_path: str) -> List[Document]:
169
+ logging.info(f"--- Quét thư mục: {folder_path} ---")
170
+ documents = []
171
+ if not os.path.exists(folder_path):
172
+ os.makedirs(folder_path, exist_ok=True)
173
+ return []
174
+ for root, _, files in os.walk(folder_path):
175
+ for filename in files:
176
+ file_path = os.path.join(root, filename)
177
+ filename_lower = filename.lower()
178
+ try:
179
+ if filename_lower.endswith(".pdf"):
180
+ loader = PyPDFLoader(file_path)
181
+ docs = loader.load()
182
+ for d in docs:
183
+ d.metadata["source"] = filename
184
+ documents.extend(docs)
185
+ elif filename_lower.endswith(".docx"):
186
+ text = docx2txt.process(file_path)
187
+ if text.strip():
188
+ documents.append(Document(page_content=text, metadata={"source": filename}))
189
+ elif filename_lower.endswith((".xlsx", ".xls", ".csv")):
190
+ excel_docs = process_excel_file(file_path, filename)
191
+ documents.extend(excel_docs)
192
+ elif filename_lower.endswith((".txt", ".md")):
193
+ with open(file_path, "r", encoding="utf-8") as f:
194
+ text = f.read()
195
+ if text.strip():
196
+ documents.append(Document(page_content=text, metadata={"source": filename}))
197
+ except Exception as e:
198
+ logging.error(f"Lỗi đọc file {filename}: {e}")
199
+ logging.info(f"Tổng số tài liệu gốc: {len(documents)}")
200
+ return documents
201
 
202
+ def load_or_create_splits(raw_docs):
203
+ """Load splits từ cache nếu có, nếu không thì chunk và cache lại"""
204
+ if os.path.exists(SPLITS_CACHE) and not FORCE_REBUILD_DB:
205
+ logging.info("--- Load splits từ cache ---")
206
+ with open(SPLITS_CACHE, "rb") as f:
207
+ return pickle.load(f)
208
+ logging.info("--- Tạo splits mới (chunk_size=800, overlap=150) ---")
209
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=150)
210
+ splits = text_splitter.split_documents(raw_docs)
211
+ with open(SPLITS_CACHE, "wb") as f:
212
+ pickle.dump(splits, f)
213
+ return splits
214
+
215
+ # ---------- RETRIEVERS ----------
216
+ def get_retrievers():
217
+ logging.info("--- Tải Embedding Model ---")
218
+ embedding_model = HuggingFaceEmbeddings(
219
+ model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
220
+ )
221
+
222
+ # Khởi tạo Chroma (load nếu có, tạo mới nếu không)
223
+ vectorstore = None
224
+ splits = []
225
+
226
+ if os.path.exists(DB_PATH) and os.listdir(DB_PATH) and not FORCE_REBUILD_DB:
227
+ logging.info("--- Phát hiện Chroma DB cũ, đang tải... ---")
228
+ try:
229
+ vectorstore = Chroma(
230
+ persist_directory=DB_PATH,
231
+ embedding_function=embedding_model,
232
+ )
233
+ # Load splits từ cache
234
+ if os.path.exists(SPLITS_CACHE):
235
+ with open(SPLITS_CACHE, "rb") as f:
236
+ splits = pickle.load(f)
237
+ else:
238
+ # Trường hợp hiếm: có DB nhưng mất cache, phải load lại raw
239
+ raw_docs = load_documents_from_folder(DATA_PATH)
240
+ if raw_docs:
241
+ splits = load_or_create_splits(raw_docs)
242
+ except Exception as e:
243
+ logging.error(f"Lỗi load Chroma DB: {e}. Tiến hành tạo mới.")
244
+ vectorstore = None
245
+
246
+ if vectorstore is None:
247
+ logging.info("--- Tạo Index dữ liệu mới ---")
248
+ raw_docs = load_documents_from_folder(DATA_PATH)
249
+ if not raw_docs:
250
+ logging.warning("⚠️ Không có tài liệu nào trong thư mục data.")
251
+ return None, None
252
+ splits = load_or_create_splits(raw_docs)
253
+ vectorstore = Chroma.from_documents(
254
+ documents=splits,
255
+ embedding=embedding_model,
256
+ persist_directory=DB_PATH
257
+ )
258
+ # Upload dữ liệu mới lên Dataset
259
+ upload_persistent_data()
260
+
261
+ # === FAST RETRIEVER (Ensemble, k=8) ===
262
+ bm25_fast = BM25Retriever.from_documents(splits)
263
+ bm25_fast.k = 8
264
+ vector_fast = vectorstore.as_retriever(search_kwargs={"k": 8})
265
+ fast_retriever = EnsembleRetriever(
266
+ retrievers=[bm25_fast, vector_fast],
267
+ weights=[0.5, 0.5]
268
+ )
269
+
270
+ # === DEEP RETRIEVER (GIỮ NGUYÊN bge-reranker-v2-m3) ===
271
+ bm25_deep = BM25Retriever.from_documents(splits)
272
+ bm25_deep.k = 12
273
+ vector_deep = vectorstore.as_retriever(search_kwargs={"k": 12})
274
+ ensemble_deep = EnsembleRetriever(
275
+ retrievers=[bm25_deep, vector_deep],
276
+ weights=[0.5, 0.5]
277
+ )
278
+
279
+ logging.info("--- Tải CrossEncoderReranker (bge-reranker-v2-m3) ---")
280
+ reranker_model = HuggingFaceCrossEncoder(
281
+ model_name="BAAI/bge-reranker-v2-m3",
282
+ model_kwargs={'device': 'cpu', 'low_cpu_mem_usage': True}
283
+ )
284
+ compressor = CrossEncoderReranker(model=reranker_model, top_n=3)
285
+ deep_retriever = ContextualCompressionRetriever(
286
+ base_compressor=compressor,
287
+ base_retriever=ensemble_deep
288
+ )
289
+
290
  return fast_retriever, deep_retriever
291
 
292
+ # ---------- DEEPMED BOT ----------
293
  class DeepMedBot:
294
  def __init__(self):
295
+ self.fast_chain = None
296
+ self.deep_chain = None
297
  self.ready = False
298
+ self.fallback_llm = None
299
 
300
  if not GOOGLE_API_KEY:
301
+ logging.error("⚠️ Thiếu GOOGLE_API_KEY!")
302
  return
303
+
304
  try:
305
  self.fast_retriever, self.deep_retriever = get_retrievers()
306
+ self.llm = ChatGoogleGenerativeAI(
307
+ model="gemini-1.5-flash", # ổn định và nhanh
308
+ temperature=0.2,
309
+ google_api_key=GOOGLE_API_KEY,
310
+ convert_system_message_to_human=True
311
+ )
312
+ self.fallback_llm = self.llm
313
+
314
+ if self.fast_retriever and self.deep_retriever:
315
+ self._build_chains()
316
+ self.ready = True
317
+ logging.info("✅ Bot DeepMed đã sẵn sàng với reranker v2-m3!")
318
+ else:
319
+ logging.warning("⚠️ Không có retriever, chỉ dùng kiến thức nền.")
320
+ self.ready = True
321
  except Exception as e:
322
+ logging.error(f"🔥 Lỗi khởi tạo bot: {e}")
323
+ logging.debug(traceback.format_exc())
324
+
325
  def _build_chains(self):
326
+ # Prompt viết lại câu hỏi
327
+ context_system_prompt = (
328
+ "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 "
329
+ "thành một câu hoàn chỉnh để tìm kiếm thông tin. "
330
+ "CHỈ TRẢ VỀ CÂU HỎI ĐÃ VIẾT LẠI, KHÔNG TRẢ LỜI."
 
 
 
331
  )
332
+ context_prompt = ChatPromptTemplate.from_messages([
333
+ ("system", context_system_prompt),
334
+ MessagesPlaceholder("chat_history"),
335
+ ("human", "{input}"),
336
+ ])
337
 
338
+ # Prompt trả lời
339
+ qa_system_prompt = (
340
+ "Bạn là 'DeepMed-AI' - Trợ lý Dược lâm sàng chuyên nghiệp.\n"
341
+ "Nhiệm vụ: vấn điều trị CHỈ DỰA TRÊN Dữ liệu nội bộ (Context) được cung cấp.\n\n"
342
+ "QUY TẮC AN TOÀN:\n"
343
+ "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"
344
+ "2. Chỉ đề xuất thuốc có trong danh sách Context.\n"
345
+ "3. Mọi khẳng định phải trích dẫn từ Context.\n\n"
 
 
 
346
  "Context:\n{context}"
347
  )
348
+ qa_prompt = ChatPromptTemplate.from_messages([
349
+ ("system", qa_system_prompt),
350
+ MessagesPlaceholder("chat_history"),
351
+ ("human", "{input}"),
352
+ ])
353
+
354
+ question_answer_chain = create_stuff_documents_chain(self.llm, qa_prompt)
355
+
356
+ history_aware_fast = create_history_aware_retriever(self.llm, self.fast_retriever, context_prompt)
357
+ self.fast_chain = create_retrieval_chain(history_aware_fast, question_answer_chain)
358
+
359
+ history_aware_deep = create_history_aware_retriever(self.llm, self.deep_retriever, context_prompt)
360
+ self.deep_chain = create_retrieval_chain(history_aware_deep, question_answer_chain)
361
+
362
+ def chat_stream(self, message: str, history: list, mode: str):
363
+ if not self.ready:
364
+ yield "Hệ thống đang khởi động hoặc gặp lỗi cấu hình..."
365
+ return
366
+
367
+ # Xử lý lịch sử
368
+ chat_history = []
369
+ if history:
370
+ for turn in history[-MAX_HISTORY_TURNS:]:
371
+ if isinstance(turn, (list, tuple)) and len(turn) == 2:
372
+ u, b = turn
373
+ if u and b and str(u).strip() and str(b).strip():
374
+ chat_history.append(HumanMessage(content=str(u)))
375
+ chat_history.append(AIMessage(content=str(b)))
376
+
377
+ active_chain = self.deep_chain if "Chuyên sâu" in mode else self.fast_chain
378
+
379
+ if not active_chain:
380
+ try:
381
+ resp = self.llm.invoke([HumanMessage(content=message)])
382
+ yield f"⚠️ (Chế độ kiến thức chung) {resp.content}"
383
+ return
384
+ except:
385
+ yield "Lỗi: Không thể kết nối với AI. Vui lòng kiểm tra API Key."
386
+ return
387
+
388
+ full_response = ""
389
+ retrieved_docs = []
390
+
391
  try:
392
+ for chunk in active_chain.stream({"input": message, "chat_history": chat_history}):
393
+ if "answer" in chunk:
394
+ full_response += chunk["answer"]
395
+ yield full_response
396
+ elif "context" in chunk:
397
+ retrieved_docs = chunk["context"]
398
+
399
+ # Thêm phần nguồn tham khảo
400
+ if retrieved_docs:
401
+ refs = self._build_references_text(retrieved_docs)
402
+ if refs:
403
+ full_response += f"\n\n---\n📚 **Nguồn tham khảo ({mode}):**\n{refs}"
404
+ yield full_response
405
  except Exception as e:
406
+ logging.error(f"Lỗi khi chat: {e}")
407
+ logging.error(traceback.format_exc())
408
+ if not full_response:
409
+ try:
410
+ yield "⚠️ Gặp lỗi truy xuất dữ liệu. Đang chuyển sang chế độ trả lời nhanh...\n\n"
411
+ fallback_resp = self.llm.invoke([HumanMessage(content=message)])
412
+ yield fallback_resp.content
413
+ except:
414
+ yield f"Đã xảy ra lỗi hệ thống. Vui lòng thử lại. (Lỗi: {str(e)})"
415
+ else:
416
+ yield full_response + f"\n\n[Lỗi ngắt kết nối: {str(e)}]"
417
+
418
+ @staticmethod
419
+ def _build_references_text(docs) -> str:
420
+ lines = []
421
+ seen = set()
422
+ for doc in docs:
423
+ src = doc.metadata.get("source", "Tài liệu")
424
+ row_info = f"(Dòng {doc.metadata['row']})" if "row" in doc.metadata else ""
425
+ type_info = " [Kho thuốc]" if doc.metadata.get("type") == "excel_record" else ""
426
+ ref_str = f"- {src}{type_info} {row_info}"
427
+ if ref_str not in seen:
428
+ lines.append(ref_str)
429
+ seen.add(ref_str)
430
+ return "\n".join(lines)
431
 
432
+ # Khởi tạo bot
433
  bot = DeepMedBot()
434
 
435
+ # ---------- GRADIO UI ----------
436
+ theme = gr.themes.Soft(
437
+ primary_hue="blue",
438
+ secondary_hue="emerald",
439
+ neutral_hue="gray",
440
+ font=gr.themes.GoogleFont("Inter")
 
 
 
441
  )
442
 
443
+ css = """
444
+ footer {visibility: hidden}
445
+ .gr-chatbot .user-message {background-color: #e6f7ff}
446
+ .gr-chatbot .bot-message {background-color: #f0f2f6}
447
+ """
448
+
449
+ def chat_handler(message, history, mode):
450
+ """Generator cho Gradio ChatInterface"""
451
+ yield from bot.chat_stream(message, history, mode)
452
+
453
+ with gr.Blocks(theme=theme, css=css, title="DeepMed AI") as demo:
454
+ gr.Markdown("""
455
+ # 🏥 DeepMed AI - Trợ lý Dược lâm sàng
456
+ **Hệ thống tra cứu phác đồ, thuốc, bệnh án nội bộ**
457
+ Chạy trên nền tảng Gemini + RAG với reranker BGE-M3.
458
+ """)
459
+
460
+ with gr.Row():
461
+ with gr.Column(scale=4):
462
+ mode_select = gr.Radio(
463
+ choices=["⚡ Tốc độ (Nhanh)", "🔍 Chuyên sâu (Chính xác)"],
464
+ value="⚡ Tốc độ (Nhanh)",
465
+ label="Chế độ tra cứu",
466
+ info="'Tốc độ' dùng ensemble, 'Chuyên sâu' dùng thêm reranker BGE-M3."
467
+ )
468
+ with gr.Column(scale=1):
469
+ clear_btn = gr.ClearButton(value="🗑️ Xoá chat", size="sm")
470
+
471
+ # Chatbot với avatar
472
+ chatbot = gr.Chatbot(
473
+ avatar_images=("🧑‍⚕️", "🤖"),
474
+ height=550,
475
+ show_copy_button=True,
476
+ bubble_full_width=False,
477
+ layout="panel"
478
+ )
479
+
480
+ with gr.Row():
481
+ msg = gr.Textbox(
482
+ 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?')",
483
+ scale=9,
484
+ container=False
485
+ )
486
+ submit = gr.Button("📨 Gửi", variant="primary", scale=1, min_width=100)
487
+
488
+ # Accordion hiển thị nguồn tham khảo
489
+ with gr.Accordion("📚 Nguồn tham khảo", open=False):
490
+ ref_markdown = gr.Markdown("_Chưa có nguồn trích dẫn._")
491
+
492
+ # Tab quản lý dữ liệu (upload file)
493
+ with gr.Tab("📂 Quản lý dữ liệu"):
494
+ gr.Markdown("Tải lên tài liệu PDF, Excel, Word, TXT để cập nhật cơ sở tri thức.")
495
+ file_upload = gr.File(
496
+ file_count="multiple",
497
+ label="Chọn file",
498
+ file_types=[".pdf", ".xlsx", ".xls", ".csv", ".docx", ".txt", ".md"]
499
+ )
500
+ upload_btn = gr.Button("⬆️ Cập nhật dữ liệu", variant="secondary")
501
+ upload_status = gr.Textbox(label="Trạng thái", interactive=False)
502
+
503
+ def upload_files(files):
504
+ if not files:
505
+ return "❌ Chưa chọn file nào."
506
+ os.makedirs(DATA_PATH, exist_ok=True)
507
+ for f in files:
508
+ # Lưu file vào thư mục dữ liệu
509
+ f.save(os.path.join(DATA_PATH, f.name))
510
+ # Cảnh báo: cần rebuild lại DB để áp dụng dữ liệu mới
511
+ return "✅ Đã tải lên. Vui lòng **khởi động lại Space** (Settings → Restart this Space) để rebuild database."
512
+
513
+ upload_btn.click(upload_files, inputs=file_upload, outputs=upload_status)
514
+
515
+ # Xử lý chat
516
+ def respond(message, chat_history, mode):
517
+ bot_response = ""
518
+ for chunk in bot.chat_stream(message, chat_history, mode):
519
+ bot_response = chunk
520
+ chat_history.append((message, bot_response))
521
+ # Trích xuất phần nguồn tham khảo từ bot_response để hiển thị riêng
522
+ refs = ""
523
+ if "📚 **Nguồn tham khảo**" in bot_response:
524
+ parts = bot_response.split("---\n📚 **Nguồn tham khảo**")
525
+ bot_response = parts[0].strip()
526
+ refs = "📚 **Nguồn tham khảo**" + parts[1]
527
+ return chat_history, bot_response, refs
528
+
529
+ submit.click(
530
+ respond,
531
+ inputs=[msg, chatbot, mode_select],
532
+ outputs=[chatbot, msg, ref_markdown]
533
+ ).then(lambda: "", None, msg) # Xóa textbox sau khi gửi
534
+
535
+ msg.submit(
536
+ respond,
537
+ inputs=[msg, chatbot, mode_select],
538
+ outputs=[chatbot, msg, ref_markdown]
539
+ ).then(lambda: "", None, msg)
540
+
541
+ clear_btn.click(lambda: ([], "", "_Chưa có nguồn trích dẫn._"), None, [chatbot, msg, ref_markdown])
542
+
543
+ # ---------- KHỞI CHẠY APP ----------
544
  if __name__ == "__main__":
545
  demo.launch()