PBThuong96 commited on
Commit
da32c1e
·
verified ·
1 Parent(s): e410773

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +228 -171
app.py CHANGED
@@ -3,8 +3,9 @@ import sys
3
  sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
4
 
5
  import os
 
6
  import logging
7
- import traceback
8
  import gradio as gr
9
  import pandas as pd
10
  import docx2txt
@@ -13,251 +14,307 @@ from langchain_google_genai import ChatGoogleGenerativeAI
13
  from langchain_chroma import Chroma
14
  from langchain_community.document_loaders import PyPDFLoader
15
  from langchain_text_splitters import RecursiveCharacterTextSplitter
16
- from langchain_community.retrievers import BM25Retriever
17
- from langchain.retrievers.ensemble import EnsembleRetriever
18
- from langchain.chains import create_retrieval_chain, create_history_aware_retriever
19
- from langchain.chains.combine_documents import create_stuff_documents_chain
20
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
21
  from langchain_core.messages import HumanMessage, AIMessage
22
  from langchain_core.documents import Document
 
23
  from langchain_huggingface import HuggingFaceEmbeddings
 
24
 
25
- from langchain.retrievers import ContextualCompressionRetriever
26
- from langchain.retrievers.document_compressors import CrossEncoderReranker
27
- from langchain_community.cross_encoders import HuggingFaceCrossEncoder
28
-
29
- GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
30
  DATA_PATH = "medical_data"
31
  DB_PATH = "chroma_db"
32
- MAX_HISTORY_TURNS = 6
33
-
34
- USE_BM25 = True
35
- USE_MMR = True
36
 
37
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
38
 
39
- def load_documents_from_folder(folder_path: str) -> list[Document]:
40
- logging.info(f"--- Bắt đầu quét thư mục: {folder_path} ---")
41
- documents: list[Document] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  if not os.path.exists(folder_path):
43
  os.makedirs(folder_path, exist_ok=True)
44
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  for root, _, files in os.walk(folder_path):
46
  for filename in files:
 
 
47
  file_path = os.path.join(root, filename)
48
- filename_lower = filename.lower()
49
- try:
50
- if filename_lower.endswith(".pdf"):
51
- loader = PyPDFLoader(file_path)
52
- docs = loader.load()
53
- for d in docs: d.metadata["source"] = filename
54
- documents.extend(docs)
55
- elif filename_lower.endswith(".docx"):
56
- text = docx2txt.process(file_path)
57
- if text.strip(): documents.append(Document(page_content=text, metadata={"source": filename}))
58
- elif filename_lower.endswith((".xlsx", ".xls", ".csv")):
59
- # Xử lý đơn giản cho excel/csv
60
- if filename_lower.endswith(".csv"): df = pd.read_csv(file_path)
61
- else: df = pd.read_excel(file_path)
62
- text_data = "\n".join([" | ".join(f"{c}:{v}" for c,v in r.items() if pd.notna(v)) for _, r in df.iterrows()])
63
- if text_data.strip(): documents.append(Document(page_content=text_data, metadata={"source": filename}))
64
- elif filename_lower.endswith((".txt", ".md")):
65
- with open(file_path, "r", encoding="utf-8") as f: text = f.read()
66
- if text.strip(): documents.append(Document(page_content=text, metadata={"source": filename}))
67
- except Exception as e:
68
- logging.error(f"Lỗi file {filename}: {e}")
69
- return documents
70
-
71
- def build_vectorstore_and_corpus(embedding_model):
72
- from shutil import rmtree
73
- splits: list[Document] = []
74
- vectorstore = None
75
-
76
- if os.path.exists(DB_PATH) and os.listdir(DB_PATH):
77
- try:
78
- vectorstore = Chroma(persist_directory=DB_PATH, embedding_function=embedding_model)
79
- existing = vectorstore.get()
80
- if existing.get("documents"):
81
- for text, meta in zip(existing["documents"], existing["metadatas"]):
82
- splits.append(Document(page_content=text, metadata=meta))
83
- logging.info(f"Đã load {len(splits)} chunks từ DB cũ.")
84
- else:
85
- splits = []
86
- except Exception:
87
- rmtree(DB_PATH, ignore_errors=True)
88
- splits = []
89
-
90
- if not splits:
91
- logging.info("--- Tạo index mới ---")
92
- documents = load_documents_from_folder(DATA_PATH)
93
- if not documents: return None, []
94
-
95
- text_splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=150)
96
- splits = text_splitter.split_documents(documents)
97
-
98
- vectorstore = Chroma.from_documents(documents=splits, embedding=embedding_model, persist_directory=DB_PATH)
99
-
100
- return vectorstore, splits
101
 
102
- def get_retriever():
103
- logging.info("--- Tải Embedding Model ---")
104
- embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
105
-
106
- vectorstore, splits = build_vectorstore_and_corpus(embedding_model)
107
- if not vectorstore: return None
108
 
109
- if USE_MMR:
110
- base_retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 10})
111
- else:
112
- base_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
113
-
114
- if USE_BM25:
115
- bm25_retriever = BM25Retriever.from_documents(splits)
116
- bm25_retriever.k = 10
117
- ensemble_retriever = EnsembleRetriever(
118
- retrievers=[bm25_retriever, base_retriever],
119
- weights=[0.4, 0.6]
120
- )
121
- first_stage_retriever = ensemble_retriever
122
  else:
123
- first_stage_retriever = base_retriever
124
 
125
- logging.info("--- Tải Reranker Model (BGE-M3) ---")
126
- reranker_model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
 
 
127
 
128
- compressor = CrossEncoderReranker(model=reranker_model, top_n=5)
129
-
130
- compression_retriever = ContextualCompressionRetriever(
131
- base_compressor=compressor,
132
- base_retriever=first_stage_retriever
133
- )
134
-
135
- logging.info("Đã tích hợp Reranker thành công.")
136
- return compression_retriever
137
 
138
  class DeepMedBot:
139
  def __init__(self):
140
  self.retriever = None
141
- self.rag_chain = None
 
142
  self.ready = False
143
 
144
  if not GOOGLE_API_KEY:
145
- logging.error("Thiếu GOOGLE_API_KEY")
146
  return
147
-
148
  try:
149
- self.retriever = get_retriever()
150
- if not self.retriever: return
151
-
 
 
 
 
 
 
 
 
152
  self.llm = ChatGoogleGenerativeAI(
153
  model="gemini-2.5-flash",
154
  temperature=0.2,
155
- google_api_key=GOOGLE_API_KEY
 
156
  )
 
157
  self._build_chains()
158
  self.ready = True
159
  logging.info("Bot đã sẵn sàng!")
 
160
  except Exception as e:
161
- logging.error(f"Lỗi init bot: {e}")
162
- logging.debug(traceback.format_exc())
163
 
164
  def _build_chains(self):
165
- contextualize_q_system_prompt = (
166
- "Dựa trên lịch sử trò chuyện và câu hỏi mới nhất, "
167
- "hãy viết lại câu hỏi thành một câu hoàn chỉnh nếu cần. "
168
- "Nếu không liên quan, giữ nguyên. KHÔNG trả lời, chỉ viết lại."
 
169
  )
170
- contextualize_q_prompt = ChatPromptTemplate.from_messages([
171
- ("system", contextualize_q_system_prompt),
172
  MessagesPlaceholder("chat_history"),
173
  ("human", "{input}"),
174
  ])
175
- history_aware_retriever = create_history_aware_retriever(
176
- self.llm, self.retriever, contextualize_q_prompt
177
- )
178
 
 
179
  qa_system_prompt = (
180
- "Bạn là trợ lý y tế DeepMed. Dựa vào Context sau để trả lời.\n"
181
  "Context:\n{context}\n\n"
182
- "Yêu cầu:\n"
183
- "- Chỉ trả lời dựa trên Context. Nếu không có thông tin, nói 'Tôi không tìm thấy thông tin trong tài liệu'.\n"
184
- "- Trả lời ngắn gọn, cấu trúc.\n"
 
185
  )
186
  qa_prompt = ChatPromptTemplate.from_messages([
187
  ("system", qa_system_prompt),
188
- MessagesPlaceholder("chat_history"),
189
- ("human", "{input}"),
190
  ])
191
 
192
- question_answer_chain = create_stuff_documents_chain(self.llm, qa_prompt)
193
- self.rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
194
 
195
  def chat_stream(self, message: str, history: list):
196
- """
197
- Hàm Generator dùng cho Streaming
198
- """
199
  if not self.ready:
200
- yield "Hệ thống đang khởi động hoặc lỗi dữ liệu."
201
  return
202
 
203
- chat_history = []
204
- for u, b in history[-MAX_HISTORY_TURNS:]:
205
- chat_history.append(HumanMessage(content=u))
206
- chat_history.append(AIMessage(content=b))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
 
208
  full_response = ""
209
- retrieved_docs = []
210
 
211
  try:
212
- for chunk in self.rag_chain.stream({"input": message, "chat_history": chat_history}):
213
- if "answer" in chunk:
214
- content = chunk["answer"]
215
- full_response += content
216
- yield full_response
217
-
218
- if "context" in chunk:
219
- retrieved_docs = chunk["context"]
220
-
221
- if retrieved_docs:
222
- refs = self._build_references_text(retrieved_docs)
223
- if refs:
224
- full_response += f"\n\n---\n📚 **Nguồn:**\n{refs}"
225
- yield full_response
226
 
 
 
 
 
 
 
227
  except Exception as e:
228
- logging.error(f"Lỗi stream: {e}")
229
- yield "Gặp lỗi trong quá trình xử lý."
230
 
231
  @staticmethod
232
- def _build_references_text(docs) -> str:
233
- from collections import defaultdict
234
- source_pages = defaultdict(set)
235
  for doc in docs:
236
  src = os.path.basename(doc.metadata.get("source", "Tài liệu"))
 
237
  if "page" in doc.metadata:
238
- source_pages[src].add(doc.metadata["page"] + 1)
239
- else:
240
- source_pages[src]
241
 
242
- lines = []
243
- for src, pages in source_pages.items():
244
- if pages:
245
- p_str = ", ".join(str(p) for p in sorted(pages))
246
- lines.append(f"- {src} (Trang {p_str})")
247
- else:
248
- lines.append(f"- {src}")
249
- return "\n".join(lines)
250
 
251
  bot = DeepMedBot()
252
 
253
- def gradio_chat_stream(message, history):
 
254
  for partial_response in bot.chat_stream(message, history):
255
  yield partial_response
256
 
 
 
 
 
 
257
  demo = gr.ChatInterface(
258
- fn=gradio_chat_stream,
259
- title="🏥 DeepMed AI - Trợ lý Y tế tại Trung Tâm Y Tế Khu Vực Thanh Ba",
 
 
 
 
 
260
  )
261
 
262
  if __name__ == "__main__":
263
- demo.launch(share=True)
 
3
  sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
4
 
5
  import os
6
+ import json
7
  import logging
8
+ import hashlib
9
  import gradio as gr
10
  import pandas as pd
11
  import docx2txt
 
14
  from langchain_chroma import Chroma
15
  from langchain_community.document_loaders import PyPDFLoader
16
  from langchain_text_splitters import RecursiveCharacterTextSplitter
 
 
 
 
17
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
18
  from langchain_core.messages import HumanMessage, AIMessage
19
  from langchain_core.documents import Document
20
+ from langchain_core.output_parsers import StrOutputParser
21
  from langchain_huggingface import HuggingFaceEmbeddings
22
+ from langchain.chains.combine_documents import create_stuff_documents_chain
23
 
24
+ # --- CẤU HÌNH ---
25
+ # Thay API Key của bạn vào đây hoặc set biến môi trường
26
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
 
 
27
  DATA_PATH = "medical_data"
28
  DB_PATH = "chroma_db"
29
+ TRACKING_FILE = "processed_files.json" # File lưu trạng thái để không học lại dữ liệu cũ
 
 
 
30
 
31
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
32
 
33
+ # --- HÀM XỬ LÝ DỮ LIỆU (INCREMENTAL) ---
34
+
35
+ def calculate_md5(file_path):
36
+ """Tính mã hash MD5 của file"""
37
+ hash_md5 = hashlib.md5()
38
+ try:
39
+ with open(file_path, "rb") as f:
40
+ for chunk in iter(lambda: f.read(4096), b""):
41
+ hash_md5.update(chunk)
42
+ return hash_md5.hexdigest()
43
+ except Exception:
44
+ return None
45
+
46
+ def load_excel_enhanced(file_path, filename):
47
+ """Đọc Excel giữ ngữ cảnh từng dòng"""
48
+ try:
49
+ df = pd.read_excel(file_path)
50
+ text_parts = []
51
+ columns = df.columns.tolist()
52
+ for index, row in df.iterrows():
53
+ row_str = f"Nguồn: {filename}, Dòng {index+2}: "
54
+ items = []
55
+ for col in columns:
56
+ val = row[col]
57
+ if pd.notna(val):
58
+ items.append(f"{col}: {val}")
59
+ if items:
60
+ text_parts.append(row_str + " | ".join(items))
61
+ return "\n".join(text_parts)
62
+ except Exception as e:
63
+ logging.error(f"Lỗi đọc Excel {filename}: {e}")
64
+ return ""
65
+
66
+ def process_single_file(file_path, filename):
67
+ """Chọn loader phù hợp cho từng loại file"""
68
+ docs = []
69
+ filename_lower = filename.lower()
70
+ try:
71
+ if filename_lower.endswith(".pdf"):
72
+ loader = PyPDFLoader(file_path)
73
+ loaded_docs = loader.load()
74
+ for d in loaded_docs:
75
+ d.metadata["source"] = filename
76
+ docs.extend(loaded_docs)
77
+
78
+ elif filename_lower.endswith((".xlsx", ".xls")):
79
+ text = load_excel_enhanced(file_path, filename)
80
+ if text:
81
+ docs.append(Document(page_content=text, metadata={"source": filename}))
82
+
83
+ elif filename_lower.endswith(".docx"):
84
+ text = docx2txt.process(file_path)
85
+ if text.strip():
86
+ docs.append(Document(page_content=text, metadata={"source": filename}))
87
+
88
+ elif filename_lower.endswith(".txt"):
89
+ with open(file_path, "r", encoding="utf-8") as f:
90
+ text = f.read()
91
+ if text.strip():
92
+ docs.append(Document(page_content=text, metadata={"source": filename}))
93
+
94
+ except Exception as e:
95
+ logging.error(f"Không thể đọc file {filename}: {e}")
96
+
97
+ return docs
98
+
99
+ def load_and_process_new_files(folder_path, embedding_model):
100
+ """Chỉ xử lý file mới hoặc file bị thay đổi"""
101
  if not os.path.exists(folder_path):
102
  os.makedirs(folder_path, exist_ok=True)
103
+ return Chroma(persist_directory=DB_PATH, embedding_function=embedding_model)
104
+
105
+ # Load file tracking cũ
106
+ if os.path.exists(TRACKING_FILE):
107
+ with open(TRACKING_FILE, "r") as f:
108
+ processed_files = json.load(f)
109
+ else:
110
+ processed_files = {}
111
+
112
+ new_docs = []
113
+ current_files_status = {}
114
+ files_changed = False
115
+
116
+ logging.info("--- Đang quét thay đổi trong thư mục dữ liệu ---")
117
+
118
  for root, _, files in os.walk(folder_path):
119
  for filename in files:
120
+ if filename.startswith("~$"): continue # Bỏ qua file tạm của Word/Excel
121
+
122
  file_path = os.path.join(root, filename)
123
+ file_hash = calculate_md5(file_path)
124
+
125
+ if not file_hash: continue
126
+
127
+ current_files_status[filename] = file_hash
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ # Kiểm tra: Nếu file chưa có trong DB hoặc Hash đã đổi -> Load lại
130
+ if filename not in processed_files or processed_files[filename] != file_hash:
131
+ logging.info(f"Phát hiện mới/cập nhật: {filename}")
132
+ file_docs = process_single_file(file_path, filename)
133
+ new_docs.extend(file_docs)
134
+ files_changed = True
135
 
136
+ vectorstore = Chroma(persist_directory=DB_PATH, embedding_function=embedding_model)
137
+
138
+ if new_docs:
139
+ logging.info(f"Đang Embed {len(new_docs)} chunks mới...")
140
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
141
+ splits = text_splitter.split_documents(new_docs)
142
+ vectorstore.add_documents(splits)
143
+ logging.info("Hoàn tất cập nhật DB.")
 
 
 
 
 
144
  else:
145
+ logging.info("Không dữ liệu mới cần cập nhật.")
146
 
147
+ # Lưu lại trạng thái mới (chỉ lưu những file hiện đang tồn tại)
148
+ if files_changed or len(current_files_status) != len(processed_files):
149
+ with open(TRACKING_FILE, "w") as f:
150
+ json.dump(current_files_status, f)
151
 
152
+ return vectorstore
153
+
154
+ # --- BOT CLASS ---
 
 
 
 
 
 
155
 
156
  class DeepMedBot:
157
  def __init__(self):
158
  self.retriever = None
159
+ self.qa_chain = None
160
+ self.history_chain = None
161
  self.ready = False
162
 
163
  if not GOOGLE_API_KEY:
164
+ logging.error("LỖI: Chưa cấu hình GOOGLE_API_KEY")
165
  return
166
+
167
  try:
168
+ logging.info("--- Khởi tạo Embedding Model ---")
169
+ # Model đa ngôn ngữ tốt cho tiếng Việt
170
+ embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
171
+
172
+ # Load DB (Thông minh)
173
+ self.vectorstore = load_and_process_new_files(DATA_PATH, embedding_model)
174
+
175
+ # Thiết lập Retriever
176
+ self.retriever = self.vectorstore.as_retriever(search_kwargs={"k": 5})
177
+
178
+ logging.info("--- Khởi tạo LLM ---")
179
  self.llm = ChatGoogleGenerativeAI(
180
  model="gemini-2.5-flash",
181
  temperature=0.2,
182
+ google_api_key=GOOGLE_API_KEY,
183
+ streaming=True # Quan trọng cho hiệu ứng gõ chữ
184
  )
185
+
186
  self._build_chains()
187
  self.ready = True
188
  logging.info("Bot đã sẵn sàng!")
189
+
190
  except Exception as e:
191
+ logging.error(f"Lỗi khởi tạo Bot: {e}")
 
192
 
193
  def _build_chains(self):
194
+ # 1. Chain viết lại câu hỏi dựa trên lịch sử
195
+ context_system_prompt = (
196
+ "Dựa trên lịch sử trò chuyện câu hỏi mới nhất của người dùng, "
197
+ "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.\n"
198
+ "KHÔNG trả lời câu hỏi, chỉ viết lại câu hỏi thôi."
199
  )
200
+ context_prompt = ChatPromptTemplate.from_messages([
201
+ ("system", context_system_prompt),
202
  MessagesPlaceholder("chat_history"),
203
  ("human", "{input}"),
204
  ])
205
+ self.history_chain = context_prompt | self.llm | StrOutputParser()
 
 
206
 
207
+ # 2. Chain trả lời câu hỏi (RAG)
208
  qa_system_prompt = (
209
+ "Bạn là trợ lý y tế DeepMed tại TTYT Thanh Ba. Dựa vào Context sau để trả lời.\n"
210
  "Context:\n{context}\n\n"
211
+ "Quy tắc:\n"
212
+ "- Trả lời ngắn gọn, đúng trọng tâm.\n"
213
+ "- Nếu thông tin thuốc (liều, chống chỉ định), phải giữ nguyên số liệu.\n"
214
+ "- Nếu không có thông tin trong Context, hãy nói 'Tôi chưa có dữ liệu về vấn đề này'."
215
  )
216
  qa_prompt = ChatPromptTemplate.from_messages([
217
  ("system", qa_system_prompt),
218
+ ("human", "{question}"),
 
219
  ])
220
 
221
+ # create_stuff_documents_chain nhận vào 'context' (list docs) và 'question'
222
+ self.qa_chain = create_stuff_documents_chain(self.llm, qa_prompt)
223
 
224
  def chat_stream(self, message: str, history: list):
 
 
 
225
  if not self.ready:
226
+ yield "Hệ thống đang khởi động hoặc lỗi cấu hình..."
227
  return
228
 
229
+ # Bước 1: Contextualize - Hiểu ngữ cảnh nếu có lịch sử
230
+ actual_question = message
231
+ if history:
232
+ yield "🔄 Đang phân tích ngữ cảnh..."
233
+ chat_history_objs = []
234
+ # Lấy 4 turn gần nhất để làm context
235
+ for u, a in history[-4:]:
236
+ chat_history_objs.append(HumanMessage(content=u))
237
+ chat_history_objs.append(AIMessage(content=a))
238
+
239
+ try:
240
+ actual_question = self.history_chain.invoke({
241
+ "chat_history": chat_history_objs,
242
+ "input": message
243
+ })
244
+ except Exception:
245
+ actual_question = message # Fallback nếu lỗi
246
+
247
+ # Bước 2: Retrieval - Tìm kiếm
248
+ yield "📚 Đang tra cứu tài liệu y khoa..."
249
+ docs = self.retriever.invoke(actual_question)
250
+
251
+ if not docs:
252
+ yield "Không tìm thấy thông tin phù hợp trong cơ sở dữ liệu nội bộ."
253
+ return
254
 
255
+ # Bước 3: Generation - Trả lời Streaming
256
  full_response = ""
257
+ input_data = {"context": docs, "question": actual_question}
258
 
259
  try:
260
+ # Loop qua từng token được sinh ra
261
+ for chunk in self.qa_chain.stream(input_data):
262
+ # Xử lý format output (đôi khi là string, đôi khi là object)
263
+ if isinstance(chunk, str):
264
+ content = chunk
265
+ elif hasattr(chunk, "content"):
266
+ content = chunk.content
267
+ else:
268
+ content = str(chunk)
269
+
270
+ full_response += content
271
+ yield full_response
 
 
272
 
273
+ # Bước 4: Thêm nguồn tham khảo
274
+ refs = self._build_references_text(docs)
275
+ if refs:
276
+ full_response += f"\n\n---\n📚 **Nguồn tham khảo:**\n{refs}"
277
+ yield full_response
278
+
279
  except Exception as e:
280
+ logging.error(f"Lỗi Stream: {e}")
281
+ yield "Đã xảy ra lỗi trong quá trình tạo câu trả lời."
282
 
283
  @staticmethod
284
+ def _build_references_text(docs):
285
+ sources = set()
 
286
  for doc in docs:
287
  src = os.path.basename(doc.metadata.get("source", "Tài liệu"))
288
+ # Nếu có số trang (PDF)
289
  if "page" in doc.metadata:
290
+ src += f" (Trang {doc.metadata['page'] + 1})"
291
+ sources.add(src)
 
292
 
293
+ return "\n".join([f"- {s}" for s in sources])
294
+
295
+ # --- GIAO DIỆN GRADIO ---
 
 
 
 
 
296
 
297
  bot = DeepMedBot()
298
 
299
+ def gradio_chat_generator(message, history):
300
+ # Hàm này đóng vai trò cầu nối, gọi generator của bot
301
  for partial_response in bot.chat_stream(message, history):
302
  yield partial_response
303
 
304
+ custom_css = """
305
+ #component-0 {height: 90vh !important;}
306
+ footer {visibility: hidden}
307
+ """
308
+
309
  demo = gr.ChatInterface(
310
+ fn=gradio_chat_generator,
311
+ title="🏥 DeepMed AI - Trợ lý Y tế TTYT Thanh Ba",
312
+ description="Hỗ trợ tra cứu Phác đồ điều trị, Dược thư và Quy trình kỹ thuật.",
313
+ theme="soft",
314
+ css=custom_css,
315
+ examples=["Liều dùng Paracetamol cho trẻ em?", "Phác đồ điều trị sốt xuất huyết?", "Quy trình rửa tay ngoại khoa?"],
316
+ cache_examples=False
317
  )
318
 
319
  if __name__ == "__main__":
320
+ demo.launch(share=False)