DrPie commited on
Commit
76c06ab
·
verified ·
1 Parent(s): 25ffee2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -152
app.py CHANGED
@@ -1,164 +1,103 @@
1
- # --- MUST BE AT THE TOP ---
2
  import os
3
- import shutil
4
-
5
- # Đặt cache vào /tmp (HF Space cho phép ghi vào /tmp)
6
- os.environ["HF_HOME"] = "/tmp/hf_home"
7
- os.environ["TRANSFORMERS_CACHE"] = "/tmp/hf_cache"
8
- os.environ["HF_DATASETS_CACHE"] = "/tmp/hf_datasets"
9
- os.environ["XDG_CACHE_HOME"] = "/tmp/.cache"
10
- os.environ["HOME"] = "/tmp"
11
- os.makedirs("/tmp/.cache", exist_ok=True)
12
- shutil.rmtree("/.cache", ignore_errors=True)
13
-
14
- # --- LOGIN HF HUB ---
15
- from huggingface_hub import login, hf_hub_download
16
- HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN")
17
- if HF_TOKEN:
18
- login(HF_TOKEN)
19
- else:
20
- print("Warning: HF token not found. Only public repos will be accessible.")
21
-
22
- # --- LOAD DỮ LIỆU ---
23
- HF_REPO_ID = "DrPie/eGoV_Data" # dataset repo chứa dữ liệu
24
- REPO_TYPE = "dataset"
25
-
26
- import pickle, gzip, re, json
27
  import numpy as np
28
- from rank_bm25 import BM25Okapi
29
- import google.generativeai as genai
30
  from flask import Flask, request, jsonify
31
- from flask_cors import CORS
32
-
33
- print("--- KHỞI ĐỘNG MÁY CHỦ CHATBOT ---")
34
- try:
35
- print("Đang tải các tài nguyên cần thiết từ Hugging Face Hub...")
36
- RAW_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="toan_bo_du_lieu_final.json", repo_type=REPO_TYPE)
37
- BM25_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="bm25.pkl.gz", repo_type=REPO_TYPE)
38
- IDMAP_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="id_to_record.pkl", repo_type=REPO_TYPE)
39
-
40
- print("✅ Tải file dữ liệu thành công!")
41
-
42
- API_KEY = os.environ.get("GOOGLE_API_KEY")
43
- if not API_KEY:
44
- raise ValueError("Lỗi: GOOGLE_API_KEY chưa được thiết lập trong Secrets của Space")
45
- genai.configure(api_key=API_KEY)
46
- generation_model = genai.GenerativeModel('gemini-2.5-flash')
47
-
48
- # Không còn embedding và FAISS
49
- with gzip.open(BM25_PATH, "rb") as f:
50
- bm25 = pickle.load(f)
51
- with open(IDMAP_PATH, "rb") as f:
52
- procedure_map = pickle.load(f)
53
-
54
- print(f"✅ BM25 loaded, tổng {len(procedure_map)} thủ tục hành chính.")
55
-
56
- except Exception as e:
57
- print(f"❌ Lỗi khi tải tài nguyên: {e}")
58
-
59
- # --- LOGIC XỬ LÝ ---
60
- def classify_followup(text: str):
61
- text = text.lower().strip()
62
- score = 0
63
- strong_followup = [
64
- r"\b(nó|cái (này|đó|ấy)|thủ tục (này|đó|ấy))\b",
65
- r"\b(vừa (nói|hỏi)|trước đó|ở trên|phía trên)\b",
66
- r"\b(tiếp theo|tiếp|còn nữa|ngoài ra)\b",
67
- r"\b(thế (thì|à)|vậy (thì|à)|như vậy)\b"
68
- ]
69
- detail_qs = [
70
- r"\b(mất bao lâu|thời gian|bao nhiêu tiền|chi phí|phí)\b",
71
- r"\b(ở đâu|tại đâu|chỗ nào|địa chỉ)\b",
72
- r"\b(cần (gì|những gì)|yêu cầu|điều kiện)\b"
73
- ]
74
- specific_services = [
75
- r"\b(làm|cấp|gia hạn|đổi|đăng ký)\s+(căn cước|cmnd|cccd)\b",
76
- r"\b(làm|cấp|gia hạn|đổi)\s+hộ chiếu\b",
77
- r"\b(đăng ký)\s+(kết hôn|sinh|tử|hộ khẩu)\b"
78
- ]
79
- if any(re.search(p, text) for p in strong_followup):
80
- score -= 3
81
- if any(re.search(p, text) for p in detail_qs):
82
- score -= 2
83
- if any(re.search(p, text) for p in specific_services):
84
- score += 3
85
- if len(text.split()) <= 4:
86
- score -= 1
87
- return 0 if score < 0 else 1
88
-
89
- def retrieve(query: str, top_k=3):
90
- # Chỉ dùng BM25
91
- tokenized_query = query.split()
92
- bm25_scores = bm25.get_scores(tokenized_query)
93
- top_idx = np.argsort(-bm25_scores)[:top_k].tolist()
94
- return top_idx
95
-
96
- def get_full_procedure_text(parent_id):
97
- procedure = procedure_map.get(parent_id)
98
- if not procedure:
99
- return "Không tìm thấy thủ tục."
100
- field_map = {
101
- "ten_thu_tuc": "Tên thủ tục",
102
- "cach_thuc_thuc_hien": "Cách thức thực hiện",
103
- "thanh_phan_ho_so": "Thành phần hồ sơ",
104
- "trinh_tu_thuc_hien": "Trình tự thực hiện",
105
- "co_quan_thuc_hien": "Cơ quan thực hiện",
106
- "yeu_cau_dieu_kien": "Yêu cầu, điều kiện",
107
- "thu_tuc_lien_quan": "Thủ tục liên quan",
108
- "nguon": "Nguồn"
109
- }
110
- parts = [f"{field_map[k]}:\n{str(v).strip()}" for k,v in procedure.items() if v and k in field_map]
111
- return "\n\n".join(parts)
112
-
113
- # --- FLASK APP ---
114
- app = Flask(__name__)
115
- CORS(app)
116
 
117
- @app.route('/', methods=['GET'])
118
- def home():
119
- return "eGov-Bot backend is running!", 200
120
 
121
- chat_histories = {}
 
 
 
 
 
122
 
123
- @app.route('/chat', methods=['POST'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def chat():
125
- data = request.json
126
- user_query = data.get('question')
127
- session_id = data.get('session_id', 'default')
128
-
129
- if not user_query:
130
- return jsonify({"error": "Không có câu hỏi"}), 400
131
-
132
- if session_id not in chat_histories:
133
- chat_histories[session_id] = []
134
- current_history = chat_histories[session_id]
135
-
136
- context = ""
137
- if classify_followup(user_query) == 0 and current_history:
138
- context = current_history[-1].get('context', '')
139
- else:
140
- retrieved_indices = retrieve(user_query)
141
- if retrieved_indices:
142
- parent_id = retrieved_indices[0]
143
- context = get_full_procedure_text(parent_id)
144
-
145
- history_str = "\n".join([f"{item['role']}: {item['content']}" for item in current_history])
146
- prompt = f"""Bạn là trợ lý eGov-Bot. Trả lời tiếng Việt, chính xác, dựa vào DỮ LIỆU sau.
147
- 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 để tham khảo.
148
- Lịch sử trò chuyện: {history_str}
149
  DỮ LIỆU:
150
- ---
151
- {context}
152
- ---
153
- CÂU HỎI: {user_query}"""
154
 
155
- response = generation_model.generate_content(prompt)
156
- answer = response.text
157
 
158
- current_history.append({'role': 'user', 'content': user_query})
159
- current_history.append({'role': 'model', 'content': answer, 'context': context})
 
 
 
160
 
161
- return jsonify({"answer": answer})
 
 
162
 
163
- if __name__ == '__main__':
164
- app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
 
 
 
1
  import os
2
+ import re
3
+ import unicodedata
4
+ import pickle
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import numpy as np
 
 
6
  from flask import Flask, request, jsonify
7
+ from rank_bm25 import BM25Okapi
8
+ from huggingface_hub import InferenceClient
9
+
10
+ # ===================== #
11
+ # TIỀN XỬ VĂN BẢN #
12
+ # ===================== #
13
+
14
+ def normalize_text(text: str) -> str:
15
+ text = text.lower()
16
+ text = ''.join(c for c in unicodedata.normalize('NFD', text) if unicodedata.category(c) != 'Mn') # bỏ dấu tiếng Việt
17
+ text = re.sub(r'[^a-z0-9\s]', ' ', text) # bỏ ký tự đặc biệt
18
+ return text
19
+
20
+ def tokenize(text: str):
21
+ return normalize_text(text).split()
22
+
23
+ # ===================== #
24
+ # LOAD DỮ LIỆU #
25
+ # ===================== #
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ # File id_to_record.pkl chứa dict: id -> {ten_thu_tuc, mo_ta, yeu_cau, co_quan, link ...}
28
+ with open("id_to_record.pkl", "rb") as f:
29
+ id_to_record = pickle.load(f)
30
 
31
+ # Tạo corpus cho BM25: mỗi record nối các trường thành 1 text
32
+ corpus = []
33
+ for rid, rec in id_to_record.items():
34
+ fields = [str(rec.get(k, "")) for k in ["ten_thu_tuc", "mo_ta", "yeu_cau", "co_quan", "linh_vuc"]]
35
+ text = " ".join(fields)
36
+ corpus.append(tokenize(text))
37
 
38
+ bm25 = BM25Okapi(corpus)
39
+
40
+ # ===================== #
41
+ # KHỞI TẠO FLASK APP #
42
+ # ===================== #
43
+
44
+ app = Flask(__name__)
45
+ HF_TOKEN = os.getenv("HF_TOKEN")
46
+ HF_MODEL = os.getenv("HF_MODEL", "gemini-pro") # đổi sang model bạn dùng
47
+ client = InferenceClient(token=HF_TOKEN)
48
+
49
+ # ===================== #
50
+ # HÀM LẤY CONTEXT #
51
+ # ===================== #
52
+
53
+ def retrieve_context(query: str, top_k: int = 5):
54
+ tokens = tokenize(query)
55
+ scores = bm25.get_scores(tokens)
56
+ top_idx = np.argsort(-scores)[:top_k]
57
+ context_parts = []
58
+ for idx in top_idx:
59
+ if scores[idx] > 0: # chỉ lấy nếu score > 0
60
+ rid = list(id_to_record.keys())[idx]
61
+ rec = id_to_record[rid]
62
+ # context gồm tên, mô tả, yêu cầu và link nếu có
63
+ ctx = f"Tên: {rec.get('ten_thu_tuc','')}\nMô tả: {rec.get('mo_ta','')}\nYêu cầu: {rec.get('yeu_cau','')}\nCơ quan: {rec.get('co_quan','')}\nLink: {rec.get('link','')}"
64
+ context_parts.append(ctx)
65
+ return "\n\n".join(context_parts)
66
+
67
+ # ===================== #
68
+ # ROUTE /chat #
69
+ # ===================== #
70
+
71
+ @app.route("/chat", methods=["POST"])
72
  def chat():
73
+ user_query = request.json.get("query", "")
74
+ if not user_query.strip():
75
+ return jsonify({"answer": "Bạn chưa nhập câu hỏi."})
76
+
77
+ context = retrieve_context(user_query)
78
+
79
+ prompt = f"""
80
+ Bạn trợ eGov-Bot, trả lời bằng tiếng Việt.
81
+ Ưu tiên dùng thông tin từ DỮ LIỆU dưới đây để trả lời.
82
+ Nếu dữ liệu không đủ, có thể suy luận hợp lý hoặc trả lời rằng chưa có đủ thông tin.
83
+ Nếu có link nguồn trong dữ liệu, hãy cung cấp.
84
+
 
 
 
 
 
 
 
 
 
 
 
 
85
  DỮ LIỆU:
86
+ {context if context.strip() else "Không tìm thấy thông tin nào khớp trực tiếp."}
 
 
 
87
 
88
+ CÂU HỎI: {user_query}
89
+ """
90
 
91
+ try:
92
+ response = client.text_generation(model=HF_MODEL, prompt=prompt, max_new_tokens=512)
93
+ return jsonify({"answer": response.strip()})
94
+ except Exception as e:
95
+ return jsonify({"answer": f"Lỗi khi gọi model: {e}"})
96
 
97
+ # ===================== #
98
+ # MAIN APP #
99
+ # ===================== #
100
 
101
+ if __name__ == "__main__":
102
+ # Debug mode cho dev, production có thể bỏ
103
+ app.run(host="0.0.0.0", port=7860)