PBThuong96 commited on
Commit
1469b0c
·
verified ·
1 Parent(s): 2b7ddb0

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +244 -0
app.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __import__('pysqlite3')
2
+ import sys
3
+ sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
4
+
5
+ import os
6
+ import gradio as gr
7
+
8
+ # --- IMPORT CÁC THƯ VIỆN ---
9
+ from langchain_google_genai import ChatGoogleGenerativeAI
10
+ from langchain_chroma import Chroma
11
+ # Loaders cho nhiều định dạng
12
+ from langchain_community.document_loaders import (
13
+ PyPDFLoader,
14
+ DirectoryLoader,
15
+ TextLoader,
16
+ Docx2txtLoader,
17
+ UnstructuredExcelLoader
18
+ )
19
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
20
+ from langchain_community.retrievers import BM25Retriever
21
+ from langchain.retrievers import EnsembleRetriever
22
+ from langchain.chains import create_retrieval_chain, create_history_aware_retriever
23
+ from langchain.chains.combine_documents import create_stuff_documents_chain
24
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
25
+ from langchain_core.messages import HumanMessage, AIMessage
26
+ from langchain_core.documents import Document
27
+ from langchain_huggingface import HuggingFaceEmbeddings
28
+
29
+ # ==========================================
30
+ # CẤU HÌNH
31
+ # ==========================================
32
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
33
+ DATA_PATH = "medical_data"
34
+ DB_PATH = "chroma_db"
35
+
36
+ if not GOOGLE_API_KEY:
37
+ print("CẢNH BÁO: Chưa thiết lập GOOGLE_API_KEY!")
38
+
39
+ # ==========================================
40
+ # XỬ LÝ DỮ LIỆU ĐA ĐỊNH DẠNG
41
+ # ==========================================
42
+ def load_documents_from_folder(folder_path):
43
+ """Hàm đọc nhiều loại file khác nhau từ thư mục"""
44
+ documents = []
45
+ if not os.path.exists(folder_path):
46
+ os.makedirs(folder_path)
47
+ return []
48
+
49
+ for filename in os.listdir(folder_path):
50
+ file_path = os.path.join(folder_path, filename)
51
+ loader = None
52
+
53
+ try:
54
+ if filename.endswith(".pdf"):
55
+ loader = PyPDFLoader(file_path)
56
+ elif filename.endswith(".docx") or filename.endswith(".doc"):
57
+ loader = Docx2txtLoader(file_path)
58
+ elif filename.endswith(".txt"):
59
+ loader = TextLoader(file_path, encoding="utf-8")
60
+ elif filename.endswith(".xlsx") or filename.endswith(".xls"):
61
+ loader = UnstructuredExcelLoader(file_path)
62
+
63
+ if loader:
64
+ print(f"-> Đang đọc file: {filename}")
65
+ docs = loader.load()
66
+ # Thêm tên file vào metadata nếu chưa có (để trích dẫn sau này)
67
+ for doc in docs:
68
+ if "source" not in doc.metadata:
69
+ doc.metadata["source"] = filename
70
+ documents.extend(docs)
71
+ except Exception as e:
72
+ print(f"Lỗi khi đọc file {filename}: {e}")
73
+
74
+ return documents
75
+
76
+ def get_retriever():
77
+ print("--- Đang tải model Embedding... ---")
78
+ embedding_model = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
79
+
80
+ splits = []
81
+ vectorstore = None
82
+
83
+ # 1. Kiểm tra DB cũ
84
+ if os.path.exists(DB_PATH) and os.listdir(DB_PATH):
85
+ try:
86
+ print("--- Tìm thấy Database cũ, đang tải lên... ---")
87
+ vectorstore = Chroma(persist_directory=DB_PATH, embedding_function=embedding_model)
88
+
89
+ # Lấy dữ liệu để tái tạo BM25 (Keyword Search)
90
+ existing_data = vectorstore.get()
91
+ if existing_data['documents']:
92
+ for text, meta in zip(existing_data['documents'], existing_data['metadatas']):
93
+ splits.append(Document(page_content=text, metadata=meta))
94
+ else:
95
+ print("Cảnh báo: Database rỗng.")
96
+ except Exception as e:
97
+ print(f"Lỗi đọc DB cũ: {e}")
98
+
99
+ # 2. Nếu chưa có dữ liệu (splits rỗng), đọc từ file gốc
100
+ if not splits:
101
+ print("--- Chưa có dữ liệu index, bắt đầu đọc file nguồn... ---")
102
+ documents = load_documents_from_folder(DATA_PATH)
103
+
104
+ if not documents:
105
+ print("Lỗi: Không tìm thấy tài liệu nào (PDF, DOCX, TXT...) trong thư mục medical_data.")
106
+ return None
107
+
108
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
109
+ splits = text_splitter.split_documents(documents)
110
+
111
+ print(f"--- Đang mã hóa {len(splits)} đoạn văn bản vào ChromaDB... ---")
112
+ vectorstore = Chroma.from_documents(
113
+ documents=splits,
114
+ embedding=embedding_model,
115
+ persist_directory=DB_PATH
116
+ )
117
+
118
+ # 3. Tạo Hybrid Search (BM25 + Vector)
119
+ if not splits: return None
120
+
121
+ bm25_retriever = BM25Retriever.from_documents(splits)
122
+ bm25_retriever.k = 5
123
+ chroma_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
124
+
125
+ ensemble_retriever = EnsembleRetriever(
126
+ retrievers=[bm25_retriever, chroma_retriever],
127
+ weights=[0.4, 0.6]
128
+ )
129
+ return ensemble_retriever
130
+
131
+ # ==========================================
132
+ # LOGIC CHATBOT THÔNG MINH (CÓ NHỚ)
133
+ # ==========================================
134
+ rag_chain = None
135
+
136
+ def init_chatbot():
137
+ global rag_chain
138
+ retriever = get_retriever()
139
+ if not retriever: return False
140
+
141
+ llm = ChatGoogleGenerativeAI(
142
+ model="gemini-2.5-flash",
143
+ temperature=0.3,
144
+ google_api_key=GOOGLE_API_KEY
145
+ )
146
+
147
+ # --- BƯỚC 1: Contextualize Question ---
148
+ # (Viết lại câu hỏi mới dựa trên lịch sử để AI hiểu context)
149
+ contextualize_q_system_prompt = (
150
+ "Dựa trên lịch sử trò chuyện và câu hỏi mới nhất của người dùng, "
151
+ "nếu câu hỏi liên quan đến ngữ cảnh trước đó, hãy viết lại nó thành một câu hỏi độc lập đầy đủ ý nghĩa. "
152
+ "Nếu không liên quan, giữ nguyên câu hỏi gốc. KHÔNG trả lời câu hỏi, chỉ viết lại thôi."
153
+ )
154
+ contextualize_q_prompt = ChatPromptTemplate.from_messages([
155
+ ("system", contextualize_q_system_prompt),
156
+ MessagesPlaceholder("chat_history"),
157
+ ("human", "{input}"),
158
+ ])
159
+ # Retriever biết nhớ lịch sử
160
+ history_aware_retriever = create_history_aware_retriever(
161
+ llm, retriever, contextualize_q_prompt
162
+ )
163
+
164
+ # --- BƯỚC 2: Answer Question ---
165
+ # (Trả lời dựa trên Documents tìm được)
166
+ qa_system_prompt = (
167
+ "Bạn là trợ lý y tế DeepMed. Sử dụng các đoạn văn bản được cung cấp (Context) để trả lời câu hỏi. "
168
+ "Nếu không biết, hãy nói không biết. Nếu tìm thấy nội dung trả lời hãy trích dẫn tài liệu. Giữ câu trả lời ngắn gọn, súc tích.\n\n"
169
+ "Context:\n{context}"
170
+ )
171
+ qa_prompt = ChatPromptTemplate.from_messages([
172
+ ("system", qa_system_prompt),
173
+ MessagesPlaceholder("chat_history"),
174
+ ("human", "{input}"),
175
+ ])
176
+
177
+ question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
178
+
179
+ # Kết hợp lại thành chuỗi RAG hoàn chỉnh
180
+ rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
181
+
182
+ print("--- Chatbot đã sẵn sàng! ---")
183
+ return True
184
+
185
+ init_success = init_chatbot()
186
+
187
+ # ==========================================
188
+ # HÀM CHAT (Xử lý Lịch sử & Trích dẫn)
189
+ # ==========================================
190
+ def chat(message, history):
191
+ if not init_success:
192
+ return "Hệ thống chưa sẵn sàng. Kiểm tra lại data và API Key."
193
+
194
+ # 1. Chuyển đổi lịch sử Gradio sang format LangChain
195
+ chat_history = []
196
+ for user_msg, bot_msg in history:
197
+ chat_history.append(HumanMessage(content=user_msg))
198
+ chat_history.append(AIMessage(content=bot_msg))
199
+
200
+ try:
201
+ # 2. Gọi Chain xử lý
202
+ response = rag_chain.invoke({
203
+ "input": message,
204
+ "chat_history": chat_history
205
+ })
206
+
207
+ answer = response["answer"]
208
+
209
+ # 3. Xử lý Trích dẫn nguồn (References)
210
+ sources = set()
211
+ if "context" in response:
212
+ for doc in response["context"]:
213
+ source_name = doc.metadata.get("source", "Tài liệu không tên")
214
+ page_num = doc.metadata.get("page", None)
215
+
216
+ # Format tên file cho gọn (bỏ đường dẫn)
217
+ source_name = os.path.basename(source_name)
218
+
219
+ if page_num is not None:
220
+ sources.add(f"{source_name} (Trang {page_num + 1})")
221
+ else:
222
+ sources.add(source_name)
223
+
224
+ if sources:
225
+ answer += "\n\n---\n📚 **Tài liệu tham khảo:**\n" + "\n".join([f"- {s}" for s in sources])
226
+
227
+ return answer
228
+
229
+ except Exception as e:
230
+ return f"Lỗi hệ thống: {str(e)}"
231
+
232
+ # ==========================================
233
+ # GIAO DIỆN
234
+ # ==========================================
235
+ demo = gr.ChatInterface(
236
+ fn=chat,
237
+ title="🏥 DeepMed AI Pro - Trợ lý Y khoa Đa tài liệu",
238
+ description="Hỗ trợ PDF, DOCX, TXT, XLSX. Có khả năng nhớ ngữ cảnh hội thoại.",
239
+ theme="soft",
240
+ examples=["Bệnh cúm mùa là gì?", "Triệu chứng ra sao?", "Cách điều trị?"],
241
+ )
242
+
243
+ if __name__ == "__main__":
244
+ demo.launch()