deddoggo commited on
Commit
0c16fc9
·
1 Parent(s): b8e326d
app.py CHANGED
@@ -1,238 +1,58 @@
1
- # app.py
2
- # Phiên bản triển khai cuối cùng, tải các mô hình từ Hub và các tài sản dữ liệu đã xử lý trước.
3
-
4
- import os
5
- import sys
6
- import json
7
- import re
8
- import pickle
9
- from collections import defaultdict
10
-
11
- # Core ML/DL và Unsloth
12
- import torch
13
- from unsloth import FastLanguageModel
14
- from transformers import TextStreamer
15
-
16
- # RAG - Retrieval
17
- import faiss
18
- from sentence_transformers import SentenceTransformer
19
- from rank_bm25 import BM25Okapi
20
- import numpy as np
21
-
22
- # Deployment
23
  import gradio as gr
24
-
25
- print("✅ Import thư viện thành công.")
26
-
27
- # --- PHẦN 1: CẤU HÌNH VÀ ĐƯỜNG DẪN ---
28
-
29
- # Tên các mô hình sẽ được tải từ Hub
30
- EMBEDDING_MODEL_NAME = "bkai-foundation-models/vietnamese-bi-encoder"
31
- LLM_MODEL_NAME = "unsloth/Llama-3.2-3B-Instruct-bnb-4bit"
32
-
33
- # Đường dẫn đến các file bạn sẽ upload lên Space
34
- # Tạo một thư mục 'data' trong Space để chứa chúng cho gọn gàng
35
- RAW_LAW_DATA_FILE = "data/luat_chi_tiet_output_openai_sdk_final_cleaned.json"
36
- FAISS_INDEX_FILE = "data/my_law_faiss_flatip_normalized.index"
37
-
38
- # Tên các file sẽ được tạo ra trong quá trình chạy (nếu chưa có)
39
- PROCESSED_CHUNKS_FILE = "processed_chunks.json"
40
- BM25_MODEL_FILE = "bm25_model.pkl"
41
-
42
- # Biến toàn cục để lưu trữ tài nguyên
43
- APP_RESOURCES = {}
44
-
45
- # --- PHẦN 2: CÁC HÀM TIỆN ÍCH VÀ XỬ LÝ DỮ LIỆU ---
46
-
47
- def process_law_data_to_chunks(structured_data):
48
- """Làm phẳng dữ liệu luật cấu trúc thành danh sách các chunks."""
49
- flat_list = []
50
- articles = [structured_data] if isinstance(structured_data, dict) else structured_data
51
- for article_data in articles:
52
- if not isinstance(article_data, dict): continue
53
- clauses = article_data.get("clauses", [])
54
- for clause in clauses:
55
- points = clause.get("points_in_clause", [])
56
- if points:
57
- for point in points:
58
- text = point.get("point_text_original")
59
- if text:
60
- flat_list.append({"text": text, "metadata": {"article": article_data.get("article"), "clause": clause.get("clause_number"), "point": point.get("point_id")}})
61
- else:
62
- text = clause.get("clause_text_original")
63
- if text:
64
- flat_list.append({"text": text, "metadata": {"article": article_data.get("article"), "clause": clause.get("clause_number")}})
65
- return flat_list
66
-
67
- def tokenize_vi_simple(text):
68
- """Tokenize tiếng Việt đơn giản."""
69
- text = text.lower()
70
- text = re.sub(r'[^\w\s]', '', text)
71
- return text.split()
72
-
73
- # --- PHẦN 3: LOGIC CỐT LÕI CỦA ỨNG DỤNG ---
74
-
75
- def load_app_resources():
76
- """Tải hoặc tạo tất cả các tài nguyên cần thiết cho ứng dụng."""
77
- print("--- Bắt đầu quá trình tải tài nguyên ---")
78
-
79
- # 1. Tải các mô hình AI từ Hub
80
- print("1. Đang tải LLM và Embedding Model từ Hugging Face Hub...")
81
- device = "cuda" if torch.cuda.is_available() else "cpu"
82
- llm_model, tokenizer = FastLanguageModel.from_pretrained(
83
- model_name=LLM_MODEL_NAME, max_seq_length=2048, dtype=None, load_in_4bit=True
84
  )
85
- FastLanguageModel.for_inference(llm_model)
86
- embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME, device=device)
87
- APP_RESOURCES['llm_model'] = llm_model
88
- APP_RESOURCES['tokenizer'] = tokenizer
89
- APP_RESOURCES['embedding_model'] = embedding_model
90
- print("✅ Tải mô hình AI thành công.")
91
-
92
- # 2. Tải và xử lý dữ liệu luật từ file JSON bạn cung cấp
93
- print(f"2. Đang tải và xử lý dữ liệu từ '{RAW_LAW_DATA_FILE}'...")
94
- if not os.path.exists(RAW_LAW_DATA_FILE):
95
- raise FileNotFoundError(f"Không tìm thấy file dữ liệu luật: '{RAW_LAW_DATA_FILE}'. Vui lòng tạo thư mục 'data' và upload file này lên Space.")
96
- with open(RAW_LAW_DATA_FILE, 'r', encoding='utf-8') as f:
97
- raw_data = json.load(f)
98
- chunks_data = process_law_data_to_chunks(raw_data)
99
- APP_RESOURCES['chunks_data'] = chunks_data
100
- print(f"✅ Đã xử lý thành {len(chunks_data)} chunks dữ liệu.")
101
-
102
- # 3. Tải FAISS Index đã được tạo sẵn
103
- print(f"3. Đang tải FAISS index từ '{FAISS_INDEX_FILE}'...")
104
- if not os.path.exists(FAISS_INDEX_FILE):
105
- raise FileNotFoundError(f"Không tìm thấy file FAISS index: '{FAISS_INDEX_FILE}'. Vui lòng upload file này.")
106
- faiss_index = faiss.read_index(FAISS_INDEX_FILE)
107
- APP_RESOURCES['faiss_index'] = faiss_index
108
- print(f"✅ Đã tải FAISS Index với {faiss_index.ntotal} vectors.")
109
-
110
- # 4. Tải hoặc tạo BM25 Model (bước này nhanh nên có thể tạo on-the-fly)
111
- if os.path.exists(BM25_MODEL_FILE):
112
- print(f"4. Đang tải BM25 model từ '{BM25_MODEL_FILE}'...")
113
- with open(BM25_MODEL_FILE, 'rb') as f:
114
- bm25_model = pickle.load(f)
115
- else:
116
- print("4. Không tìm thấy BM25 model. Đang tạo mới...")
117
- tokenized_corpus = [tokenize_vi_simple(c['text']) for c in chunks_data]
118
- bm25_model = BM25Okapi(tokenized_corpus)
119
- with open(BM25_MODEL_FILE, 'wb') as f:
120
- pickle.dump(bm25_model, f)
121
- print(f"✅ Đã tạo và lưu BM25 model vào '{BM25_MODEL_FILE}'.")
122
- APP_RESOURCES['bm25_model'] = bm25_model
123
-
124
- print("\n--- Tải tài nguyên hoàn tất! Ứng dụng đã sẵn sàng. ---")
125
-
126
- def search_relevant_laws(query_text, k=5):
127
- """Thực hiện Hybrid Search."""
128
- # (Hàm này giữ nguyên như trước, không cần thay đổi)
129
- rrf_k_constant = 60
130
- embedding_model = APP_RESOURCES['embedding_model']
131
- faiss_index = APP_RESOURCES['faiss_index']
132
- chunks_data = APP_RESOURCES['chunks_data']
133
- bm25_model = APP_RESOURCES['bm25_model']
134
-
135
- query_embedding = embedding_model.encode([query_text], convert_to_tensor=True)
136
- query_embedding_np = query_embedding.cpu().numpy().astype('float32')
137
- faiss.normalize_L2(query_embedding_np)
138
- num_candidates = min(k * 10, faiss_index.ntotal)
139
- _, semantic_indices = faiss_index.search(query_embedding_np, num_candidates)
140
-
141
- tokenized_query = tokenize_vi_simple(query_text)
142
- bm25_scores = bm25_model.get_scores(tokenized_query)
143
- bm25_results = sorted(enumerate(bm25_scores), key=lambda x: x[1], reverse=True)[:num_candidates]
144
-
145
- rrf_scores = defaultdict(float)
146
- if semantic_indices.size > 0:
147
- for rank, doc_idx in enumerate(semantic_indices[0]):
148
- if doc_idx != -1: rrf_scores[doc_idx] += 1.0 / (rrf_k_constant + rank)
149
- for rank, (doc_idx, score) in enumerate(bm25_results):
150
- if score > 0: rrf_scores[doc_idx] += 1.0 / (rrf_k_constant + rank)
151
-
152
- fused_results = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
153
-
154
- final_results = []
155
- for doc_idx, score in fused_results[:k]:
156
- result = chunks_data[doc_idx].copy()
157
- result['retrieval_score'] = score
158
- final_results.append(result)
159
-
160
- return final_results
161
-
162
- def generate_llm_response(query, context):
163
- """Sinh câu trả lời từ LLM."""
164
- # (Hàm này giữ nguyên như trước, không cần thay đổi)
165
- llm_model = APP_RESOURCES['llm_model']
166
- tokenizer = APP_RESOURCES['tokenizer']
167
- prompt = f"""Bạn là một trợ lý AI chuyên tư vấn về luật giao thông đường bộ Việt Nam. Dựa vào các thông tin luật được cung cấp dưới đây để trả lời câu hỏi của người dùng một cách chính xác và chi tiết.
168
-
169
- ### Thông tin luật được trích dẫn:
170
- {context}
171
-
172
- ### Câu hỏi của người dùng:
173
- {query}
174
-
175
- ### Trả lời của bạn:"""
176
- inputs = tokenizer(prompt, return_tensors="pt").to(llm_model.device)
177
- output_ids = llm_model.generate(**inputs, max_new_tokens=512, temperature=0.3, do_sample=True, pad_token_id=tokenizer.eos_token_id)
178
- response_text = tokenizer.decode(output_ids[0][inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()
179
- return response_text
180
-
181
- # --- PHẦN 4: CÁC HÀM GIAO TIẾP VỚI GRADIO ---
182
-
183
- def retriever_interface(query):
184
- """Giao tiếp với Tab 1: Chỉ tìm kiếm."""
185
- retrieved_results = search_relevant_laws(query)
186
- if not retrieved_results: return "Không tìm thấy điều luật nào liên quan."
187
 
188
- output_md = "### Các điều luật liên quan nhất:\n\n"
189
- for i, res in enumerate(retrieved_results):
190
- meta = res.get('metadata', {})
191
- text = res.get('text', 'N/A')
192
- output_md += f"**{i+1}. {meta.get('article', 'N/A')} | {meta.get('clause', 'N/A')} | {meta.get('point', 'N/A')}**\n"
193
- output_md += f"> {text}\n\n---\n\n"
194
- return output_md
195
-
196
- def rag_interface(query, progress=gr.Progress()):
197
- """Giao tiếp với Tab 2: RAG hoàn chỉnh."""
198
- progress(0.2, desc="Đang tìm kiếm ngữ cảnh...")
199
- retrieved_results = search_relevant_laws(query)
200
-
201
- if not retrieved_results:
202
- context_for_llm = "Không tìm thấy thông tin luật liên quan."
203
- context_for_display = "Không tìm thấy điều luật nào liên quan để tạo câu trả lời."
204
- else:
205
- context_for_llm = "\n\n---\n\n".join([r['text'] for r in retrieved_results])
206
- context_for_display = retriever_interface(query)
207
-
208
- progress(0.7, desc="Đang sinh câu trả lời...")
209
- final_answer = generate_llm_response(query, context_for_llm)
210
- progress(1, desc="Hoàn tất!")
211
- return final_answer, context_for_display
212
-
213
- # --- PHẦN 5: KHỞI CHẠY ỨNG DỤNG ---
214
-
215
- # Tải tài nguyên một lần duy nhất
216
- load_app_resources()
217
-
218
- # Xây dựng giao diện
219
- with gr.Blocks(theme=gr.themes.Soft(), title="Chatbot Luật GTĐB") as demo:
220
- gr.Markdown("# ⚖️ Chatbot Luật Giao thông Đường bộ Việt Nam (RAG)")
221
- with gr.Tabs():
222
- with gr.TabItem("Tìm kiếm Điều luật (Retriever)"):
223
- retriever_query = gr.Textbox(label="Nhập nội dung tìm kiếm", placeholder="Vd: Vượt đèn đỏ")
224
- retriever_button = gr.Button("Tìm kiếm", variant="secondary")
225
- retriever_output = gr.Markdown(label="Các điều luật liên quan")
226
-
227
- with gr.TabItem("Hỏi-Đáp (RAG)"):
228
- rag_query = gr.Textbox(label="Nhập câu hỏi", placeholder="Vd: Vượt đèn đỏ phạt bao nhiêu tiền?")
229
- rag_button = gr.Button("Gửi", variant="primary")
230
- rag_answer = gr.Textbox(label="Câu trả lời", interactive=False, lines=7)
231
- with gr.Accordion("Xem ngữ cảnh đã sử dụng", open=False):
232
- rag_context = gr.Markdown()
233
-
234
- retriever_button.click(fn=retriever_interface, inputs=retriever_query, outputs=retriever_output)
235
- rag_button.click(fn=rag_interface, inputs=rag_query, outputs=[rag_answer, rag_context])
236
 
237
  if __name__ == "__main__":
238
- demo.launch()
 
1
+ # file: app.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import gradio as gr
3
+ import time
4
+
5
+ from rag_pipeline import initialize_components, generate_response
6
+
7
+ # --- KHỞI TẠO CÁC THÀNH PHẦN (CHỈ CHẠY 1 LẦN) ---
8
+ start_time = time.time()
9
+ print("Bắt đầu khởi tạo ứng dụng Chatbot Luật Giao thông...")
10
+ DATA_PATH = "data/luat_chi_tiet_output_openai_sdk_final_cleaned.json"
11
+ COMPONENTS = initialize_components(DATA_PATH)
12
+ end_time = time.time()
13
+ print(f"✅ Ứng dụng đã sẵn sàng! Thời gian khởi tạo: {end_time - start_time:.2f} giây.")
14
+ # ----------------------------------------------------
15
+
16
+
17
+ def chat_interface(query, history):
18
+ """
19
+ Hàm xử lý logic cho giao diện chat của Gradio.
20
+ """
21
+ print(f"Nhận được câu hỏi từ người dùng: '{query}'")
22
+ # Gọi hàm generate_response với query và các thành phần đã được khởi tạo
23
+ response = generate_response(query, COMPONENTS)
24
+ return response
25
+
26
+ # --- GIAO DIỆN GRADIO ---
27
+ with gr.Blocks(theme=gr.themes.Soft(), title="Chatbot Luật Giao thông Việt Nam") as demo:
28
+ gr.Markdown(
29
+ """
30
+ # ⚖️ Chatbot Luật Giao thông Việt Nam
31
+ Hỏi đáp về các quy định, mức phạt trong luật giao thông đường bộ dựa trên cơ sở dữ liệu được cung cấp.
32
+ *Lưu ý: Đây là một sản phẩm demo. Thông tin chỉ mang tính chất tham khảo.*
33
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ chatbot = gr.Chatbot(label="Chatbot", height=500)
37
+ msg = gr.Textbox(label="Nhập câu hỏi của bạn", placeholder="Ví dụ: Vượt đèn đỏ bị phạt bao nhiêu tiền?")
38
+ clear = gr.ClearButton([msg, chatbot])
39
+
40
+ def respond(message, chat_history):
41
+ bot_message = chat_interface(message, chat_history)
42
+ chat_history.append((message, bot_message))
43
+ return "", chat_history
44
+
45
+ msg.submit(respond, [msg, chatbot], [msg, chatbot])
46
+
47
+ gr.Examples(
48
+ examples=[
49
+ "Phương tiện giao thông đường bộ gồm những loại nào?",
50
+ "Vượt đèn đỏ phạt bao nhiêu tiền đối với xe máy?",
51
+ "Nồng độ cồn cho phép khi lái xe ô bao nhiêu?",
52
+ "Đi sai làn đường bị trừ mấy điểm bằng lái?",
53
+ ],
54
+ inputs=msg
55
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  if __name__ == "__main__":
58
+ demo.launch()
data_processor.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # file: data_processor.py
2
+ import json
3
+
4
+ def process_law_data_to_chunks(structured_data_input):
5
+ """
6
+ Xử lý dữ liệu luật từ cấu trúc JSON lồng nhau thành một danh sách phẳng các chunks.
7
+ Mỗi chunk chứa text và metadata tương ứng.
8
+ """
9
+ flat_list = []
10
+
11
+ # Đảm bảo đầu vào là một danh sách các điều luật (articles)
12
+ if isinstance(structured_data_input, dict) and "article" in structured_data_input:
13
+ articles_list = [structured_data_input]
14
+ elif isinstance(structured_data_input, list):
15
+ articles_list = structured_data_input
16
+ else:
17
+ print("Lỗi: Dữ liệu đầu vào không phải là danh sách các Điều luật hoặc một đối tượng Điều luật.")
18
+ return flat_list
19
+
20
+ for article_data in articles_list:
21
+ if not isinstance(article_data, dict):
22
+ print(f"Cảnh báo: Bỏ qua một mục trong danh sách điều luật vì không phải là dictionary: {article_data}")
23
+ continue
24
+
25
+ article_metadata_base = {
26
+ "source_document": article_data.get("source_document"),
27
+ "article": article_data.get("article"),
28
+ "article_title": article_data.get("article_title")
29
+ }
30
+
31
+ clauses = article_data.get("clauses", [])
32
+ if not isinstance(clauses, list):
33
+ print(f"Cảnh báo: 'clauses' trong điều {article_metadata_base.get('article')} không phải là danh sách. Bỏ qua.")
34
+ continue
35
+
36
+ for clause_data in clauses:
37
+ if not isinstance(clause_data, dict):
38
+ print(f"Cảnh báo: Bỏ qua một mục trong 'clauses' vì không phải là dictionary: {clause_data}")
39
+ continue
40
+
41
+ clause_metadata_base = article_metadata_base.copy()
42
+ clause_metadata_base.update({
43
+ "clause_number": clause_data.get("clause_number"),
44
+ "clause_metadata_summary": clause_data.get("clause_metadata_summary")
45
+ })
46
+
47
+ points_in_clause = clause_data.get("points_in_clause", [])
48
+ if not isinstance(points_in_clause, list):
49
+ print(f"Cảnh báo: 'points_in_clause' trong khoản {clause_metadata_base.get('clause_number')} của điều {article_metadata_base.get('article')} không phải là danh sách. Bỏ qua.")
50
+ continue
51
+
52
+ if points_in_clause:
53
+ for point_data in points_in_clause:
54
+ if not isinstance(point_data, dict):
55
+ print(f"Cảnh báo: Bỏ qua một mục trong 'points_in_clause' vì không phải là dictionary: {point_data}")
56
+ continue
57
+
58
+ chunk_text = point_data.get("point_text_original") or point_data.get("violation_description_summary")
59
+ if not chunk_text:
60
+ continue
61
+
62
+ current_point_metadata = clause_metadata_base.copy()
63
+ point_specific_metadata = point_data.copy()
64
+ if "point_text_original" in point_specific_metadata:
65
+ del point_specific_metadata["point_text_original"]
66
+
67
+ current_point_metadata.update(point_specific_metadata)
68
+ final_metadata_cleaned = {k: v for k, v in current_point_metadata.items() if v is not None}
69
+
70
+ flat_list.append({"text": chunk_text, "metadata": final_metadata_cleaned})
71
+ else:
72
+ chunk_text = clause_data.get("clause_text_original")
73
+ if chunk_text:
74
+ current_clause_metadata = clause_metadata_base.copy()
75
+ additional_clause_info = {}
76
+ for key, value in clause_data.items():
77
+ if key not in ["clause_text_original", "points_in_clause", "clause_number", "clause_metadata_summary"]:
78
+ additional_clause_info[key] = value
79
+
80
+ if additional_clause_info:
81
+ current_clause_metadata.update(additional_clause_info)
82
+
83
+ final_metadata_cleaned = {k: v for k, v in current_clause_metadata.items() if v is not None}
84
+ flat_list.append({"text": chunk_text, "metadata": final_metadata_cleaned})
85
+
86
+ return flat_list
llm_handler.py → rag_pipeline.py RENAMED
File without changes
retrieval_handler.py → retriever.py RENAMED
File without changes