File size: 6,897 Bytes
7fcfdd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# app.py
import os
import pickle
import gzip
import json
import re
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
from rank_bm25 import BM25Okapi
import google.generativeai as genai
from flask import Flask, request, jsonify
from flask_cors import CORS
from huggingface_hub import hf_hub_download # Thư viện để tải file từ Hub

print("--- KHỞI ĐỘNG MÁY CHỦ CHATBOT ---")

# --- 1. THIẾT LẬP VÀ TẢI TÀI NGUYÊN TỪ HUGGING FACE HUB ---
try:
    print("Đang tải các tài nguyên cần thiết từ Hugging Face Hub...")
    
    # !!! THAY THẾ BẰNG USERNAME VÀ TÊN DATASET CỦA BẠN !!!
    HF_REPO_ID = "TEN_USERNAME_HF/egov-bot-data" 

    # Tự động tải các file từ "Kho Dữ liệu" về môi trường của Space
    RAW_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="toan_bo_du_lieu_final.json")
    FAISS_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="index.faiss")
    METAS_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="metas.pkl.gz")
    BM25_PATH = hf_hub_download(repo_id=HF_REPO_ID, filename="bm25.pkl.gz")
    print("✅ Tải file dữ liệu thành công!")

    # Lấy API Key từ Secret của Hugging Face
    API_KEY = os.environ.get("GOOGLE_API_KEY") 
    genai.configure(api_key=API_KEY)

    # Tải các mô hình và dữ liệu
    generation_model = genai.GenerativeModel('gemini-2.5-flash') 
    embedding_model = SentenceTransformer("AITeamVN/Vietnamese_Embedding")
    
    faiss_index = faiss.read_index(FAISS_PATH)
    with gzip.open(METAS_PATH, "rb") as f:
        metadatas = pickle.load(f)
    with gzip.open(BM25_PATH, "rb") as f:
        bm25 = pickle.load(f)
    with open(RAW_PATH, "r", encoding="utf-8") as f:
        raw_data = json.load(f)
        procedure_map = {item['nguon']: item for item in raw_data}

    print(f"✅ Tải tài nguyên thành công! Sẵn có {faiss_index.ntotal} chunks kiến thức.")

except Exception as e:
    print(f"❌ LỖI KHI TẢI TÀI NGUYÊN: {e}")

# --- 2. CÁC HÀM XỬ LÝ CỦA BỘ NÃO (LOGIC TỪ COLAB CỦA BẠN) ---
# (Toàn bộ các hàm classify_followup, minmax_scale, retrieve, get_full_procedure_text của bạn được giữ nguyên ở đây)
def classify_followup(text: str):
    text = text.lower().strip()
    score = 0
    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"]
    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"]
    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"]
    if any(re.search(p, text) for p in strong_followup_keywords): score -= 3
    if any(re.search(p, text) for p in detail_questions): score -= 2
    if any(re.search(p, text) for p in specific_services): score += 3
    if len(text.split()) <= 4: score -=1
    return 0 if score < 0 else 1

def minmax_scale(arr):
    arr = np.array(arr, dtype="float32")
    if len(arr) == 0 or np.max(arr) == np.min(arr): return np.zeros_like(arr)
    return (arr - np.min(arr)) / (np.max(arr) - np.min(arr))

def retrieve(query: str, top_k=3):
    qv = embedding_model.encode([query], normalize_embeddings=True).astype("float32")
    D, I = faiss_index.search(qv, top_k * 5)
    vec_scores = (1 - D[0]).tolist()
    vec_idx = I[0].tolist()
    tokenized_query = query.split()
    bm25_scores_all = bm25.get_scores(tokenized_query)
    bm25_top_idx = np.argsort(-bm25_scores_all)[:top_k * 5].tolist()
    union_idx = list(dict.fromkeys(vec_idx + bm25_top_idx))
    vec_map = {i: s for i, s in zip(vec_idx, vec_scores)}
    vec_list = [vec_map.get(i, 0.0) for i in union_idx]
    bm25_list = [bm25_scores_all[i] for i in union_idx]
    vec_scaled = minmax_scale(vec_list)
    bm25_scaled = minmax_scale(bm25_list)
    fused = 0.7 * vec_scaled + 0.3 * bm25_scaled
    order = np.argsort(-fused)
    return [union_idx[i] for i in order[:top_k]]

def get_full_procedure_text(parent_id):
    procedure = procedure_map.get(parent_id)
    if not procedure: return "Không tìm thấy thủ tục."
    parts = []
    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"}
    for k, v in procedure.items():
        if v and k in field_map:
            parts.append(f"{field_map[k]}:\n{str(v).strip()}")
    return "\n\n".join(parts)

# --- 3. KHỞI TẠO MÁY CHỦ FLASK VÀ API ---
app = Flask(__name__)
CORS(app)

chat_histories = {} 

@app.route('/chat', methods=['POST'])
def chat():
    data = request.json
    user_query = data.get('question')
    session_id = data.get('session_id', 'default')

    if not user_query:
        return jsonify({"error": "Không có câu hỏi nào được cung cấp"}), 400

    if session_id not in chat_histories:
        chat_histories[session_id] = []
    
    current_history = chat_histories[session_id]
    
    context = ""
    if classify_followup(user_query) == 0 and current_history:
        context = current_history[-1].get('context', '')
        print(f"[{session_id}] Dùng lại ngữ cảnh cũ cho câu hỏi followup.")
    else:
        retrieved_indices = retrieve(user_query)
        if retrieved_indices:
            parent_id = metadatas[retrieved_indices[0]]["parent_id"]
            context = get_full_procedure_text(parent_id)
        print(f"[{session_id}] Đã tìm được ngữ cảnh mới.")

    history_str = "\n".join([f"{item['role']}: {item['content']}" for item in current_history])
    prompt = f"""Bạn là trợ lý eGov-Bot... (Nội dung prompt của bạn ở đây)

    Lịch sử trò chuyện: {history_str}

    DỮ LIỆU: --- {context} ---

    CÂU HỎI: {user_query}

    """
    
    response = generation_model.generate_content(prompt)
    final_answer = response.text

    current_history.append({'role': 'user', 'content': user_query})
    current_history.append({'role': 'model', 'content': final_answer, 'context': context})
    
    return jsonify({"answer": final_answer})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=7860)