DrPie commited on
Commit
e7ae63e
·
1 Parent(s): a2727ec

Cập nhật app.py và các file chatbot

Browse files
Files changed (4) hide show
  1. Dockerfile +14 -0
  2. app.py +177 -0
  3. packages.txt +1 -0
  4. requirements.txt +11 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+
7
+ # Thêm dòng này để cài đặt các gói từ packages.txt
8
+ RUN apt-get update && apt-get install -y $(cat packages.txt)
9
+
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # app.py (đoạn quan trọng: set cache + login + download)
3
+ # --- MUST BE AT THE TOP OF app.py, BEFORE ANY HF / TRANSFORMERS / SENTENCE_TRANSFORMERS IMPORT ---
4
+ import os
5
+ import shutil
6
+
7
+ # Put all HF/transformer caches into /tmp (writable in HF Space)
8
+ os.environ["HF_HOME"] = "/tmp/hf_home"
9
+ os.environ["TRANSFORMERS_CACHE"] = "/tmp/hf_cache" # deprecated warning is OK
10
+ os.environ["HF_DATASETS_CACHE"] = "/tmp/hf_datasets"
11
+ # Ensure libraries that respect XDG or HOME will write to /tmp instead of root '/.cache'
12
+ os.environ["XDG_CACHE_HOME"] = "/tmp/.cache"
13
+ os.environ["HOME"] = "/tmp"
14
+
15
+ # ensure directories exist and remove any old root-level cache attempt (ignore errors)
16
+ os.makedirs("/tmp/.cache", exist_ok=True)
17
+ shutil.rmtree("/.cache", ignore_errors=True)
18
+
19
+
20
+ # Bây giờ import cái cần thiết từ huggingface_hub để login trước khi download
21
+ from huggingface_hub import login, hf_hub_download
22
+
23
+ # LẤY TOKEN TỪ ENV (tên token phải khớp với key bạn thêm vào Secrets)
24
+ HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
25
+ if HF_TOKEN:
26
+ login(HF_TOKEN) # đăng nhập để có thể truy cập repo private
27
+ else:
28
+ # Không có token → chỉ truy cập repo public được
29
+ print("Warning: HF token not found in env (HF_TOKEN). Only public repos will be accessible.")
30
+
31
+ # --- Chọn repo dữ liệu chính xác và repo_type ---
32
+ # Nếu bạn lưu dữ liệu trong dataset repo, dùng repo_type="dataset"
33
+ HF_REPO_ID = "HungBB/egov-bot-data" # <--- kiểm tra & sửa lại theo repo của bạn
34
+ REPO_TYPE = "dataset" # hoặc "space" / "model" nếu data nằm ở đó
35
+
36
+ # Bây giờ import các thư viện khác (sau khi đã login / set cache)
37
+ import pickle
38
+ import gzip
39
+ import json
40
+ import re
41
+ import numpy as np
42
+ import faiss
43
+ from sentence_transformers import SentenceTransformer
44
+ from rank_bm25 import BM25Okapi
45
+ import google.generativeai as genai
46
+ from flask import Flask, request, jsonify
47
+ from flask_cors import CORS
48
+
49
+ print("--- KHỞI ĐỘNG MÁY CHỦ CHATBOT ---")
50
+
51
+ # --- TẢI FILES TỪ HUGGING FACE HUB ---
52
+ try:
53
+ print("Đang tải các tài nguyên cần thiết từ Hugging Face Hub...")
54
+
55
+ RAW_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="toan_bo_du_lieu_final.json", repo_type=REPO_TYPE)
56
+ FAISS_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="index.faiss", repo_type=REPO_TYPE)
57
+ METAS_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="metas.pkl.gz", repo_type=REPO_TYPE)
58
+ BM25_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="bm25.pkl.gz", repo_type=REPO_TYPE)
59
+ IDMAP_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="id_to_record.pkl", repo_type=REPO_TYPE) # ✅ thêm dòng này
60
+
61
+ print("✅ Tải file dữ liệu thành công!")
62
+
63
+ # Google API key (từ Secrets)
64
+ API_KEY = os.environ.get("GOOGLE_API_KEY")
65
+ if not API_KEY:
66
+ raise ValueError("Lỗi: GOOGLE_API_KEY chưa được thiết lập trong phần Secrets của Hugging Face Space.")
67
+ genai.configure(api_key=API_KEY)
68
+
69
+ generation_model = genai.GenerativeModel('gemini-2.5-flash')
70
+ embedding_model = SentenceTransformer("AITeamVN/Vietnamese_Embedding")
71
+
72
+ faiss_index = faiss.read_index(FAISS_PATH)
73
+ with gzip.open(METAS_PATH, "rb") as f:
74
+ metadatas = pickle.load(f)
75
+ with gzip.open(BM25_PATH, "rb") as f:
76
+ bm25 = pickle.load(f)
77
+
78
+ # ✅ Load trực tiếp map ID từ file pkl (nhanh hơn nhiều so với đọc JSON)
79
+ with open(IDMAP_PATH, "rb") as f:
80
+ procedure_map = pickle.load(f)
81
+
82
+ print(f"✅ Tải tài nguyên thành công! Sẵn có {faiss_index.ntotal} chunks kiến thức.")
83
+ print(f"✅ Có {len(procedure_map)} thủ tục hành chính trong id_to_record.pkl")
84
+
85
+ except Exception as e:
86
+ print(f"❌ LỖI KHI TẢI TÀI NGUYÊN: {e}")
87
+ # tùy chọn: raise e # nếu muốn Space hiển thị lỗi build rõ hơn
88
+
89
+ # --- 2. CÁC HÀM XỬ LÝ CỦA BỘ NÃO (LOGIC TỪ COLAB CỦA BẠN) ---
90
+ def classify_followup(text: str):
91
+ text = text.lower().strip()
92
+ score = 0
93
+ strong_followup_keywords = [r"\b(nó|cái (này|đó|ấy)|thủ tục (này|đó|ấy))\b", r"\b(vừa (nói|hỏi)|trước đó|ở trên|phía trên)\b", r"\b(tiếp theo|tiếp|còn nữa|ngoài ra)\b", r"\b(thế (thì|à)|vậy (thì|à)|như vậy)\b"]
94
+ detail_questions = [r"\b(mất bao lâu|thời gian|bao nhiêu tiền|chi phí|phí)\b", r"\b(ở đâu|tại đâu|chỗ nào|địa chỉ)\b", r"\b(cần (gì|những gì)|yêu cầu|điều kiện)\b"]
95
+ specific_services = [r"\b(làm|cấp|gia hạn|đổi|đăng ký)\s+(căn cước|cmnd|cccd)\b", r"\b(làm|cấp|gia hạn|đổi)\s+hộ chiếu\b", r"\b(đăng ký)\s+(kết hôn|sinh|tử|hộ khẩu)\b"]
96
+ if any(re.search(p, text) for p in strong_followup_keywords): score -= 3
97
+ if any(re.search(p, text) for p in detail_questions): score -= 2
98
+ if any(re.search(p, text) for p in specific_services): score += 3
99
+ if len(text.split()) <= 4: score -=1
100
+ return 0 if score < 0 else 1
101
+
102
+ def minmax_scale(arr):
103
+ arr = np.array(arr, dtype="float32")
104
+ if len(arr) == 0 or np.max(arr) == np.min(arr): return np.zeros_like(arr)
105
+ return (arr - np.min(arr)) / (np.max(arr) - np.min(arr))
106
+
107
+ def retrieve(query: str, top_k=3):
108
+ qv = embedding_model.encode([query], normalize_embeddings=True).astype("float32")
109
+ D, I = faiss_index.search(qv, top_k * 5)
110
+ vec_scores = (1 - D[0]).tolist()
111
+ vec_idx = I[0].tolist()
112
+ tokenized_query = query.split()
113
+ bm25_scores_all = bm25.get_scores(tokenized_query)
114
+ bm25_top_idx = np.argsort(-bm25_scores_all)[:top_k * 5].tolist()
115
+ union_idx = list(dict.fromkeys(vec_idx + bm25_top_idx))
116
+ vec_map = {i: s for i, s in zip(vec_idx, vec_scores)}
117
+ vec_list = [vec_map.get(i, 0.0) for i in union_idx]
118
+ bm25_list = [bm25_scores_all[i] for i in union_idx]
119
+ vec_scaled = minmax_scale(vec_list)
120
+ bm25_scaled = minmax_scale(bm25_list)
121
+ fused = 0.7 * vec_scaled + 0.3 * bm25_scaled
122
+ order = np.argsort(-fused)
123
+ return [union_idx[i] for i in order[:top_k]]
124
+
125
+ def get_full_procedure_text(parent_id):
126
+ procedure = procedure_map.get(parent_id)
127
+ if not procedure: return "Không tìm thấy thủ tục."
128
+ parts = []
129
+ field_map = {"ten_thu_tuc": "Tên thủ tục", "cach_thuc_thuc_hien": "Cách thức thực hiện", "thanh_phan_ho_so": "Thành phần hồ sơ", "trinh_tu_thuc_hien": "Trình tự thực hiện", "co_quan_thuc_hien": "Cơ quan thực hiện", "yeu_cau_dieu_kien": "Yêu cầu, điều kiện", "thu_tuc_lien_quan": "Thủ tục liên quan", "nguon": "Nguồn"}
130
+ for k, v in procedure.items():
131
+ if v and k in field_map:
132
+ parts.append(f"{field_map[k]}:\n{str(v).strip()}")
133
+ return "\n\n".join(parts)
134
+
135
+ # --- 3. KHỞI TẠO MÁY CHỦ FLASK VÀ API ---
136
+ app = Flask(__name__)
137
+ CORS(app)
138
+ chat_histories = {}
139
+
140
+ @app.route('/chat', methods=['POST'])
141
+ def chat():
142
+ data = request.json
143
+ user_query = data.get('question')
144
+ session_id = data.get('session_id', 'default')
145
+ if not user_query:
146
+ return jsonify({"error": "Không có câu hỏi nào được cung cấp"}), 400
147
+ if session_id not in chat_histories:
148
+ chat_histories[session_id] = []
149
+ current_history = chat_histories[session_id]
150
+ context = ""
151
+ if classify_followup(user_query) == 0 and current_history:
152
+ context = current_history[-1].get('context', '')
153
+ print(f"[{session_id}] Dùng lại ngữ cảnh cũ.")
154
+ else:
155
+ retrieved_indices = retrieve(user_query)
156
+ if retrieved_indices:
157
+ parent_id = metadatas[retrieved_indices[0]]["parent_id"]
158
+ context = get_full_procedure_text(parent_id)
159
+ print(f"[{session_id}] Đã tìm được ngữ cảnh mới.")
160
+ history_str = "\n".join([f"{item['role']}: {item['content']}" for item in current_history])
161
+ prompt = f"""Bạn là trợ lý eGov-Bot dịch vụ công Việt Nam. Trả lời tiếng Việt, chính xác, dựa hoàn toàn vào DỮ LIỆU được cung cấp.
162
+ Nếu thiếu dữ liệu, hãy nói "Mình chưa có thông tin" và đưa link nguồn trong dữ liệu để người dùng tham khảo thêm.
163
+
164
+ Lịch sử trò chuyện:
165
+ {history_str}
166
+
167
+ DỮ LIỆU: --- {context} ---
168
+ CÂU HỎI: {user_query}"""
169
+ response = generation_model.generate_content(prompt)
170
+ final_answer = response.text
171
+ current_history.append({'role': 'user', 'content': user_query})
172
+ current_history.append({'role': 'model', 'content': final_answer, 'context': context})
173
+ return jsonify({"answer": final_answer})
174
+
175
+ if __name__ == '__main__':
176
+ app.run(host='0.0.0.0', port=7860)
177
+
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ swig
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask
2
+ flask-cors
3
+ numpy==1.26.4
4
+ sentence-transformers==2.6.1
5
+ transformers>=4.30.0
6
+ torch>=2.0.0
7
+ faiss-cpu==1.7.4
8
+ rank_bm25==0.2.2
9
+ google-generativeai
10
+ huggingface-hub
11
+ sentencepiece