File size: 10,185 Bytes
b655c88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab66a23
47738d8
 
b655c88
 
4f1a142
 
 
 
 
 
 
 
 
 
 
 
b655c88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47738d8
 
ab66a23
4f1a142
 
47738d8
b655c88
 
 
 
 
f73363c
 
 
 
b655c88
 
 
 
f73363c
b655c88
f73363c
b655c88
 
 
 
 
 
 
 
 
f73363c
 
b655c88
f73363c
 
47738d8
f73363c
 
 
 
47738d8
f73363c
47738d8
 
f73363c
ab66a23
f73363c
4f1a142
 
47738d8
b655c88
 
 
f73363c
 
 
 
b655c88
 
 
 
ab66a23
f73363c
 
b655c88
f73363c
 
 
 
 
 
b655c88
 
 
 
f73363c
 
b655c88
f73363c
 
ab66a23
 
f73363c
 
 
 
 
b655c88
 
 
 
 
 
 
 
 
0700453
 
 
 
 
 
b655c88
0700453
 
 
b655c88
0700453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab66a23
4f1a142
 
0700453
 
 
 
b655c88
 
 
 
 
 
 
 
 
 
 
 
 
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
"""
Admin Service Layer: Tài liệu hệ thống, User, History, Feedback, RAG thống kê/reindex.
Dùng cho trang quản trị Admin.
"""

import os
import sys
import uuid
import shutil

# Đảm bảo import được data_processing (từ thư mục gốc project)
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT_DIR not in sys.path:
    sys.path.insert(0, ROOT_DIR)

from backend import db
from backend.auth import is_admin
from backend.runtime_paths import PDF_DIR
from backend.db_sync import schedule_pdf_upload, schedule_pdf_delete, schedule_vector_sync
from data_processing.dynamic_indexing import add_pdf_file
from data_processing.indexing import delete_chunks_by_source


# ======================== RAG Cache Reset ========================

def _clear_rag_cache():
    """Reset cache RAG để cập nhật danh sách tài liệu mới sau khi thêm/sửa/xóa."""
    try:
        from backend import rag_chain_pg
        rag_chain_pg._source_cache = None
        print("[Admin] ✅ Đã clear RAG source cache")
    except Exception as e:
        print(f"[Admin] ⚠️ Lỗi clear cache: {e}")


# ======================== UserService ========================

def list_users(limit: int = 500) -> list:
    """Danh sách người dùng (admin)."""
    return db.list_users(limit=limit)


def update_user_role(ma_nguoi_dung: str, vai_tro: str) -> tuple[bool, str]:
    """Cập nhật vai trò (user/admin). Returns (success, message)."""
    if vai_tro not in ("user", "admin"):
        return False, "Vai trò không hợp lệ"
    ok = db.update_user_role(ma_nguoi_dung, vai_tro)
    return ok, "Đã cập nhật vai trò" if ok else "Cập nhật thất bại"


def lock_user(ma_nguoi_dung: str) -> bool:
    """Khóa tài khoản."""
    return db.lock_user(ma_nguoi_dung)


def unlock_user(ma_nguoi_dung: str) -> bool:
    """Mở khóa tài khoản."""
    return db.unlock_user(ma_nguoi_dung)


# ======================== HistoryService ========================

def list_conversations(limit: int = 200) -> list:
    """Lịch sử hỏi đáp toàn hệ thống (admin)."""
    return db.list_conversations_admin(limit=limit)


def list_feedback(limit: int = 300) -> list:
    """Danh sách phản hồi người dùng (admin)."""
    return db.list_feedback_admin(limit=limit)


def update_feedback_status(ma_phan_hoi: int, trang_thai: str) -> tuple[bool, str]:
    """Cập nhật trạng thái xử lý phản hồi: moi / daxem / dong."""
    if trang_thai not in ("moi", "daxem", "dong"):
        return False, "Trạng thái không hợp lệ"
    ok = db.update_phan_hoi_status(ma_phan_hoi, trang_thai)
    return ok, "Đã cập nhật" if ok else "Cập nhật thất bại"


# ======================== Tài liệu hệ thống (DocumentService) ========================


def list_system_docs() -> list:
    """
    Danh sách tài liệu hệ thống: từ bảng tai_lieu_he_thong (nếu có)
    và/hoặc quét thư mục PDF runtime để đồng bộ.
    """
    from backend.db import list_tai_lieu_he_thong
    rows = list_tai_lieu_he_thong()
    # Bổ sung từ thư mục PDF runtime nếu file tồn tại nhưng chưa trong DB
    os.makedirs(PDF_DIR, exist_ok=True)
    for f in os.listdir(PDF_DIR):
        if f.lower().endswith(".pdf"):
            path = os.path.join(PDF_DIR, f)
            path_norm = os.path.normpath(path)
            if not any(os.path.normpath(r["duong_dan"]) == path_norm for r in rows):
                rows.append({
                    "ma_tai_lieu": f"file_{os.path.splitext(f)[0]}",
                    "ten_file": f,
                    "duong_dan": path_norm,
                    "ngay_them": None,
                    "ngay_cap_nhat": None,
                })
    return rows


def create_system_doc(file_path: str, filename: str = None) -> tuple[bool, str]:
    """
    Thêm tài liệu hệ thống: copy file vào thư mục PDF runtime và ghi DB.
    file_path: đường dẫn file tải lên (Streamlit UploadedFile hoặc path).
    """
    filename = filename or os.path.basename(file_path)
    if not filename.lower().endswith(".pdf"):
        return False, "Chỉ chấp nhận file PDF"
    ma = str(uuid.uuid4())
    dest = os.path.join(PDF_DIR, filename)
    os.makedirs(PDF_DIR, exist_ok=True)
    try:
        if hasattr(file_path, "read"):
            with open(dest, "wb") as f:
                f.write(file_path.read())
        else:
            shutil.copy2(file_path, dest)
        db.insert_tai_lieu_he_thong(ma_tai_lieu=ma, ten_file=filename, duong_dan=os.path.abspath(dest))
        # Index ngay sau khi upload để hỏi đáp dùng được luôn.
        chunks_added = add_pdf_file(dest)
        schedule_pdf_upload(dest, filename)
        # Clear cache để tài liệu mới có thể được search ngay lập tức
        _clear_rag_cache()
        return True, f"Đã thêm tài liệu và index {chunks_added} chunks."
    except Exception as e:
        return False, str(e)


def update_system_doc(ma_tai_lieu: str, file_path: str = None, ten_file: str = None) -> tuple[bool, str]:
    """
    Cập nhật tài liệu hệ thống (metadata hoặc thay file).
    Nếu thay file: xóa chunks cũ → add chunks mới.
    """
    docs = [r for r in list_system_docs() if r.get("ma_tai_lieu") == ma_tai_lieu]
    if not docs:
        return False, "Không tìm thấy tài liệu"
    old_path = docs[0].get("duong_dan")
    old_filename = docs[0].get("ten_file")  # Tên file cũ (để xóa chunks cũ)
    new_name = ten_file or (os.path.basename(file_path) if file_path else docs[0]["ten_file"])
    
    if file_path and os.path.exists(old_path):
        try:
            if hasattr(file_path, "read"):
                with open(old_path, "wb") as f:
                    f.write(file_path.read())
            else:
                shutil.copy2(file_path, old_path)
        except Exception as e:
            return False, str(e)
    
    # Cập nhật tên trong DB (upsert)
    db.insert_tai_lieu_he_thong(ma_tai_lieu=ma_tai_lieu, ten_file=new_name, duong_dan=old_path)
    
    # Khi thay file, cần cập nhật vector ngay để tránh dùng dữ liệu cũ
    if file_path:
        # Xóa chunks của file cũ trước
        deleted_chunks = delete_chunks_by_source(old_filename)
        print(f"[Admin] Deleted {deleted_chunks} old chunks for {old_filename}")
        # Add chunks của file mới
        chunks_added = add_pdf_file(old_path)
        msg = f"Đã cập nhật và index lại {chunks_added} chunks (xóa {deleted_chunks} chunks cũ)."
    else:
        msg = "Đã cập nhật metadata tài liệu."
    
    schedule_pdf_upload(old_path, new_name)
    
    # Clear cache để cập nhật được phản ánh ngay
    _clear_rag_cache()
    return True, msg


def delete_system_doc(ma_tai_lieu: str) -> tuple[bool, str]:
    """
    Xóa tài liệu hệ thống: xóa chunks từ ChromaDB, xóa file, và xóa bản ghi DB.
    Cập nhật tài liệu + chunks đồng bộ.
    """
    docs = [r for r in list_system_docs() if r.get("ma_tai_lieu") == ma_tai_lieu]
    if not docs:
        return False, "Không tìm thấy tài liệu"
    path = docs[0].get("duong_dan")
    filename = docs[0].get("ten_file") or (os.path.basename(path) if path else "")
    
    deleted_chunks = 0
    try:
        # 1. Xóa chunks từ ChromaDB trước
        if filename:
            deleted_chunks = delete_chunks_by_source(filename)
            print(f"[Admin] Deleted {deleted_chunks} chunks for {filename}")
        
        # 2. Xóa file vật lý
        if path and os.path.isfile(path):
            os.remove(path)
    except OSError:
        pass
    
    # 3. Xóa DB record
    db.delete_tai_lieu_he_thong(ma_tai_lieu)
    
    # 4. Upload delete notification to HF
    if filename:
        schedule_pdf_delete(filename)
    
    # 5. Clear RAG cache để cập nhật danh sách tài liệu + chunks
    _clear_rag_cache()
    
    return True, f"Đã xóa tài liệu '{filename}' và {deleted_chunks} chunks khỏi hệ thống."


def reindex_doc(ma_tai_lieu: str) -> tuple[bool, str]:
    """Tái lập chỉ mục cho một tài liệu (reindex toàn bộ vẫn an toàn hơn)."""
    # Đơn giản: gọi reindex_all (ChromaDB không hỗ trợ xóa theo source dễ dàng)
    return reindex_all()


def reindex_all() -> tuple[bool, str]:
    """
    Đồng bộ chỉ mục theo kiểu tăng dần (incremental):
    - Quét thư mục PDF runtime
    - Chỉ index tài liệu CHƯA có trong ChromaDB
    Tránh reindex toàn bộ gây chậm khi số tài liệu lớn.
    """
    try:
        from data_processing.dynamic_indexing import add_pdf_file

        if not os.path.isdir(PDF_DIR):
            return False, f"Không có tài liệu PDF trong {PDF_DIR}"

        pdf_files = [
            os.path.join(PDF_DIR, f)
            for f in sorted(os.listdir(PDF_DIR))
            if f.lower().endswith(".pdf")
        ]
        if not pdf_files:
            return False, f"Không có tài liệu PDF trong {PDF_DIR}"

        indexed_docs = 0
        indexed_chunks = 0
        skipped_docs = 0

        for pdf_path in pdf_files:
            added = add_pdf_file(pdf_path)
            if added > 0:
                indexed_docs += 1
                indexed_chunks += added
            else:
                skipped_docs += 1

        schedule_vector_sync()
        # Clear cache sau khi reindex để search ngay được tài liệu mới
        _clear_rag_cache()
        return True, (
            f"Đồng bộ xong: thêm mới {indexed_docs} tài liệu / {indexed_chunks} chunks, "
            f"bỏ qua {skipped_docs} tài liệu đã có."
        )
    except Exception as e:
        return False, f"Lỗi: {str(e)}"


# ======================== RAGAdminService ========================

def get_rag_stats() -> dict:
    """Thống kê RAG (ChromaDB)."""
    try:
        from data_processing.indexing import get_stats
        return get_stats()
    except Exception as e:
        return {"error": str(e), "total_chunks": 0, "collection_name": ""}