HaRin2806 commited on
Commit
8275526
·
1 Parent(s): e467988

upload backend

Browse files
.env ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ GEMINI_API_KEY=AIzaSyBW0gv3-bwuKE05MP8y0N0lA2Y2KpyTvCs
2
+ MONGO_URI=mongodb+srv://linhha2705:[email protected]/
3
+ MONGO_DB_NAME=nutribot_db
4
+ JWT_SECRET_KEY=hathimylinh
5
+ OPENAI_API_KEY=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
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Sử dụng bookworm thay vì bullseye vì có SQLite3 phiên bản mới hơn
2
+ FROM python:3.9-slim-bookworm
3
+
4
+ # Thiết lập working directory
5
+ WORKDIR /app
6
+
7
+ # Thiết lập environment variables
8
+ ENV PYTHONPATH=/app
9
+ ENV ENVIRONMENT=production
10
+ ENV PYTHONUNBUFFERED=1
11
+
12
+ # Cài đặt system dependencies cần thiết cho ChromaDB
13
+ RUN apt-get update && apt-get install -y \
14
+ build-essential \
15
+ gcc \
16
+ g++ \
17
+ cmake \
18
+ sqlite3 \
19
+ libsqlite3-dev \
20
+ && rm -rf /var/lib/apt/lists/*
21
+
22
+ # Copy requirements và cài đặt Python dependencies
23
+ COPY requirements.txt .
24
+
25
+ # Cài đặt pysqlite3-binary trước để override SQLite3 nếu cần
26
+ RUN pip install --no-cache-dir pysqlite3-binary
27
+
28
+ # Cài đặt các dependencies khác
29
+ RUN pip install --no-cache-dir -r requirements.txt
30
+
31
+ # Copy source code
32
+ COPY . .
33
+
34
+ # Tạo thư mục cần thiết
35
+ RUN mkdir -p logs uploads chroma_data
36
+
37
+ # Expose port
38
+ EXPOSE 7860
39
+
40
+ # Chạy ứng dụng
41
+ CMD ["python", "app.py"]
api/__init__.py ADDED
File without changes
api/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (122 Bytes). View file
 
api/__pycache__/admin.cpython-39.pyc ADDED
Binary file (51 kB). View file
 
api/__pycache__/auth.cpython-39.pyc ADDED
Binary file (10.9 kB). View file
 
api/__pycache__/chat.cpython-39.pyc ADDED
Binary file (8.42 kB). View file
 
api/__pycache__/data.cpython-39.pyc ADDED
Binary file (1.94 kB). View file
 
api/__pycache__/feedback.cpython-39.pyc ADDED
Binary file (2.84 kB). View file
 
api/__pycache__/history.cpython-39.pyc ADDED
Binary file (14.8 kB). View file
 
api/admin.py ADDED
@@ -0,0 +1,2385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ import logging
3
+ import datetime
4
+ import os
5
+ from bson.objectid import ObjectId
6
+ from models.user_model import User
7
+ from models.conversation_model import get_db, Conversation
8
+ from flask_jwt_extended import jwt_required, get_jwt_identity
9
+ from functools import wraps
10
+ import json
11
+ from core.embedding_model import get_embedding_model
12
+ from werkzeug.utils import secure_filename
13
+ import google.generativeai as genai
14
+ from config import GEMINI_API_KEY
15
+ import time
16
+ import sys
17
+ from models.feedback_model import Feedback
18
+
19
+ # Thiết lập logging
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Tạo blueprint
23
+ admin_routes = Blueprint('admin', __name__)
24
+
25
+ # Configure Gemini
26
+ genai.configure(api_key=GEMINI_API_KEY)
27
+
28
+ def setup_debug_logging():
29
+ """Thiết lập logging debug cho Gemini response"""
30
+ log_dir = "logs"
31
+ if not os.path.exists(log_dir):
32
+ os.makedirs(log_dir)
33
+ return log_dir
34
+
35
+ def save_gemini_response_to_file(response_text, parsed_data, doc_id):
36
+ """Lưu response của Gemini vào file để debug"""
37
+ try:
38
+ log_dir = setup_debug_logging()
39
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
40
+
41
+ # Tạo tên file log
42
+ log_filename = f"gemini_response_{doc_id}_{timestamp}.log"
43
+ log_path = os.path.join(log_dir, log_filename)
44
+
45
+ with open(log_path, 'w', encoding='utf-8') as f:
46
+ f.write("=" * 80 + "\n")
47
+ f.write(f"GEMINI RESPONSE DEBUG LOG\n")
48
+ f.write(f"Document ID: {doc_id}\n")
49
+ f.write(f"Timestamp: {datetime.datetime.now().isoformat()}\n")
50
+ f.write("=" * 80 + "\n\n")
51
+
52
+ f.write("RAW RESPONSE FROM GEMINI:\n")
53
+ f.write("-" * 40 + "\n")
54
+ f.write(response_text)
55
+ f.write("\n\n")
56
+
57
+ f.write("PARSED JSON DATA:\n")
58
+ f.write("-" * 40 + "\n")
59
+ f.write(json.dumps(parsed_data, indent=2, ensure_ascii=False))
60
+ f.write("\n\n")
61
+
62
+ # Debug chunks content vs summary
63
+ f.write("CHUNKS CONTENT vs SUMMARY ANALYSIS:\n")
64
+ f.write("-" * 40 + "\n")
65
+ for i, chunk in enumerate(parsed_data.get('chunks', [])[:3]): # Chỉ log 3 chunks đầu
66
+ f.write(f"CHUNK {i+1} ({chunk.get('id', 'no-id')}):\n")
67
+ f.write(f"Title: {chunk.get('title', 'no-title')}\n")
68
+ f.write(f"Summary Length: {len(chunk.get('summary', ''))}\n")
69
+ f.write(f"Content Length: {len(chunk.get('content', ''))}\n")
70
+ f.write(f"Summary: {chunk.get('summary', 'no-summary')[:200]}...\n")
71
+ f.write(f"Content: {chunk.get('content', 'no-content')[:200]}...\n")
72
+ f.write("\n")
73
+
74
+ # Debug tables
75
+ if parsed_data.get('tables'):
76
+ f.write("TABLES ANALYSIS:\n")
77
+ f.write("-" * 40 + "\n")
78
+ for i, table in enumerate(parsed_data.get('tables', [])[:2]):
79
+ f.write(f"TABLE {i+1} ({table.get('id', 'no-id')}):\n")
80
+ f.write(f"Title: {table.get('title', 'no-title')}\n")
81
+ f.write(f"Summary: {table.get('summary', 'no-summary')[:100]}...\n")
82
+ f.write(f"Content: {table.get('content', 'no-content')[:100]}...\n")
83
+ f.write("\n")
84
+
85
+ logger.info(f"Saved Gemini response debug log to: {log_path}")
86
+ return log_path
87
+
88
+ except Exception as e:
89
+ logger.error(f"Error saving debug log: {e}")
90
+ return None
91
+
92
+ def require_admin(f):
93
+ """Decorator để kiểm tra quyền admin"""
94
+ @wraps(f)
95
+ @jwt_required()
96
+ def decorated_function(*args, **kwargs):
97
+ try:
98
+ user_id = get_jwt_identity()
99
+ user = User.find_by_id(user_id)
100
+
101
+ if not user or not user.is_admin():
102
+ return jsonify({
103
+ "success": False,
104
+ "error": "Không có quyền truy cập admin"
105
+ }), 403
106
+
107
+ request.current_user = user
108
+ return f(*args, **kwargs)
109
+ except Exception as e:
110
+ logger.error(f"Lỗi xác thực admin: {e}")
111
+ return jsonify({
112
+ "success": False,
113
+ "error": "Lỗi xác thực"
114
+ }), 500
115
+ return decorated_function
116
+
117
+ # ===== DASHBOARD ROUTES =====
118
+
119
+ @admin_routes.route('/stats/overview', methods=['GET'])
120
+ @require_admin
121
+ def get_overview_stats():
122
+ """Lấy thống kê tổng quan cho dashboard"""
123
+ try:
124
+ db = get_db()
125
+
126
+ # Sử dụng timezone Việt Nam (UTC+7)
127
+ import pytz
128
+ vietnam_tz = pytz.timezone('Asia/Ho_Chi_Minh')
129
+ now_vietnam = datetime.datetime.now(vietnam_tz)
130
+
131
+ # Thống kê users thực tế
132
+ total_users = db.users.count_documents({}) if hasattr(db, 'users') else 0
133
+ today_start = now_vietnam.replace(hour=0, minute=0, second=0, microsecond=0)
134
+ new_users_today = db.users.count_documents({
135
+ "created_at": {"$gte": today_start.astimezone(pytz.UTC).replace(tzinfo=None)}
136
+ }) if hasattr(db, 'users') else 0
137
+
138
+ # Thống kê conversations thực tế
139
+ total_conversations = db.conversations.count_documents({})
140
+ day_ago = now_vietnam - datetime.timedelta(days=1)
141
+ recent_conversations = db.conversations.count_documents({
142
+ "updated_at": {"$gte": day_ago.astimezone(pytz.UTC).replace(tzinfo=None)}
143
+ })
144
+
145
+ # Thống kê tin nhắn thực tế
146
+ pipeline = [
147
+ {"$project": {"message_count": {"$size": "$messages"}}},
148
+ {"$group": {"_id": None, "total_messages": {"$sum": "$message_count"}}}
149
+ ]
150
+ message_result = list(db.conversations.aggregate(pipeline))
151
+ total_messages = message_result[0]["total_messages"] if message_result else 0
152
+
153
+ # Thống kê admin thực tế
154
+ total_admins = db.users.count_documents({"role": "admin"}) if hasattr(db, 'users') else 0
155
+
156
+ # Thống kê theo tuần (7 ngày gần nhất) - SỬA LẠI
157
+ daily_stats = []
158
+ for i in range(7):
159
+ # Tính ngày theo timezone Việt Nam
160
+ target_date = now_vietnam.date() - datetime.timedelta(days=6-i)
161
+ day_start = vietnam_tz.localize(datetime.datetime.combine(target_date, datetime.time.min))
162
+ day_end = vietnam_tz.localize(datetime.datetime.combine(target_date, datetime.time.max))
163
+
164
+ # Convert về UTC để query MongoDB
165
+ day_start_utc = day_start.astimezone(pytz.UTC).replace(tzinfo=None)
166
+ day_end_utc = day_end.astimezone(pytz.UTC).replace(tzinfo=None)
167
+
168
+ # Đếm conversations được tạo HOẶC cập nhật trong ngày
169
+ daily_conversations_created = db.conversations.count_documents({
170
+ "created_at": {"$gte": day_start_utc, "$lte": day_end_utc}
171
+ })
172
+
173
+ daily_conversations_updated = db.conversations.count_documents({
174
+ "updated_at": {"$gte": day_start_utc, "$lte": day_end_utc},
175
+ "created_at": {"$lt": day_start_utc} # Không đếm trùng với conversations mới tạo
176
+ })
177
+
178
+ total_daily_activity = daily_conversations_created + daily_conversations_updated
179
+
180
+ daily_users = 0
181
+ if hasattr(db, 'users'):
182
+ daily_users = db.users.count_documents({
183
+ "created_at": {"$gte": day_start_utc, "$lte": day_end_utc}
184
+ })
185
+
186
+ daily_stats.append({
187
+ "date": target_date.strftime("%Y-%m-%d"),
188
+ "label": target_date.strftime("%d/%m"),
189
+ "conversations": total_daily_activity,
190
+ "users": daily_users,
191
+ "created": daily_conversations_created,
192
+ "updated": daily_conversations_updated
193
+ })
194
+
195
+ # Thống kê theo độ tuổi thực tế
196
+ age_pipeline = [
197
+ {"$group": {"_id": "$age_context", "count": {"$sum": 1}}},
198
+ {"$sort": {"_id": 1}}
199
+ ]
200
+ age_stats = list(db.conversations.aggregate(age_pipeline))
201
+
202
+ return jsonify({
203
+ "success": True,
204
+ "stats": {
205
+ "users": {
206
+ "total": total_users,
207
+ "new_today": new_users_today,
208
+ "active": total_users
209
+ },
210
+ "conversations": {
211
+ "total": total_conversations,
212
+ "recent": recent_conversations
213
+ },
214
+ "messages": {
215
+ "total": total_messages,
216
+ "avg_per_conversation": round(total_messages / total_conversations, 1) if total_conversations > 0 else 0
217
+ },
218
+ "admins": {
219
+ "total": total_admins
220
+ },
221
+ "daily_stats": daily_stats,
222
+ "age_distribution": [
223
+ {
224
+ "age_group": f"{stat['_id']} tuổi" if stat['_id'] else "Không rõ",
225
+ "count": stat["count"]
226
+ }
227
+ for stat in age_stats
228
+ ],
229
+ "timezone": "Asia/Ho_Chi_Minh",
230
+ "current_time": now_vietnam.isoformat()
231
+ }
232
+ })
233
+
234
+ except Exception as e:
235
+ logger.error(f"Lỗi lấy thống kê tổng quan: {str(e)}")
236
+ return jsonify({
237
+ "success": False,
238
+ "error": str(e)
239
+ }), 500
240
+
241
+ @admin_routes.route('/system-info', methods=['GET'])
242
+ @require_admin
243
+ def get_system_info():
244
+ """Lấy thông tin hệ thống"""
245
+ try:
246
+ from core.embedding_model import get_embedding_model
247
+
248
+ # Thông tin vector database
249
+ try:
250
+ embedding_model = get_embedding_model()
251
+ vector_count = embedding_model.count()
252
+ except:
253
+ vector_count = 0
254
+
255
+ # Thông tin database
256
+ db = get_db()
257
+ collections = db.list_collection_names()
258
+
259
+ system_info = {
260
+ "database": {
261
+ "type": "MongoDB",
262
+ "collections": len(collections),
263
+ "status": "active"
264
+ },
265
+ "vector_db": {
266
+ "type": "ChromaDB",
267
+ "embeddings": vector_count,
268
+ "model": "multilingual-e5-base"
269
+ },
270
+ "ai": {
271
+ "generation_model": "Gemini 2.0 Flash",
272
+ "embedding_model": "multilingual-e5-base",
273
+ "status": "active"
274
+ }
275
+ }
276
+
277
+ return jsonify({
278
+ "success": True,
279
+ "system_info": system_info
280
+ })
281
+
282
+ except Exception as e:
283
+ logger.error(f"Lỗi lấy thông tin hệ thống: {str(e)}")
284
+ return jsonify({
285
+ "success": False,
286
+ "error": str(e)
287
+ }), 500
288
+
289
+ @admin_routes.route('/alerts', methods=['GET'])
290
+ @require_admin
291
+ def get_system_alerts():
292
+ """Lấy cảnh báo hệ thống"""
293
+ try:
294
+ alerts = []
295
+
296
+ # Kiểm tra số lượng conversations
297
+ db = get_db()
298
+ total_conversations = db.conversations.count_documents({})
299
+
300
+ if total_conversations > 100:
301
+ alerts.append({
302
+ "type": "info",
303
+ "title": "Lượng dữ liệu cao",
304
+ "message": f"Hệ thống có {total_conversations} cuộc hội thoại",
305
+ "severity": "low"
306
+ })
307
+
308
+ # Mặc định: hệ thống hoạt động bình thường
309
+ if not alerts:
310
+ alerts.append({
311
+ "type": "info",
312
+ "title": "Hệ thống hoạt động bình thường",
313
+ "message": "Tất cả dịch vụ đang chạy ổn định",
314
+ "severity": "low"
315
+ })
316
+
317
+ return jsonify({
318
+ "success": True,
319
+ "alerts": alerts
320
+ })
321
+
322
+ except Exception as e:
323
+ logger.error(f"Lỗi lấy cảnh báo hệ thống: {str(e)}")
324
+ return jsonify({
325
+ "success": False,
326
+ "error": str(e)
327
+ }), 500
328
+
329
+ # ===== USER MANAGEMENT ROUTES =====
330
+
331
+ @admin_routes.route('/users', methods=['GET'])
332
+ @require_admin
333
+ def get_all_users():
334
+ """Lấy danh sách người dùng với thông tin thực tế"""
335
+ try:
336
+ db = get_db()
337
+ users_collection = db.users
338
+
339
+ page = int(request.args.get('page', 1))
340
+ per_page = int(request.args.get('per_page', 20))
341
+ search = request.args.get('search', '')
342
+ gender_filter = request.args.get('gender', '')
343
+ role_filter = request.args.get('role', '')
344
+ sort_by = request.args.get('sort_by', 'created_at')
345
+ sort_order = request.args.get('sort_order', 'desc')
346
+
347
+ # Tạo query filter
348
+ query_filter = {}
349
+ if search:
350
+ query_filter["$or"] = [
351
+ {"name": {"$regex": search, "$options": "i"}},
352
+ {"email": {"$regex": search, "$options": "i"}}
353
+ ]
354
+ if gender_filter:
355
+ query_filter["gender"] = gender_filter
356
+ if role_filter:
357
+ query_filter["role"] = role_filter
358
+
359
+ # Pagination
360
+ skip = (page - 1) * per_page
361
+ sort_direction = -1 if sort_order == 'desc' else 1
362
+
363
+ users_cursor = users_collection.find(query_filter).sort(sort_by, sort_direction).skip(skip).limit(per_page)
364
+ total_users = users_collection.count_documents(query_filter)
365
+
366
+ users_list = []
367
+ for user_data in users_cursor:
368
+ # Đếm conversations thực tế
369
+ conversation_count = db.conversations.count_documents({"user_id": user_data["_id"]})
370
+
371
+ # Lấy conversation mới nhất để biết last_activity
372
+ latest_conversation = db.conversations.find_one(
373
+ {"user_id": user_data["_id"]},
374
+ sort=[("updated_at", -1)]
375
+ )
376
+
377
+ # Đếm tin nhắn
378
+ user_conversations = list(db.conversations.find({"user_id": user_data["_id"]}))
379
+ total_messages = sum(len(conv.get("messages", [])) for conv in user_conversations)
380
+
381
+ users_list.append({
382
+ "id": str(user_data["_id"]),
383
+ "name": user_data.get("name", ""),
384
+ "email": user_data.get("email", ""),
385
+ "gender": user_data.get("gender", ""),
386
+ "role": user_data.get("role", "user"),
387
+ "created_at": user_data.get("created_at").isoformat() if user_data.get("created_at") else None,
388
+ "updated_at": user_data.get("updated_at").isoformat() if user_data.get("updated_at") else None,
389
+ "last_login": user_data.get("last_login").isoformat() if user_data.get("last_login") else None,
390
+ "conversation_count": conversation_count,
391
+ "message_count": total_messages,
392
+ "last_activity": latest_conversation.get("updated_at").isoformat() if latest_conversation and latest_conversation.get("updated_at") else None,
393
+ "avg_messages_per_conversation": round(total_messages / conversation_count, 1) if conversation_count > 0 else 0
394
+ })
395
+
396
+ # Thống kê tổng hợp
397
+ stats = {
398
+ "total_users": total_users,
399
+ "total_admins": users_collection.count_documents({"role": "admin"}),
400
+ "total_regular_users": users_collection.count_documents({"role": "user"}),
401
+ "active_users": users_collection.count_documents({"last_login": {"$exists": True}}),
402
+ "gender_stats": {
403
+ "male": users_collection.count_documents({"gender": "male"}),
404
+ "female": users_collection.count_documents({"gender": "female"}),
405
+ "other": users_collection.count_documents({"gender": "other"}),
406
+ "unknown": users_collection.count_documents({"gender": {"$in": [None, ""]}})
407
+ }
408
+ }
409
+
410
+ return jsonify({
411
+ "success": True,
412
+ "users": users_list,
413
+ "stats": stats,
414
+ "pagination": {
415
+ "page": page,
416
+ "per_page": per_page,
417
+ "total": total_users,
418
+ "pages": (total_users + per_page - 1) // per_page
419
+ }
420
+ })
421
+
422
+ except Exception as e:
423
+ logger.error(f"Lỗi lấy danh sách users: {str(e)}")
424
+ return jsonify({
425
+ "success": False,
426
+ "error": str(e)
427
+ }), 500
428
+
429
+ @admin_routes.route('/users/<user_id>', methods=['GET'])
430
+ @require_admin
431
+ def get_user_detail(user_id):
432
+ """Lấy chi tiết người dùng với thông tin đầy đủ"""
433
+ try:
434
+ user = User.find_by_id(user_id)
435
+ if not user:
436
+ return jsonify({
437
+ "success": False,
438
+ "error": "Không tìm thấy người dùng"
439
+ }), 404
440
+
441
+ db = get_db()
442
+
443
+ # Lấy conversations của user
444
+ user_conversations = list(db.conversations.find(
445
+ {"user_id": ObjectId(user_id)},
446
+ {"title": 1, "created_at": 1, "updated_at": 1, "age_context": 1, "messages": 1}
447
+ ).sort("updated_at", -1))
448
+
449
+ # Tính thống kê chi tiết
450
+ total_messages = sum(len(conv.get("messages", [])) for conv in user_conversations)
451
+ conversation_stats = {
452
+ "total_conversations": len(user_conversations),
453
+ "total_messages": total_messages,
454
+ "avg_messages_per_conversation": round(total_messages / len(user_conversations), 1) if user_conversations else 0,
455
+ "most_recent_conversation": user_conversations[0].get("updated_at").isoformat() if user_conversations and user_conversations[0].get("updated_at") else None,
456
+ "oldest_conversation": user_conversations[-1].get("created_at").isoformat() if user_conversations and user_conversations[-1].get("created_at") else None
457
+ }
458
+
459
+ # Thống kê theo độ tuổi
460
+ age_stats = {}
461
+ for conv in user_conversations:
462
+ age = conv.get("age_context")
463
+ if age:
464
+ age_stats[age] = age_stats.get(age, 0) + 1
465
+
466
+ user_detail = {
467
+ "id": str(user.user_id),
468
+ "name": user.name,
469
+ "email": user.email,
470
+ "gender": user.gender,
471
+ "role": user.role,
472
+ "created_at": user.created_at.isoformat() if user.created_at else None,
473
+ "updated_at": user.updated_at.isoformat() if user.updated_at else None,
474
+ "last_login": user.last_login.isoformat() if user.last_login else None,
475
+ "stats": conversation_stats,
476
+ "age_usage": age_stats,
477
+ "recent_conversations": [
478
+ {
479
+ "id": str(conv["_id"]),
480
+ "title": conv.get("title", ""),
481
+ "created_at": conv.get("created_at").isoformat() if conv.get("created_at") else None,
482
+ "updated_at": conv.get("updated_at").isoformat() if conv.get("updated_at") else None,
483
+ "message_count": len(conv.get("messages", [])),
484
+ "age_context": conv.get("age_context")
485
+ }
486
+ for conv in user_conversations[:10]
487
+ ]
488
+ }
489
+
490
+ return jsonify({
491
+ "success": True,
492
+ "user": user_detail
493
+ })
494
+
495
+ except Exception as e:
496
+ logger.error(f"Lỗi lấy chi tiết user: {str(e)}")
497
+ return jsonify({
498
+ "success": False,
499
+ "error": str(e)
500
+ }), 500
501
+
502
+ @admin_routes.route('/users/<user_id>', methods=['DELETE'])
503
+ @require_admin
504
+ def delete_user(user_id):
505
+ """Xóa người dùng"""
506
+ try:
507
+ user = User.find_by_id(user_id)
508
+ if not user:
509
+ return jsonify({
510
+ "success": False,
511
+ "error": "Không tìm thấy người dùng"
512
+ }), 404
513
+
514
+ # Xóa tất cả conversations của user
515
+ db = get_db()
516
+ conversations_deleted = db.conversations.delete_many({"user_id": ObjectId(user_id)})
517
+
518
+ # Xóa user
519
+ user_deleted = user.delete()
520
+
521
+ if user_deleted:
522
+ return jsonify({
523
+ "success": True,
524
+ "message": f"Đã xóa người dùng và {conversations_deleted.deleted_count} cuộc hội thoại"
525
+ })
526
+ else:
527
+ return jsonify({
528
+ "success": False,
529
+ "error": "Không thể xóa người dùng"
530
+ }), 500
531
+
532
+ except Exception as e:
533
+ logger.error(f"Lỗi xóa user: {str(e)}")
534
+ return jsonify({
535
+ "success": False,
536
+ "error": str(e)
537
+ }), 500
538
+
539
+ @admin_routes.route('/users/bulk-delete', methods=['POST'])
540
+ @require_admin
541
+ def bulk_delete_users():
542
+ """Xóa nhiều người dùng"""
543
+ try:
544
+ data = request.json
545
+ user_ids = data.get('user_ids', [])
546
+
547
+ if not user_ids:
548
+ return jsonify({
549
+ "success": False,
550
+ "error": "Không có user nào được chọn"
551
+ }), 400
552
+
553
+ deleted_count = 0
554
+ failed_ids = []
555
+
556
+ db = get_db()
557
+
558
+ for user_id in user_ids:
559
+ try:
560
+ # Xóa conversations của user
561
+ db.conversations.delete_many({"user_id": ObjectId(user_id)})
562
+
563
+ # Xóa user
564
+ user = User.find_by_id(user_id)
565
+ if user and user.delete():
566
+ deleted_count += 1
567
+ else:
568
+ failed_ids.append(user_id)
569
+ except Exception as e:
570
+ logger.error(f"Lỗi xóa user {user_id}: {e}")
571
+ failed_ids.append(user_id)
572
+
573
+ return jsonify({
574
+ "success": True,
575
+ "message": f"Đã xóa {deleted_count}/{len(user_ids)} người dùng",
576
+ "deleted_count": deleted_count,
577
+ "failed_ids": failed_ids
578
+ })
579
+
580
+ except Exception as e:
581
+ logger.error(f"Lỗi xóa bulk users: {str(e)}")
582
+ return jsonify({
583
+ "success": False,
584
+ "error": str(e)
585
+ }), 500
586
+
587
+ # ===== CONVERSATION MANAGEMENT ROUTES =====
588
+ @admin_routes.route('/conversations', methods=['GET'])
589
+ @require_admin
590
+ def get_all_conversations():
591
+ """Lấy danh sách cuộc hội thoại - Fixed ObjectId serialization"""
592
+ try:
593
+ db = get_db()
594
+
595
+ page = int(request.args.get('page', 1))
596
+ per_page = int(request.args.get('per_page', 20))
597
+ search = request.args.get('search', '')
598
+ age_filter = request.args.get('age', '')
599
+ archived_filter = request.args.get('archived', 'all')
600
+
601
+ # Tạo query filter
602
+ query_filter = {}
603
+ if search:
604
+ query_filter["title"] = {"$regex": search, "$options": "i"}
605
+ if age_filter:
606
+ query_filter["age_context"] = int(age_filter)
607
+ if archived_filter != 'all':
608
+ query_filter["is_archived"] = archived_filter == 'true'
609
+
610
+ skip = (page - 1) * per_page
611
+ conversations = list(db.conversations.find(query_filter).skip(skip).limit(per_page).sort("updated_at", -1))
612
+ total_conversations = db.conversations.count_documents(query_filter)
613
+
614
+ conversations_list = []
615
+ for conv in conversations:
616
+ try:
617
+ # ✅ FIX: Safely convert ObjectId to string và handle user lookup
618
+ user_id = conv.get("user_id")
619
+ user_name = "Unknown"
620
+
621
+ if user_id:
622
+ try:
623
+ # Ensure user_id is ObjectId
624
+ if isinstance(user_id, str):
625
+ user_id = ObjectId(user_id)
626
+
627
+ user = User.find_by_id(str(user_id)) # Convert to string for the method
628
+ if user:
629
+ user_name = user.name
630
+ except Exception as user_error:
631
+ logger.warning(f"Could not fetch user {user_id}: {user_error}")
632
+ user_name = "Unknown"
633
+
634
+ # ✅ FIX: Safely handle datetime objects
635
+ def safe_isoformat(dt):
636
+ if dt is None:
637
+ return None
638
+ if hasattr(dt, 'isoformat'):
639
+ return dt.isoformat()
640
+ return str(dt)
641
+
642
+ # ✅ FIX: Safely handle messages array
643
+ messages = conv.get("messages", [])
644
+ processed_messages = []
645
+
646
+ for msg in messages:
647
+ if isinstance(msg, dict):
648
+ processed_msg = {
649
+ "role": msg.get("role", ""),
650
+ "content": msg.get("content", ""),
651
+ "timestamp": safe_isoformat(msg.get("timestamp"))
652
+ }
653
+ processed_messages.append(processed_msg)
654
+
655
+ conversation_data = {
656
+ "id": str(conv["_id"]), # ✅ Always convert ObjectId to string
657
+ "title": conv.get("title", ""),
658
+ "user_name": user_name,
659
+ "age_context": conv.get("age_context"),
660
+ "message_count": len(messages),
661
+ "is_archived": conv.get("is_archived", False),
662
+ "created_at": safe_isoformat(conv.get("created_at")),
663
+ "updated_at": safe_isoformat(conv.get("updated_at")),
664
+ "messages": processed_messages
665
+ }
666
+
667
+ conversations_list.append(conversation_data)
668
+
669
+ except Exception as conv_error:
670
+ logger.error(f"Error processing conversation {conv.get('_id')}: {conv_error}")
671
+ # Skip this conversation but continue with others
672
+ continue
673
+
674
+ return jsonify({
675
+ "success": True,
676
+ "conversations": conversations_list,
677
+ "pagination": {
678
+ "page": page,
679
+ "per_page": per_page,
680
+ "total": total_conversations,
681
+ "pages": (total_conversations + per_page - 1) // per_page
682
+ }
683
+ })
684
+
685
+ except Exception as e:
686
+ logger.error(f"Lỗi lấy danh sách conversations: {str(e)}")
687
+ return jsonify({
688
+ "success": False,
689
+ "error": str(e)
690
+ }), 500
691
+
692
+ @admin_routes.route('/conversations/<conversation_id>', methods=['DELETE'])
693
+ @require_admin
694
+ def delete_conversation(conversation_id):
695
+ """Xóa cuộc hội thoại - Fixed ObjectId handling"""
696
+ try:
697
+ # ✅ FIX: Use Conversation model instead of direct DB access
698
+ conversation = Conversation.find_by_id(conversation_id)
699
+ if not conversation:
700
+ return jsonify({
701
+ "success": False,
702
+ "error": "Không tìm thấy cuộc hội thoại"
703
+ }), 404
704
+
705
+ success = conversation.delete()
706
+
707
+ if success:
708
+ return jsonify({
709
+ "success": True,
710
+ "message": "Đã xóa cuộc hội thoại"
711
+ })
712
+ else:
713
+ return jsonify({
714
+ "success": False,
715
+ "error": "Không thể xóa cuộc hội thoại"
716
+ }), 500
717
+
718
+ except Exception as e:
719
+ logger.error(f"Lỗi xóa conversation: {str(e)}")
720
+ return jsonify({
721
+ "success": False,
722
+ "error": str(e)
723
+ }), 500
724
+
725
+ @admin_routes.route('/conversations/bulk-delete', methods=['POST'])
726
+ @require_admin
727
+ def bulk_delete_conversations():
728
+ """Xóa nhiều cuộc hội thoại - Fixed ObjectId handling"""
729
+ try:
730
+ data = request.json
731
+ conversation_ids = data.get('conversation_ids', [])
732
+
733
+ if not conversation_ids:
734
+ return jsonify({
735
+ "success": False,
736
+ "error": "Không có conversation nào được chọn"
737
+ }), 400
738
+
739
+ db = get_db()
740
+ deleted_count = 0
741
+ failed_ids = []
742
+
743
+ for conv_id in conversation_ids:
744
+ try:
745
+ # ✅ FIX: Ensure proper ObjectId conversion
746
+ if isinstance(conv_id, str):
747
+ conv_id = ObjectId(conv_id)
748
+
749
+ result = db.conversations.delete_one({"_id": conv_id})
750
+ if result.deleted_count > 0:
751
+ deleted_count += 1
752
+ else:
753
+ failed_ids.append(str(conv_id))
754
+
755
+ except Exception as e:
756
+ logger.error(f"Error deleting conversation {conv_id}: {e}")
757
+ failed_ids.append(str(conv_id))
758
+ continue
759
+
760
+ return jsonify({
761
+ "success": True,
762
+ "message": f"Đã xóa {deleted_count}/{len(conversation_ids)} cuộc hội thoại",
763
+ "deleted_count": deleted_count,
764
+ "failed_ids": failed_ids
765
+ })
766
+
767
+ except Exception as e:
768
+ logger.error(f"Lỗi xóa bulk conversations: {str(e)}")
769
+ return jsonify({
770
+ "success": False,
771
+ "error": str(e)
772
+ }), 500
773
+ # ===== DOCUMENT MANAGEMENT ROUTES =====
774
+
775
+ @admin_routes.route('/documents', methods=['GET'])
776
+ @require_admin
777
+ def get_all_documents():
778
+ """Lấy danh sách tài liệu t�� ChromaDB theo đúng metadata"""
779
+ try:
780
+ embedding_model = get_embedding_model()
781
+
782
+ # Lấy tất cả documents từ ChromaDB
783
+ results = embedding_model.collection.get(
784
+ include=['metadatas', 'documents']
785
+ )
786
+
787
+ if not results or not results['metadatas']:
788
+ return jsonify({
789
+ "success": True,
790
+ "documents": [],
791
+ "stats": {
792
+ "total": 0,
793
+ "by_chapter": {},
794
+ "by_type": {}
795
+ }
796
+ })
797
+
798
+ # Phân tích metadata để nhóm theo chapter
799
+ documents_by_chapter = {}
800
+ stats = {
801
+ "total": 0,
802
+ "by_chapter": {},
803
+ "by_type": {}
804
+ }
805
+
806
+ for i, metadata in enumerate(results['metadatas']):
807
+ # Lấy chapter từ metadata
808
+ chapter = metadata.get('chapter', 'unknown')
809
+ content_type = metadata.get('content_type', 'text')
810
+
811
+ # Cập nhật stats
812
+ stats["by_chapter"][chapter] = stats["by_chapter"].get(chapter, 0) + 1
813
+ stats["by_type"][content_type] = stats["by_type"].get(content_type, 0) + 1
814
+
815
+ # Nhóm theo chapter
816
+ if chapter not in documents_by_chapter:
817
+ chapter_title = get_chapter_title(chapter)
818
+ chapter_type = get_chapter_type(chapter)
819
+
820
+ documents_by_chapter[chapter] = {
821
+ "id": chapter,
822
+ "title": chapter_title,
823
+ "description": f"Tài liệu {chapter_title.lower()}",
824
+ "type": chapter_type,
825
+ "status": "processed",
826
+ "created_at": metadata.get('created_at', datetime.datetime.now().isoformat()),
827
+ "content_stats": {
828
+ "chunks": 0,
829
+ "tables": 0,
830
+ "figures": 0
831
+ }
832
+ }
833
+
834
+ # Cập nhật content stats
835
+ if content_type == "table":
836
+ documents_by_chapter[chapter]["content_stats"]["tables"] += 1
837
+ elif content_type == "figure":
838
+ documents_by_chapter[chapter]["content_stats"]["figures"] += 1
839
+ else:
840
+ documents_by_chapter[chapter]["content_stats"]["chunks"] += 1
841
+
842
+ documents_list = list(documents_by_chapter.values())
843
+ stats["total"] = len(documents_list)
844
+
845
+ logger.info(f"Found chapters: {list(documents_by_chapter.keys())}")
846
+ logger.info(f"Stats: {stats}")
847
+
848
+ return jsonify({
849
+ "success": True,
850
+ "documents": documents_list,
851
+ "stats": stats
852
+ })
853
+
854
+ except Exception as e:
855
+ logger.error(f"Lỗi lấy danh sách documents: {str(e)}")
856
+ return jsonify({
857
+ "success": False,
858
+ "error": str(e)
859
+ }), 500
860
+
861
+ def get_chapter_title(chapter):
862
+ try:
863
+ # Nếu là document upload, lấy title từ metadata
864
+ if chapter.startswith('bosung') or chapter.startswith('upload_'):
865
+ embedding_model = get_embedding_model()
866
+ results = embedding_model.collection.get(
867
+ where={"chapter": chapter},
868
+ limit=1
869
+ )
870
+
871
+ if results and results.get('metadatas') and results['metadatas'][0]:
872
+ metadata = results['metadatas'][0]
873
+ document_title = metadata.get('document_title') or metadata.get('document_source')
874
+ if document_title and document_title != 'Tài liệu upload':
875
+ return document_title
876
+
877
+ # Fallback cho các chapter chuẩn
878
+ chapter_titles = {
879
+ 'bai1': 'Bài 1: Dinh dưỡng theo lứa tuổi học sinh',
880
+ 'bai2': 'Bài 2: An toàn thực phẩm',
881
+ 'bai3': 'Bài 3: Vệ sinh dinh dưỡng',
882
+ 'bai4': 'Bài 4: Giáo dục dinh dưỡng',
883
+ 'phuluc': 'Phụ lục'
884
+ }
885
+
886
+ # Kiểm tra pattern bosung
887
+ if chapter.startswith('bosung'):
888
+ return f'Tài liệu bổ sung {chapter.replace("bosung", "")}'
889
+
890
+ return chapter_titles.get(chapter, f'Tài liệu {chapter}')
891
+
892
+ except Exception as e:
893
+ logger.error(f"Error getting chapter title: {e}")
894
+ return f'Tài liệu {chapter}'
895
+
896
+ def get_chapter_type(chapter):
897
+ """Lấy loại chapter"""
898
+ if chapter.startswith('bai'):
899
+ return 'lesson'
900
+ elif chapter == 'phuluc':
901
+ return 'appendix'
902
+ else:
903
+ return 'uploaded'
904
+
905
+ @admin_routes.route('/documents/<doc_id>', methods=['GET'])
906
+ @require_admin
907
+ def get_document_detail(doc_id):
908
+ """Lấy chi tiết document theo chapter"""
909
+ try:
910
+ logger.info(f"Getting document detail for: {doc_id}")
911
+
912
+ # Sử dụng try-catch để tránh l���i tensor
913
+ try:
914
+ embedding_model = get_embedding_model()
915
+ except Exception as model_error:
916
+ logger.warning(f"Cannot load embedding model: {model_error}")
917
+ # Fallback: truy cập trực tiếp ChromaDB
918
+ import chromadb
919
+ from config import CHROMA_PERSIST_DIRECTORY, COLLECTION_NAME
920
+
921
+ chroma_client = chromadb.PersistentClient(path=CHROMA_PERSIST_DIRECTORY)
922
+ collection = chroma_client.get_collection(name=COLLECTION_NAME)
923
+ else:
924
+ collection = embedding_model.collection
925
+
926
+ # Lấy tất cả documents và filter manually để tránh lỗi query
927
+ try:
928
+ all_results = collection.get(
929
+ include=['metadatas', 'documents'] # Bỏ 'ids' để tránh lỗi
930
+ )
931
+
932
+ if not all_results or not all_results.get('metadatas'):
933
+ logger.warning("No documents found in collection")
934
+ return jsonify({
935
+ "success": False,
936
+ "error": "Không có dữ liệu trong collection"
937
+ }), 404
938
+
939
+ # Filter manually
940
+ filtered_documents = []
941
+ filtered_metadatas = []
942
+
943
+ for i, metadata in enumerate(all_results['metadatas']):
944
+ if not metadata:
945
+ continue
946
+
947
+ chunk_id = metadata.get('chunk_id', '')
948
+ chapter = metadata.get('chapter', '')
949
+
950
+ # Kiểm tra match với doc_id
951
+ if (chunk_id.startswith(f"{doc_id}_") or
952
+ chapter == doc_id or
953
+ (doc_id in ['bai1', 'bai2', 'bai3', 'bai4'] and chunk_id.startswith(f"{doc_id}_")) or
954
+ (doc_id == 'phuluc' and 'phuluc' in chunk_id.lower())):
955
+
956
+ filtered_documents.append(all_results['documents'][i])
957
+ filtered_metadatas.append(metadata)
958
+
959
+ logger.info(f"Found {len(filtered_documents)} matching documents for {doc_id}")
960
+
961
+ if not filtered_documents:
962
+ return jsonify({
963
+ "success": False,
964
+ "error": f"Không tìm thấy tài liệu cho {doc_id}"
965
+ }), 404
966
+
967
+ except Exception as query_error:
968
+ logger.error(f"ChromaDB query error: {query_error}")
969
+ return jsonify({
970
+ "success": False,
971
+ "error": f"Lỗi truy vấn cơ sở dữ liệu: {str(query_error)}"
972
+ }), 500
973
+
974
+ # Xử lý kết quả và nhóm theo content_type
975
+ chunks_by_type = {
976
+ 'text': [],
977
+ 'table': [],
978
+ 'figure': []
979
+ }
980
+
981
+ for i, metadata in enumerate(filtered_metadatas):
982
+ content_type = metadata.get('content_type', 'text')
983
+
984
+ # Parse age_range
985
+ age_range_str = metadata.get('age_range', '1-19')
986
+ try:
987
+ if '-' in age_range_str:
988
+ age_min, age_max = map(int, age_range_str.split('-'))
989
+ else:
990
+ age_min = age_max = int(age_range_str)
991
+ except:
992
+ age_min, age_max = 1, 19
993
+
994
+ chunk = {
995
+ "id": metadata.get('chunk_id', f'chunk_{i}'),
996
+ "title": metadata.get('title', 'Không có tiêu đề'),
997
+ "content": filtered_documents[i],
998
+ "content_type": content_type,
999
+ "age_range": age_range_str,
1000
+ "age_min": age_min,
1001
+ "age_max": age_max,
1002
+ "summary": metadata.get('summary', 'Không có tóm tắt'),
1003
+ "pages": metadata.get('pages', ''),
1004
+ "word_count": metadata.get('word_count', 0),
1005
+ "token_count": metadata.get('token_count', 0),
1006
+ "related_chunks": metadata.get('related_chunks', '').split(',') if metadata.get('related_chunks') else [],
1007
+ "created_at": metadata.get('created_at', ''),
1008
+ "document_source": metadata.get('document_source', ''),
1009
+ # Metadata đặc biệt
1010
+ "contains_table": metadata.get('contains_table', False),
1011
+ "contains_figure": metadata.get('contains_figure', False),
1012
+ "table_columns": metadata.get('table_columns', '').split(',') if metadata.get('table_columns') else []
1013
+ }
1014
+
1015
+ # Phân loại vào đúng nhóm
1016
+ if content_type in chunks_by_type:
1017
+ chunks_by_type[content_type].append(chunk)
1018
+ else:
1019
+ chunks_by_type['text'].append(chunk)
1020
+
1021
+ # Tính thống kê
1022
+ total_chunks = sum(len(chunks) for chunks in chunks_by_type.values())
1023
+
1024
+ logger.info(f"Successfully processed {total_chunks} chunks for {doc_id}")
1025
+
1026
+ return jsonify({
1027
+ "success": True,
1028
+ "document": {
1029
+ "id": doc_id,
1030
+ "chunks": chunks_by_type,
1031
+ "stats": {
1032
+ "total_chunks": total_chunks,
1033
+ "text_chunks": len(chunks_by_type['text']),
1034
+ "table_chunks": len(chunks_by_type['table']),
1035
+ "figure_chunks": len(chunks_by_type['figure'])
1036
+ }
1037
+ }
1038
+ })
1039
+
1040
+ except Exception as e:
1041
+ logger.error(f"Error getting document detail: {str(e)}")
1042
+ return jsonify({
1043
+ "success": False,
1044
+ "error": f"Lỗi máy chủ: {str(e)}"
1045
+ }), 500
1046
+
1047
+ @admin_routes.route('/documents/debug/metadata', methods=['GET'])
1048
+ @require_admin
1049
+ def debug_metadata():
1050
+ """Debug metadata trong ChromaDB"""
1051
+ try:
1052
+ # Sử dụng try-catch để tránh lỗi tensor
1053
+ try:
1054
+ embedding_model = get_embedding_model()
1055
+ total_docs = embedding_model.count()
1056
+ except Exception as model_error:
1057
+ logger.warning(f"Không thể load embedding model: {model_error}")
1058
+ # Fallback: truy cập trực tiếp ChromaDB
1059
+ import chromadb
1060
+ from config import CHROMA_PERSIST_DIRECTORY, COLLECTION_NAME
1061
+
1062
+ chroma_client = chromadb.PersistentClient(path=CHROMA_PERSIST_DIRECTORY)
1063
+ collection = chroma_client.get_collection(name=COLLECTION_NAME)
1064
+ total_docs = collection.count()
1065
+
1066
+ # Lấy sample metadata
1067
+ results = collection.get(
1068
+ limit=10,
1069
+ include=['metadatas']
1070
+ )
1071
+ else:
1072
+ # Lấy 10 documents đầu tiên để debug
1073
+ results = embedding_model.collection.get(
1074
+ limit=10,
1075
+ include=['metadatas']
1076
+ )
1077
+
1078
+ debug_info = {
1079
+ "total_documents": total_docs,
1080
+ "sample_metadata": results['metadatas'][:5] if results and results.get('metadatas') else [],
1081
+ "all_metadata_keys": []
1082
+ }
1083
+
1084
+ if results and results.get('metadatas'):
1085
+ # Lấy tất cả keys từ metadata
1086
+ all_keys = set()
1087
+ for metadata in results['metadatas']:
1088
+ if metadata: # Kiểm tra metadata không None
1089
+ all_keys.update(metadata.keys())
1090
+ debug_info["all_metadata_keys"] = list(all_keys)
1091
+
1092
+ return jsonify({
1093
+ "success": True,
1094
+ "debug_info": debug_info
1095
+ })
1096
+
1097
+ except Exception as e:
1098
+ logger.error(f"Lỗi debug metadata: {str(e)}")
1099
+ return jsonify({
1100
+ "success": False,
1101
+ "error": str(e)
1102
+ }), 500
1103
+
1104
+ @admin_routes.route('/documents/upload', methods=['POST'])
1105
+ @require_admin
1106
+ def upload_document():
1107
+ """Upload PDF document"""
1108
+ try:
1109
+ if 'file' not in request.files:
1110
+ return jsonify({
1111
+ "success": False,
1112
+ "error": "Không có file được upload"
1113
+ }), 400
1114
+
1115
+ file = request.files['file']
1116
+ if file.filename == '':
1117
+ return jsonify({
1118
+ "success": False,
1119
+ "error": "Không có file được chọn"
1120
+ }), 400
1121
+
1122
+ if not file.filename.lower().endswith('.pdf'):
1123
+ return jsonify({
1124
+ "success": False,
1125
+ "error": "Chỉ chấp nhận file PDF"
1126
+ }), 400
1127
+
1128
+ # Tạo thư mục temp nếu chưa có
1129
+ temp_dir = '/tmp'
1130
+ if not os.path.exists(temp_dir):
1131
+ os.makedirs(temp_dir)
1132
+
1133
+ # Lưu file tạm thời
1134
+ filename = secure_filename(file.filename)
1135
+ temp_path = os.path.join(temp_dir, f"{int(time.time())}_{filename}")
1136
+ file.save(temp_path)
1137
+
1138
+ # Metadata từ form
1139
+ title = request.form.get('title', filename.replace('.pdf', ''))
1140
+ description = request.form.get('description', '')
1141
+ author = request.form.get('author', '')
1142
+
1143
+ # Tạo document_id duy nhất dựa trên số lượng tài liệu bổ sung hiện có
1144
+ document_id = generate_unique_document_id(title)
1145
+
1146
+ logger.info(f"Uploaded file: {filename} -> {temp_path}")
1147
+
1148
+ return jsonify({
1149
+ "success": True,
1150
+ "message": "File đã được upload thành công",
1151
+ "document_id": document_id,
1152
+ "filename": filename,
1153
+ "temp_path": temp_path,
1154
+ "metadata": {
1155
+ "title": title,
1156
+ "description": description,
1157
+ "author": author
1158
+ }
1159
+ })
1160
+
1161
+ except Exception as e:
1162
+ logger.error(f"Lỗi upload document: {str(e)}")
1163
+ return jsonify({
1164
+ "success": False,
1165
+ "error": f"Lỗi upload: {str(e)}"
1166
+ }), 500
1167
+
1168
+ def generate_unique_document_id(title):
1169
+ """Tạo document ID duy nhất dựa trên title và số thứ tự"""
1170
+ try:
1171
+ embedding_model = get_embedding_model()
1172
+
1173
+ # Đếm số tài liệu bổ sung hiện có
1174
+ results = embedding_model.collection.get(
1175
+ where={"chapter": {"$like": "bosung%"}}
1176
+ )
1177
+
1178
+ if results and results.get('metadatas'):
1179
+ # Lấy tất cả chapter IDs bắt đầu bằng "bosung"
1180
+ existing_chapters = set()
1181
+ for metadata in results['metadatas']:
1182
+ if metadata and metadata.get('chapter'):
1183
+ chapter = metadata['chapter']
1184
+ if chapter.startswith('bosung'):
1185
+ existing_chapters.add(chapter)
1186
+
1187
+ # Tìm số thứ tự cao nhất
1188
+ max_num = 0
1189
+ for chapter in existing_chapters:
1190
+ try:
1191
+ num = int(chapter.replace('bosung', ''))
1192
+ max_num = max(max_num, num)
1193
+ except:
1194
+ continue
1195
+
1196
+ next_num = max_num + 1
1197
+ else:
1198
+ next_num = 1
1199
+
1200
+ # Tạo ID mới
1201
+ document_id = f"bosung{next_num}"
1202
+ logger.info(f"Generated unique document ID: {document_id}")
1203
+
1204
+ return document_id
1205
+
1206
+ except Exception as e:
1207
+ logger.error(f"Error generating document ID: {e}")
1208
+ # Fallback: sử dụng timestamp
1209
+ return f"bosung_{int(time.time())}"
1210
+
1211
+ @admin_routes.route('/documents/<doc_id>/process', methods=['POST'])
1212
+ @require_admin
1213
+ def process_document(doc_id):
1214
+ """Process document với Gemini - THÊM DEBUG LOGGING"""
1215
+ try:
1216
+ data = request.json
1217
+ temp_path = data.get('temp_path')
1218
+
1219
+ if not temp_path or not os.path.exists(temp_path):
1220
+ return jsonify({
1221
+ "success": False,
1222
+ "error": "File không tồn tại"
1223
+ }), 400
1224
+
1225
+ logger.info(f"Processing document {doc_id} from {temp_path}")
1226
+
1227
+ # Kiểm tra và import PyPDF2
1228
+ try:
1229
+ import PyPDF2
1230
+ except ImportError:
1231
+ logger.error("PyPDF2 không được cài đặt")
1232
+ return jsonify({
1233
+ "success": False,
1234
+ "error": "Thiếu thư viện PyPDF2. Vui lòng cài đặt: pip install PyPDF2"
1235
+ }), 500
1236
+
1237
+ # Đọc PDF content
1238
+ try:
1239
+ with open(temp_path, 'rb') as file:
1240
+ pdf_reader = PyPDF2.PdfReader(file)
1241
+ pdf_text = ""
1242
+ for page_num in range(len(pdf_reader.pages)):
1243
+ page = pdf_reader.pages[page_num]
1244
+ page_text = page.extract_text()
1245
+ if page_text:
1246
+ pdf_text += page_text + "\n"
1247
+
1248
+ logger.info(f"Extracted {len(pdf_text)} characters from PDF")
1249
+
1250
+ # DEBUG: Log phần đầu của PDF text
1251
+ logger.info(f"PDF text preview (first 500 chars): {pdf_text[:500]}...")
1252
+
1253
+ except Exception as pdf_error:
1254
+ logger.error(f"Lỗi đọc PDF: {pdf_error}")
1255
+ return jsonify({
1256
+ "success": False,
1257
+ "error": f"Không thể đọc file PDF: {str(pdf_error)}"
1258
+ }), 400
1259
+
1260
+ if not pdf_text.strip():
1261
+ return jsonify({
1262
+ "success": False,
1263
+ "error": "Không thể trích xuất text từ PDF. Vui lòng đảm bảo PDF có thể copy được chữ (không phải file scan)"
1264
+ }), 400
1265
+
1266
+ # Tạo prompt cho Gemini
1267
+ try:
1268
+ prompt = create_document_processing_prompt(pdf_text)
1269
+ logger.info("Created prompt for Gemini")
1270
+
1271
+ # DEBUG: Log độ dài prompt
1272
+ logger.info(f"Prompt length: {len(prompt)} characters")
1273
+
1274
+ except Exception as prompt_error:
1275
+ logger.error(f"Lỗi tạo prompt: {prompt_error}")
1276
+ return jsonify({
1277
+ "success": False,
1278
+ "error": f"Lỗi tạo prompt: {str(prompt_error)}"
1279
+ }), 500
1280
+
1281
+ # Gọi Gemini API
1282
+ try:
1283
+ model = genai.GenerativeModel('gemini-2.5-flash')
1284
+ logger.info("Calling Gemini API...")
1285
+
1286
+ response = model.generate_content(
1287
+ prompt,
1288
+ generation_config=genai.types.GenerationConfig(
1289
+ temperature=0.1,
1290
+ max_output_tokens=100000 # Sử dụng giá trị bạn đã tăng
1291
+ )
1292
+ )
1293
+
1294
+ if not response or not response.text:
1295
+ return jsonify({
1296
+ "success": False,
1297
+ "error": "Gemini không trả về response"
1298
+ }), 500
1299
+
1300
+ result_text = response.text.strip()
1301
+ logger.info(f"Got response from Gemini: {len(result_text)} characters")
1302
+
1303
+ # DEBUG: Log phần đầu của response
1304
+ logger.info(f"Gemini response preview (first 1000 chars): {result_text[:1000]}...")
1305
+
1306
+ except Exception as gemini_error:
1307
+ logger.error(f"Lỗi gọi Gemini API: {gemini_error}")
1308
+ return jsonify({
1309
+ "success": False,
1310
+ "error": f"Lỗi gọi AI API: {str(gemini_error)}"
1311
+ }), 500
1312
+
1313
+ # Parse JSON response
1314
+ try:
1315
+ # Làm sạch JSON response
1316
+ original_result_text = result_text # Lưu bản gốc để log
1317
+
1318
+ if result_text.startswith('```json'):
1319
+ result_text = result_text.replace('```json', '').replace('```', '').strip()
1320
+ elif result_text.startswith('```'):
1321
+ result_text = result_text[3:].rstrip('```').strip()
1322
+
1323
+ logger.info(f"Cleaned response length: {len(result_text)} characters")
1324
+
1325
+ processed_data = json.loads(result_text)
1326
+ logger.info("Successfully parsed JSON response")
1327
+
1328
+ # DEBUG: Log sample chunks để kiểm tra content vs summary
1329
+ chunks = processed_data.get('chunks', [])
1330
+ logger.info(f"Found {len(chunks)} chunks in response")
1331
+
1332
+ if chunks:
1333
+ for i, chunk in enumerate(chunks[:2]): # Log 2 chunks đầu
1334
+ chunk_id = chunk.get('id', f'chunk_{i}')
1335
+ summary_len = len(chunk.get('summary', ''))
1336
+ content_len = len(chunk.get('content', ''))
1337
+ logger.info(f"Chunk {chunk_id}: summary_len={summary_len}, content_len={content_len}")
1338
+
1339
+ # Log content preview
1340
+ content_preview = chunk.get('content', '')[:300]
1341
+ summary_preview = chunk.get('summary', '')[:300]
1342
+ logger.info(f"Chunk {chunk_id} content preview: {content_preview}...")
1343
+ logger.info(f"Chunk {chunk_id} summary preview: {summary_preview}...")
1344
+
1345
+ # Lưu debug log vào file
1346
+ debug_log_path = save_gemini_response_to_file(original_result_text, processed_data, doc_id)
1347
+ logger.info(f"Debug log saved to: {debug_log_path}")
1348
+
1349
+ except json.JSONDecodeError as json_error:
1350
+ logger.error(f"JSON decode error: {json_error}")
1351
+ logger.error(f"Raw response: {result_text}")
1352
+
1353
+ # Lưu response lỗi vào file debug
1354
+ try:
1355
+ log_dir = setup_debug_logging()
1356
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
1357
+ error_log_path = os.path.join(log_dir, f"gemini_error_{doc_id}_{timestamp}.log")
1358
+
1359
+ with open(error_log_path, 'w', encoding='utf-8') as f:
1360
+ f.write("GEMINI JSON PARSE ERROR\n")
1361
+ f.write("=" * 40 + "\n")
1362
+ f.write(f"Error: {json_error}\n\n")
1363
+ f.write("Raw Response:\n")
1364
+ f.write(result_text)
1365
+
1366
+ logger.info(f"Error log saved to: {error_log_path}")
1367
+ except:
1368
+ pass
1369
+
1370
+ return jsonify({
1371
+ "success": False,
1372
+ "error": f"AI trả về format không hợp lệ: {str(json_error)}"
1373
+ }), 500
1374
+
1375
+ # Validate format
1376
+ try:
1377
+ if not validate_processed_format(processed_data):
1378
+ return jsonify({
1379
+ "success": False,
1380
+ "error": "AI trả về format không đúng cấu trúc yêu cầu"
1381
+ }), 500
1382
+ except Exception as validate_error:
1383
+ logger.error(f"Validation error: {validate_error}")
1384
+ return jsonify({
1385
+ "success": False,
1386
+ "error": f"Lỗi validate format: {str(validate_error)}"
1387
+ }), 500
1388
+
1389
+ # Lưu vào ChromaDB
1390
+ try:
1391
+ save_processed_document(doc_id, processed_data)
1392
+ logger.info(f"Successfully saved document {doc_id} to ChromaDB")
1393
+ except Exception as save_error:
1394
+ logger.error(f"Lỗi lưu document: {save_error}")
1395
+ return jsonify({
1396
+ "success": False,
1397
+ "error": f"Lỗi lưu document: {str(save_error)}"
1398
+ }), 500
1399
+
1400
+ # Xóa file tạm
1401
+ try:
1402
+ if os.path.exists(temp_path):
1403
+ os.remove(temp_path)
1404
+ logger.info(f"Deleted temp file: {temp_path}")
1405
+ except Exception as delete_error:
1406
+ logger.warning(f"Cannot delete temp file: {delete_error}")
1407
+
1408
+ return jsonify({
1409
+ "success": True,
1410
+ "message": "Xử lý tài liệu thành công",
1411
+ "processed_chunks": len(processed_data.get('chunks', [])),
1412
+ "processed_tables": len(processed_data.get('tables', [])),
1413
+ "processed_figures": len(processed_data.get('figures', [])),
1414
+ "document_id": doc_id,
1415
+ "debug_log": debug_log_path # Trả về đường dẫn log file
1416
+ })
1417
+
1418
+ except Exception as e:
1419
+ logger.error(f"Lỗi xử lý document: {str(e)}")
1420
+ return jsonify({
1421
+ "success": False,
1422
+ "error": f"Lỗi xử lý: {str(e)}"
1423
+ }), 500
1424
+
1425
+ def create_document_processing_prompt(pdf_text):
1426
+ """Tạo prompt chi tiết cho Gemini - CẢI THIỆN THÊM"""
1427
+ # Cắt ngắn text để tránh vượt quá limit
1428
+ max_text_length = 80000 # Tăng lên do bạn đã tăng token limit
1429
+ if len(pdf_text) > max_text_length:
1430
+ pdf_text = pdf_text[:max_text_length] + "..."
1431
+
1432
+ prompt = f"""
1433
+ Bạn là một chuyên gia phân tích tài liệu. Nhiệm vụ của bạn là phân tích và chia nhỏ tài liệu PDF thành các phần có nghĩa.
1434
+
1435
+ TÀI LIỆU CẦN XỬ LÝ:
1436
+ {pdf_text}
1437
+
1438
+ YÊU CẦU PHÂN TÍCH:
1439
+
1440
+ 1. PHÂN CHIA NỘI DUNG:
1441
+ - Chia tài liệu thành các chunk có nghĩa (mỗi chunk 200-1000 từ)
1442
+ - QUAN TRỌNG: Trong field "content" của mỗi chunk, bạn PHẢI SAO CHÉP NGUYÊN VĂN nội dung từ tài liệu gốc
1443
+ - Field "summary" chỉ là tóm tắt ngắn gọn (1-2 câu)
1444
+ - Field "content" phải chứa toàn bộ văn bản gốc của phần đó
1445
+ - KHÔNG viết lại, KHÔNG diễn giải, KHÔNG tóm tắt trong field "content"
1446
+
1447
+ 2. CẤU TRÚC JSON:
1448
+ Mỗi chunk PHẢI có đủ các field sau:
1449
+ - "id": ID duy nhất
1450
+ - "title": Tiêu đề của chunk
1451
+ - "content": NỘI DUNG NGUYÊN VĂN từ PDF (bắt buộc phải có)
1452
+ - "summary": Tóm tắt ngắn gọn (khác với content)
1453
+ - "content_type": "text", "table", hoặc "figure"
1454
+ - "age_range": [min_age, max_age] từ 1-19
1455
+ - "pages": trang nếu biết
1456
+ - "related_chunks": []
1457
+ - "word_count": số từ trong content
1458
+ - "token_count": ước tính token
1459
+
1460
+ 3. VÍ DỤ ĐÚNG:
1461
+ {{
1462
+ "id": "bosung1_muc1_1",
1463
+ "title": "Giới thiệu về dinh dưỡng",
1464
+ "content": "Dinh dưỡng là quá trình cung cấp cho cơ thể các chất cần thiết để duy trì sự sống, phát triển và hoạt động. Các chất dinh dưỡng bao gồm...",
1465
+ "summary": "Giới thiệu khái niệm cơ bản về dinh dưỡng",
1466
+ "content_type": "text",
1467
+ "age_range": [1, 19]
1468
+ }}
1469
+
1470
+ 4. ĐỊNH DẠNG OUTPUT JSON:
1471
+ {{
1472
+ "bai_info": {{
1473
+ "id": "bosung1",
1474
+ "title": "Tiêu đề tài liệu",
1475
+ "overview": "Tổng quan"
1476
+ }},
1477
+ "chunks": [
1478
+ {{
1479
+ "id": "bosung1_muc1_1",
1480
+ "title": "Tiêu đề chunk",
1481
+ "content": "NỘI DUNG NGUYÊN VĂN TỪ PDF - BẮTED BUỘC PHẢI CÓ",
1482
+ "summary": "Tóm tắt ngắn gọn",
1483
+ "content_type": "text",
1484
+ "age_range": [1, 19],
1485
+ "pages": "",
1486
+ "related_chunks": [],
1487
+ "word_count": 100,
1488
+ "token_count": 150,
1489
+ "contains_table": false,
1490
+ "contains_figure": false
1491
+ }}
1492
+ ],
1493
+ "tables": [],
1494
+ "figures": [],
1495
+ "total_items": {{"chunks": 1, "tables": 0, "figures": 0}}
1496
+ }}
1497
+
1498
+ LƯU Ý QUAN TRỌNG:
1499
+ - Field "content" PHẢI chứa văn bản nguyên gốc từ PDF
1500
+ - Field "summary" mới là phần tóm tắt
1501
+ - KHÔNG được để field "content" trống hoặc giống "summary"
1502
+ - Trả về JSON hợp lệ, không có text thừa
1503
+
1504
+ Hãy phân tích và trả về JSON, đảm bảo field "content" chứa nội dung đầy đủ từ PDF.
1505
+ """
1506
+ return prompt
1507
+
1508
+ def validate_processed_format(data):
1509
+ """Validate format của processed data"""
1510
+ try:
1511
+ # Kiểm tra required fields
1512
+ required_fields = ['bai_info', 'chunks', 'total_items']
1513
+ for field in required_fields:
1514
+ if field not in data:
1515
+ logger.error(f"Missing required field: {field}")
1516
+ return False
1517
+
1518
+ # Validate bai_info
1519
+ bai_info = data['bai_info']
1520
+ bai_info_fields = ['id', 'title', 'overview']
1521
+ for field in bai_info_fields:
1522
+ if field not in bai_info:
1523
+ logger.error(f"Missing bai_info field: {field}")
1524
+ return False
1525
+
1526
+ # Validate chunks
1527
+ if not isinstance(data['chunks'], list):
1528
+ logger.error("chunks must be a list")
1529
+ return False
1530
+
1531
+ for i, chunk in enumerate(data['chunks']):
1532
+ chunk_fields = ['id', 'title', 'content_type', 'age_range', 'summary']
1533
+ for field in chunk_fields:
1534
+ if field not in chunk:
1535
+ logger.error(f"Missing chunk field '{field}' in chunk {i}")
1536
+ return False
1537
+
1538
+ # Validate age_range
1539
+ age_range = chunk['age_range']
1540
+ if not isinstance(age_range, list) or len(age_range) != 2:
1541
+ logger.error(f"Invalid age_range in chunk {i}: must be list of 2 integers")
1542
+ return False
1543
+
1544
+ if not all(isinstance(x, int) and 1 <= x <= 19 for x in age_range):
1545
+ logger.error(f"Invalid age_range values in chunk {i}: must be integers 1-19")
1546
+ return False
1547
+
1548
+ # Validate optional fields
1549
+ for table in data.get('tables', []):
1550
+ if 'age_range' in table:
1551
+ age_range = table['age_range']
1552
+ if not isinstance(age_range, list) or len(age_range) != 2:
1553
+ logger.error("Invalid age_range in table")
1554
+ return False
1555
+
1556
+ for figure in data.get('figures', []):
1557
+ if 'age_range' in figure:
1558
+ age_range = figure['age_range']
1559
+ if not isinstance(age_range, list) or len(age_range) != 2:
1560
+ logger.error("Invalid age_range in figure")
1561
+ return False
1562
+
1563
+ logger.info("Document format validation passed")
1564
+ return True
1565
+
1566
+ except Exception as e:
1567
+ logger.error(f"Validation error: {e}")
1568
+ return False
1569
+
1570
+ def save_processed_document(doc_id, processed_data):
1571
+ try:
1572
+ embedding_model = get_embedding_model()
1573
+
1574
+ document_title = processed_data.get('bai_info', {}).get('title', f'Tài liệu {doc_id}')
1575
+
1576
+ # Chuẩn bị tất cả items để index
1577
+ all_items = []
1578
+
1579
+ # Xử lý chunks
1580
+ for chunk in processed_data.get('chunks', []):
1581
+ chunk_content = chunk.get('content', chunk.get('summary', ''))
1582
+ if not chunk_content:
1583
+ # Fallback nếu không có content
1584
+ chunk_content = f"Tiêu đề: {chunk['title']}\nNội dung: {chunk.get('summary', '')}"
1585
+
1586
+ # Chuẩn bị metadata theo format cần thiết
1587
+ metadata = {
1588
+ "chunk_id": chunk['id'],
1589
+ "chapter": doc_id,
1590
+ "title": chunk['title'],
1591
+ "content_type": chunk['content_type'],
1592
+ "age_range": f"{chunk['age_range'][0]}-{chunk['age_range'][1]}",
1593
+ "age_min": chunk['age_range'][0],
1594
+ "age_max": chunk['age_range'][1],
1595
+ "summary": chunk['summary'],
1596
+ "pages": chunk.get('pages', ''),
1597
+ "related_chunks": ','.join(chunk.get('related_chunks', [])),
1598
+ "word_count": chunk.get('word_count', 0),
1599
+ "token_count": chunk.get('token_count', 0),
1600
+ "contains_table": chunk.get('contains_table', False),
1601
+ "contains_figure": chunk.get('contains_figure', False),
1602
+ "created_at": datetime.datetime.now().isoformat(),
1603
+ "document_source": document_title,
1604
+ "document_title": document_title
1605
+ }
1606
+
1607
+ # Thêm vào danh sách
1608
+ all_items.append({
1609
+ "content": chunk_content,
1610
+ "metadata": metadata,
1611
+ "id": chunk['id']
1612
+ })
1613
+
1614
+ # Xử lý tables
1615
+ for table in processed_data.get('tables', []):
1616
+ table_content = table.get('content', f"Bảng: {table['title']}\nMô tả: {table.get('summary', '')}")
1617
+
1618
+ metadata = {
1619
+ "chunk_id": table['id'],
1620
+ "chapter": doc_id,
1621
+ "title": table['title'],
1622
+ "content_type": "table",
1623
+ "age_range": f"{table['age_range'][0]}-{table['age_range'][1]}",
1624
+ "age_min": table['age_range'][0],
1625
+ "age_max": table['age_range'][1],
1626
+ "summary": table['summary'],
1627
+ "pages": table.get('pages', ''),
1628
+ "related_chunks": ','.join(table.get('related_chunks', [])),
1629
+ "table_columns": ','.join(table.get('table_columns', [])),
1630
+ "word_count": table.get('word_count', 0),
1631
+ "token_count": table.get('token_count', 0),
1632
+ "created_at": datetime.datetime.now().isoformat(),
1633
+ "document_source": document_title,
1634
+ "document_title": document_title
1635
+ }
1636
+
1637
+ all_items.append({
1638
+ "content": table_content,
1639
+ "metadata": metadata,
1640
+ "id": table['id']
1641
+ })
1642
+
1643
+ # Xử lý figures
1644
+ for figure in processed_data.get('figures', []):
1645
+ figure_content = figure.get('content', f"Hình: {figure['title']}\nMô tả: {figure.get('summary', '')}")
1646
+
1647
+ metadata = {
1648
+ "chunk_id": figure['id'],
1649
+ "chapter": doc_id,
1650
+ "title": figure['title'],
1651
+ "content_type": "figure",
1652
+ "age_range": f"{figure['age_range'][0]}-{figure['age_range'][1]}",
1653
+ "age_min": figure['age_range'][0],
1654
+ "age_max": figure['age_range'][1],
1655
+ "summary": figure['summary'],
1656
+ "pages": figure.get('pages', ''),
1657
+ "related_chunks": ','.join(figure.get('related_chunks', [])),
1658
+ "created_at": datetime.datetime.now().isoformat(),
1659
+ "document_source": document_title,
1660
+ "document_title": document_title
1661
+ }
1662
+
1663
+ all_items.append({
1664
+ "content": figure_content,
1665
+ "metadata": metadata,
1666
+ "id": figure['id']
1667
+ })
1668
+
1669
+ # Sử dụng index_chunks để lưu tất cả items
1670
+ if all_items:
1671
+ success = embedding_model.index_chunks(all_items)
1672
+ if success:
1673
+ logger.info(f"Successfully indexed {len(all_items)} items for document {doc_id} (title: {document_title})")
1674
+ else:
1675
+ raise Exception("Failed to index chunks")
1676
+ else:
1677
+ logger.warning(f"No items to index for document {doc_id}")
1678
+
1679
+ except Exception as e:
1680
+ logger.error(f"Error in save_processed_document: {str(e)}")
1681
+ raise
1682
+
1683
+ @admin_routes.route('/documents/<doc_id>', methods=['DELETE'])
1684
+ @require_admin
1685
+ def delete_document(doc_id):
1686
+ """Xóa document theo chapter"""
1687
+ try:
1688
+ embedding_model = get_embedding_model()
1689
+
1690
+ # Lấy tất cả chunks của document - BỎ include=['ids']
1691
+ results = embedding_model.collection.get(
1692
+ where={"chapter": doc_id}
1693
+ # Không cần include=['ids'] vì mặc định đã trả về ids
1694
+ )
1695
+
1696
+ if results and results.get('ids'):
1697
+ # Xóa từng chunk
1698
+ for chunk_id in results['ids']:
1699
+ embedding_model.collection.delete(ids=[chunk_id])
1700
+
1701
+ return jsonify({
1702
+ "success": True,
1703
+ "message": f"Đã xóa {len(results['ids'])} chunks của document {doc_id}"
1704
+ })
1705
+ else:
1706
+ return jsonify({
1707
+ "success": False,
1708
+ "error": "Không tìm thấy document"
1709
+ }), 404
1710
+
1711
+ except Exception as e:
1712
+ logger.error(f"Lỗi xóa document: {str(e)}")
1713
+ return jsonify({
1714
+ "success": False,
1715
+ "error": str(e)
1716
+ }), 500
1717
+
1718
+ @admin_routes.route('/documents/bulk-delete', methods=['POST'])
1719
+ @require_admin
1720
+ def bulk_delete_documents():
1721
+ """Xóa nhiều documents"""
1722
+ try:
1723
+ data = request.json
1724
+ doc_ids = data.get('doc_ids', [])
1725
+
1726
+ if not doc_ids:
1727
+ return jsonify({
1728
+ "success": False,
1729
+ "error": "Không có document nào được chọn"
1730
+ }), 400
1731
+
1732
+ embedding_model = get_embedding_model()
1733
+ deleted_count = 0
1734
+
1735
+ for doc_id in doc_ids:
1736
+ try:
1737
+ # SỬA: Bỏ include=['ids']
1738
+ results = embedding_model.collection.get(
1739
+ where={"chapter": doc_id}
1740
+ )
1741
+
1742
+ if results and results.get('ids'):
1743
+ for chunk_id in results['ids']:
1744
+ embedding_model.collection.delete(ids=[chunk_id])
1745
+ deleted_count += 1
1746
+
1747
+ except Exception as e:
1748
+ logger.error(f"Lỗi xóa document {doc_id}: {e}")
1749
+ continue
1750
+
1751
+ return jsonify({
1752
+ "success": True,
1753
+ "message": f"Đã xóa {deleted_count}/{len(doc_ids)} documents",
1754
+ "deleted_count": deleted_count
1755
+ })
1756
+
1757
+ except Exception as e:
1758
+ logger.error(f"Lỗi xóa bulk documents: {str(e)}")
1759
+ return jsonify({
1760
+ "success": False,
1761
+ "error": str(e)
1762
+ }), 500
1763
+
1764
+ # ===== ANALYTICS ROUTES =====
1765
+
1766
+ @admin_routes.route('/analytics/overview', methods=['GET'])
1767
+ @require_admin
1768
+ def get_analytics_overview():
1769
+ """Lấy thống kê phân tích"""
1770
+ try:
1771
+ db = get_db()
1772
+
1773
+ # Thống kê cơ bản
1774
+ total_conversations = db.conversations.count_documents({})
1775
+ total_users = db.users.count_documents({}) if hasattr(db, 'users') else 1
1776
+
1777
+ # Thống kê theo tuần (7 ngày gần nhất)
1778
+ daily_stats = []
1779
+ for i in range(7):
1780
+ day_start = datetime.datetime.now() - datetime.timedelta(days=6-i)
1781
+ day_end = day_start + datetime.timedelta(days=1)
1782
+
1783
+ daily_conversations = db.conversations.count_documents({
1784
+ "created_at": {"$gte": day_start, "$lt": day_end}
1785
+ })
1786
+
1787
+ daily_stats.append({
1788
+ "date": day_start.strftime("%Y-%m-%d"),
1789
+ "label": day_start.strftime("%d/%m"),
1790
+ "conversations": daily_conversations,
1791
+ "users": 0 # Mock data
1792
+ })
1793
+
1794
+ # Thống kê theo độ tuổi
1795
+ age_stats = list(db.conversations.aggregate([
1796
+ {"$group": {"_id": "$age_context", "count": {"$sum": 1}}},
1797
+ {"$sort": {"_id": 1}}
1798
+ ]))
1799
+
1800
+ return jsonify({
1801
+ "success": True,
1802
+ "overview": {
1803
+ "totalUsers": total_users,
1804
+ "activeUsers": total_users,
1805
+ "totalConversations": total_conversations,
1806
+ "avgMessagesPerConversation": 6.8,
1807
+ "userGrowth": "+8.5%",
1808
+ "conversationGrowth": "+12.3%"
1809
+ },
1810
+ "dailyStats": daily_stats,
1811
+ "ageDistribution": [
1812
+ {
1813
+ "age_group": f"{stat['_id']} tuổi" if stat['_id'] else "Không rõ",
1814
+ "count": stat["count"]
1815
+ }
1816
+ for stat in age_stats
1817
+ ]
1818
+ })
1819
+
1820
+ except Exception as e:
1821
+ logger.error(f"Lỗi lấy analytics: {str(e)}")
1822
+ return jsonify({
1823
+ "success": False,
1824
+ "error": str(e)
1825
+ }), 500
1826
+
1827
+ @admin_routes.route('/users/<user_id>', methods=['PUT'])
1828
+ @require_admin
1829
+ def update_user(user_id):
1830
+ """Cập nhật thông tin người dùng"""
1831
+ try:
1832
+ data = request.json
1833
+
1834
+ user = User.find_by_id(user_id)
1835
+ if not user:
1836
+ return jsonify({
1837
+ "success": False,
1838
+ "error": "Không tìm thấy người dùng"
1839
+ }), 404
1840
+
1841
+ # Cập nhật thông tin
1842
+ if 'name' in data:
1843
+ user.name = data['name']
1844
+
1845
+ if 'gender' in data:
1846
+ user.gender = data['gender']
1847
+
1848
+ if 'role' in data:
1849
+ user.role = data['role']
1850
+
1851
+ # Lưu thay đổi
1852
+ user.save()
1853
+
1854
+ return jsonify({
1855
+ "success": True,
1856
+ "message": "Cập nhật người dùng thành công",
1857
+ "user": {
1858
+ "id": str(user.user_id),
1859
+ "name": user.name,
1860
+ "email": user.email,
1861
+ "gender": user.gender,
1862
+ "role": user.role
1863
+ }
1864
+ })
1865
+
1866
+ except Exception as e:
1867
+ logger.error(f"Lỗi cập nhật user: {str(e)}")
1868
+ return jsonify({
1869
+ "success": False,
1870
+ "error": str(e)
1871
+ }), 500
1872
+
1873
+ @admin_routes.route('/feedback', methods=['GET'])
1874
+ @require_admin
1875
+ def get_all_feedback():
1876
+ """API endpoint để admin xem tất cả feedback"""
1877
+ try:
1878
+ page = int(request.args.get('page', 1))
1879
+ per_page = int(request.args.get('per_page', 20))
1880
+ status_filter = request.args.get('status')
1881
+ skip = (page - 1) * per_page
1882
+
1883
+ feedbacks = Feedback.get_all_for_admin(limit=per_page, skip=skip, status_filter=status_filter)
1884
+
1885
+ result = []
1886
+ for feedback in feedbacks:
1887
+ result.append({
1888
+ "id": str(feedback.feedback_id),
1889
+ "user_name": getattr(feedback, 'user_name', 'Ẩn danh'),
1890
+ "user_email": getattr(feedback, 'user_email', ''),
1891
+ "rating": feedback.rating,
1892
+ "category": feedback.category,
1893
+ "title": feedback.title,
1894
+ "content": feedback.content,
1895
+ "status": feedback.status,
1896
+ "admin_response": feedback.admin_response,
1897
+ "created_at": feedback.created_at.isoformat(),
1898
+ "updated_at": feedback.updated_at.isoformat()
1899
+ })
1900
+
1901
+ return jsonify({
1902
+ "success": True,
1903
+ "feedbacks": result
1904
+ })
1905
+
1906
+ except Exception as e:
1907
+ logger.error(f"Error getting feedback for admin: {e}")
1908
+ return jsonify({
1909
+ "success": False,
1910
+ "error": str(e)
1911
+ }), 500
1912
+
1913
+ @admin_routes.route('/feedback/stats', methods=['GET'])
1914
+ @require_admin
1915
+ def get_feedback_stats():
1916
+ """API endpoint để lấy thống kê feedback"""
1917
+ try:
1918
+ stats = Feedback.get_stats()
1919
+ return jsonify({
1920
+ "success": True,
1921
+ "stats": stats
1922
+ })
1923
+ except Exception as e:
1924
+ logger.error(f"Error getting feedback stats: {e}")
1925
+ return jsonify({
1926
+ "success": False,
1927
+ "error": str(e)
1928
+ }), 500
1929
+
1930
+ @admin_routes.route('/feedback/<feedback_id>/respond', methods=['PUT'])
1931
+ @require_admin
1932
+ def respond_to_feedback(feedback_id):
1933
+ """API endpoint để admin phản hồi feedback"""
1934
+ try:
1935
+ data = request.json
1936
+ response_text = data.get('response', '')
1937
+ new_status = data.get('status', 'reviewed')
1938
+
1939
+ if not response_text.strip():
1940
+ return jsonify({
1941
+ "success": False,
1942
+ "error": "Vui lòng nhập phản hồi"
1943
+ }), 400
1944
+
1945
+ feedback = Feedback.find_by_id(feedback_id)
1946
+ if not feedback:
1947
+ return jsonify({
1948
+ "success": False,
1949
+ "error": "Không tìm thấy feedback"
1950
+ }), 404
1951
+
1952
+ success = feedback.update_admin_response(response_text.strip(), new_status)
1953
+
1954
+ if success:
1955
+ return jsonify({
1956
+ "success": True,
1957
+ "message": "Đã phản hồi feedback thành công"
1958
+ })
1959
+ else:
1960
+ return jsonify({
1961
+ "success": False,
1962
+ "error": "Không thể cập nhật phản hồi"
1963
+ }), 500
1964
+
1965
+ except Exception as e:
1966
+ logger.error(f"Error responding to feedback: {e}")
1967
+ return jsonify({
1968
+ "success": False,
1969
+ "error": str(e)
1970
+ }), 500
1971
+
1972
+ # ===== SYSTEM SETTINGS ROUTES =====
1973
+
1974
+ @admin_routes.route('/settings/system-config', methods=['GET'])
1975
+ @require_admin
1976
+ def get_system_config():
1977
+ """Lấy cấu hình hệ thống thật"""
1978
+ try:
1979
+ import os
1980
+ from config import EMBEDDING_MODEL, GEMINI_API_KEY, CHROMA_PERSIST_DIRECTORY, COLLECTION_NAME
1981
+ from core.embedding_model import get_embedding_model
1982
+
1983
+ # Thông tin database
1984
+ db = get_db()
1985
+
1986
+ # Thông tin Collections trong MongoDB
1987
+ try:
1988
+ mongo_collections = db.list_collection_names()
1989
+ mongo_stats = {}
1990
+ for collection_name in mongo_collections:
1991
+ collection = db[collection_name]
1992
+ mongo_stats[collection_name] = {
1993
+ "document_count": collection.count_documents({}),
1994
+ "estimated_size": collection.estimated_document_count()
1995
+ }
1996
+ except Exception as e:
1997
+ mongo_collections = []
1998
+ mongo_stats = {"error": str(e)}
1999
+
2000
+ # Thông tin ChromaDB/Vector Database
2001
+ try:
2002
+ embedding_model = get_embedding_model()
2003
+ vector_stats = embedding_model.get_stats()
2004
+ chroma_status = "Connected"
2005
+ vector_count = embedding_model.count()
2006
+ except Exception as e:
2007
+ vector_stats = {"error": str(e)}
2008
+ chroma_status = "Error"
2009
+ vector_count = 0
2010
+
2011
+ # Thông tin Gemini API
2012
+ gemini_status = "Connected" if GEMINI_API_KEY else "Not configured"
2013
+
2014
+ # Thông tin hệ thống
2015
+ import platform
2016
+ import psutil
2017
+
2018
+ system_info = {
2019
+ "python_version": platform.python_version(),
2020
+ "platform": platform.platform(),
2021
+ "cpu_count": psutil.cpu_count(),
2022
+ "memory_total": round(psutil.virtual_memory().total / (1024**3), 2), # GB
2023
+ "memory_available": round(psutil.virtual_memory().available / (1024**3), 2), # GB
2024
+ "disk_usage": round(psutil.disk_usage('/').percent, 2)
2025
+ }
2026
+
2027
+ # Cấu hình application
2028
+ app_config = {
2029
+ "debug_mode": os.getenv("FLASK_ENV") == "development",
2030
+ "secret_key_configured": bool(os.getenv("JWT_SECRET_KEY")),
2031
+ "mongodb_uri": os.getenv("MONGO_URI", "mongodb://localhost:27017/"),
2032
+ "database_name": os.getenv("MONGO_DB_NAME", "nutribot_db"),
2033
+ "embedding_model": EMBEDDING_MODEL,
2034
+ "chroma_directory": CHROMA_PERSIST_DIRECTORY,
2035
+ "collection_name": COLLECTION_NAME,
2036
+ "gemini_configured": bool(GEMINI_API_KEY)
2037
+ }
2038
+
2039
+ return jsonify({
2040
+ "success": True,
2041
+ "system_config": {
2042
+ "application": app_config,
2043
+ "system": system_info,
2044
+ "database": {
2045
+ "mongodb": {
2046
+ "status": "Connected",
2047
+ "collections": mongo_collections,
2048
+ "statistics": mongo_stats
2049
+ },
2050
+ "vector_db": {
2051
+ "status": chroma_status,
2052
+ "document_count": vector_count,
2053
+ "statistics": vector_stats
2054
+ }
2055
+ },
2056
+ "ai_services": {
2057
+ "gemini": {
2058
+ "status": gemini_status,
2059
+ "model": "gemini-2.0-flash"
2060
+ }
2061
+ }
2062
+ }
2063
+ })
2064
+
2065
+ except Exception as e:
2066
+ logger.error(f"Lỗi lấy system config: {str(e)}")
2067
+ return jsonify({
2068
+ "success": False,
2069
+ "error": str(e)
2070
+ }), 500
2071
+
2072
+ @admin_routes.route('/settings/performance', methods=['GET'])
2073
+ @require_admin
2074
+ def get_performance_metrics():
2075
+ """Lấy metrics hiệu năng hệ thống"""
2076
+ try:
2077
+ import psutil
2078
+ import time
2079
+ from datetime import datetime, timedelta
2080
+
2081
+ # CPU và Memory hiện tại
2082
+ cpu_percent = psutil.cpu_percent(interval=1)
2083
+ memory = psutil.virtual_memory()
2084
+ disk = psutil.disk_usage('/')
2085
+
2086
+ # Thống kê database
2087
+ db = get_db()
2088
+
2089
+ # Thống kê MongoDB performance
2090
+ try:
2091
+ # Thời gian trung bình cho queries (mock data - MongoDB professional monitoring cần tools khác)
2092
+ conversations_count = db.conversations.count_documents({})
2093
+ users_count = db.users.count_documents({}) if hasattr(db, 'users') else 0
2094
+
2095
+ # Tốc độ xử lý tin nhắn trong 24h qua
2096
+ yesterday = datetime.now() - timedelta(days=1)
2097
+ recent_conversations = db.conversations.count_documents({
2098
+ "updated_at": {"$gte": yesterday}
2099
+ })
2100
+
2101
+ db_performance = {
2102
+ "total_documents": conversations_count + users_count,
2103
+ "query_speed": "~50ms", # Mock data
2104
+ "recent_activity": recent_conversations,
2105
+ "index_efficiency": "95%" # Mock data
2106
+ }
2107
+ except Exception as e:
2108
+ db_performance = {"error": str(e)}
2109
+
2110
+ # Vector DB performance
2111
+ try:
2112
+ from core.embedding_model import get_embedding_model
2113
+ embedding_model = get_embedding_model()
2114
+ vector_count = embedding_model.count()
2115
+
2116
+ # Test search speed
2117
+ start_time = time.time()
2118
+ test_results = embedding_model.search("test query", top_k=5)
2119
+ search_time = (time.time() - start_time) * 1000 # milliseconds
2120
+
2121
+ vector_performance = {
2122
+ "total_vectors": vector_count,
2123
+ "search_speed_ms": round(search_time, 2),
2124
+ "embedding_dimension": 768, # multilingual-e5-base dimension
2125
+ "retrieval_accuracy": "85%" # Mock data - would need evaluation dataset
2126
+ }
2127
+ except Exception as e:
2128
+ vector_performance = {"error": str(e)}
2129
+
2130
+ # AI API performance
2131
+ ai_performance = {
2132
+ "average_response_time": "2.5s", # Mock data
2133
+ "success_rate": "98.5%", # Mock data
2134
+ "daily_requests": recent_conversations * 2, # Estimate
2135
+ "token_usage": "~150k tokens/day" # Mock data
2136
+ }
2137
+
2138
+ return jsonify({
2139
+ "success": True,
2140
+ "performance": {
2141
+ "system": {
2142
+ "cpu_usage": cpu_percent,
2143
+ "memory_usage": memory.percent,
2144
+ "memory_total_gb": round(memory.total / (1024**3), 2),
2145
+ "memory_used_gb": round(memory.used / (1024**3), 2),
2146
+ "disk_usage": disk.percent,
2147
+ "disk_total_gb": round(disk.total / (1024**3), 2),
2148
+ "uptime": "24h 15m" # Mock data
2149
+ },
2150
+ "database": db_performance,
2151
+ "vector_search": vector_performance,
2152
+ "ai_generation": ai_performance
2153
+ }
2154
+ })
2155
+
2156
+ except Exception as e:
2157
+ logger.error(f"Lỗi lấy performance metrics: {str(e)}")
2158
+ return jsonify({
2159
+ "success": False,
2160
+ "error": str(e)
2161
+ }), 500
2162
+
2163
+ @admin_routes.route('/settings/logs', methods=['GET'])
2164
+ @require_admin
2165
+ def get_system_logs():
2166
+ """Lấy logs hệ thống"""
2167
+ try:
2168
+ import os
2169
+ from datetime import datetime
2170
+
2171
+ log_entries = []
2172
+
2173
+ # Đọc logs từ file nếu có
2174
+ log_files = [
2175
+ "logs/app.log",
2176
+ "logs/error.log",
2177
+ "embed_data.log"
2178
+ ]
2179
+
2180
+ for log_file in log_files:
2181
+ if os.path.exists(log_file):
2182
+ try:
2183
+ with open(log_file, 'r', encoding='utf-8') as f:
2184
+ lines = f.readlines()
2185
+ # Lấy 50 dòng cuối
2186
+ recent_lines = lines[-50:] if len(lines) > 50 else lines
2187
+
2188
+ for line in recent_lines:
2189
+ if line.strip():
2190
+ log_entries.append({
2191
+ "timestamp": datetime.now().isoformat(),
2192
+ "level": "INFO", # Parse từ log format thực tế
2193
+ "source": log_file,
2194
+ "message": line.strip()
2195
+ })
2196
+ except Exception as e:
2197
+ log_entries.append({
2198
+ "timestamp": datetime.now().isoformat(),
2199
+ "level": "ERROR",
2200
+ "source": "system",
2201
+ "message": f"Cannot read {log_file}: {str(e)}"
2202
+ })
2203
+
2204
+ # Nếu không có log files, tạo mock logs
2205
+ if not log_entries:
2206
+ log_entries = [
2207
+ {
2208
+ "timestamp": datetime.now().isoformat(),
2209
+ "level": "INFO",
2210
+ "source": "system",
2211
+ "message": "Application started successfully"
2212
+ },
2213
+ {
2214
+ "timestamp": datetime.now().isoformat(),
2215
+ "level": "INFO",
2216
+ "source": "database",
2217
+ "message": "MongoDB connection established"
2218
+ },
2219
+ {
2220
+ "timestamp": datetime.now().isoformat(),
2221
+ "level": "INFO",
2222
+ "source": "vector_db",
2223
+ "message": "ChromaDB initialized successfully"
2224
+ }
2225
+ ]
2226
+
2227
+ # Sort by timestamp descending
2228
+ log_entries.sort(key=lambda x: x["timestamp"], reverse=True)
2229
+
2230
+ return jsonify({
2231
+ "success": True,
2232
+ "logs": log_entries[:100] # Limit to 100 entries
2233
+ })
2234
+
2235
+ except Exception as e:
2236
+ logger.error(f"Lỗi lấy system logs: {str(e)}")
2237
+ return jsonify({
2238
+ "success": False,
2239
+ "error": str(e)
2240
+ }), 500
2241
+
2242
+ @admin_routes.route('/settings/backup', methods=['POST'])
2243
+ @require_admin
2244
+ def create_backup():
2245
+ """Tạo backup dữ liệu"""
2246
+ try:
2247
+ from datetime import datetime
2248
+ import json
2249
+ import os
2250
+
2251
+ # Tạo thư mục backup nếu chưa có
2252
+ backup_dir = "backups"
2253
+ if not os.path.exists(backup_dir):
2254
+ os.makedirs(backup_dir)
2255
+
2256
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
2257
+
2258
+ # Backup MongoDB data
2259
+ db = get_db()
2260
+ backup_data = {
2261
+ "timestamp": datetime.now().isoformat(),
2262
+ "collections": {}
2263
+ }
2264
+
2265
+ # Export conversations
2266
+ conversations = list(db.conversations.find({}))
2267
+ for conv in conversations:
2268
+ conv["_id"] = str(conv["_id"]) # Convert ObjectId to string
2269
+ if "user_id" in conv:
2270
+ conv["user_id"] = str(conv["user_id"])
2271
+ backup_data["collections"]["conversations"] = conversations
2272
+
2273
+ # Export users if exists
2274
+ if hasattr(db, 'users'):
2275
+ users = list(db.users.find({}))
2276
+ for user in users:
2277
+ user["_id"] = str(user["_id"])
2278
+ # Remove password for security
2279
+ if "password" in user:
2280
+ del user["password"]
2281
+ backup_data["collections"]["users"] = users
2282
+
2283
+ # Save backup file
2284
+ backup_filename = f"backup_{timestamp}.json"
2285
+ backup_path = os.path.join(backup_dir, backup_filename)
2286
+
2287
+ with open(backup_path, 'w', encoding='utf-8') as f:
2288
+ json.dump(backup_data, f, indent=2, default=str, ensure_ascii=False)
2289
+
2290
+ # Get file size
2291
+ file_size = os.path.getsize(backup_path)
2292
+ file_size_mb = round(file_size / (1024 * 1024), 2)
2293
+
2294
+ return jsonify({
2295
+ "success": True,
2296
+ "message": "Backup created successfully",
2297
+ "backup": {
2298
+ "filename": backup_filename,
2299
+ "path": backup_path,
2300
+ "size_mb": file_size_mb,
2301
+ "timestamp": datetime.now().isoformat(),
2302
+ "collections_count": len(backup_data["collections"]),
2303
+ "total_documents": sum(len(coll) for coll in backup_data["collections"].values())
2304
+ }
2305
+ })
2306
+
2307
+ except Exception as e:
2308
+ logger.error(f"Lỗi tạo backup: {str(e)}")
2309
+ return jsonify({
2310
+ "success": False,
2311
+ "error": str(e)
2312
+ }), 500
2313
+
2314
+ @admin_routes.route('/settings/security', methods=['GET'])
2315
+ @require_admin
2316
+ def get_security_settings():
2317
+ """Lấy cài đặt bảo mật"""
2318
+ try:
2319
+ import os
2320
+ from datetime import datetime, timedelta
2321
+
2322
+ # Kiểm tra cấu hình bảo mật
2323
+ security_config = {
2324
+ "jwt_configured": bool(os.getenv("JWT_SECRET_KEY")),
2325
+ "admin_accounts": 1, # Mock data
2326
+ "password_policy": {
2327
+ "min_length": 6,
2328
+ "require_special_chars": False,
2329
+ "require_numbers": False
2330
+ },
2331
+ "session_timeout": "24 hours",
2332
+ "ssl_enabled": False, # Mock data
2333
+ "rate_limiting": False # Mock data
2334
+ }
2335
+
2336
+ # Recent login attempts (mock data)
2337
+ recent_logins = [
2338
+ {
2339
+ "timestamp": (datetime.now() - timedelta(hours=1)).isoformat(),
2340
+ "user": "[email protected]",
2341
+ "ip": "127.0.0.1",
2342
+ "status": "success"
2343
+ },
2344
+ {
2345
+ "timestamp": (datetime.now() - timedelta(hours=3)).isoformat(),
2346
+ "user": "[email protected]",
2347
+ "ip": "127.0.0.1",
2348
+ "status": "success"
2349
+ }
2350
+ ]
2351
+
2352
+ # Security recommendations
2353
+ recommendations = [
2354
+ {
2355
+ "priority": "high",
2356
+ "title": "Enable HTTPS",
2357
+ "description": "Deploy with SSL certificate for production"
2358
+ },
2359
+ {
2360
+ "priority": "medium",
2361
+ "title": "Implement Rate Limiting",
2362
+ "description": "Add rate limiting to prevent abuse"
2363
+ },
2364
+ {
2365
+ "priority": "low",
2366
+ "title": "Strengthen Password Policy",
2367
+ "description": "Require stronger passwords for admin accounts"
2368
+ }
2369
+ ]
2370
+
2371
+ return jsonify({
2372
+ "success": True,
2373
+ "security": {
2374
+ "configuration": security_config,
2375
+ "recent_logins": recent_logins,
2376
+ "recommendations": recommendations
2377
+ }
2378
+ })
2379
+
2380
+ except Exception as e:
2381
+ logger.error(f"Lỗi lấy security settings: {str(e)}")
2382
+ return jsonify({
2383
+ "success": False,
2384
+ "error": str(e)
2385
+ }), 500
api/auth.py ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify, make_response
2
+ import re
3
+ import logging
4
+ from models.user_model import User
5
+ from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, get_jwt
6
+ import datetime
7
+ from functools import wraps
8
+
9
+ # Cấu hình logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Tạo blueprint
13
+ auth_routes = Blueprint('auth', __name__)
14
+
15
+ def validate_email(email):
16
+ """Kiểm tra định dạng email"""
17
+ pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
18
+ return re.match(pattern, email) is not None
19
+
20
+ def validate_password(password):
21
+ """Kiểm tra mật khẩu có đủ mạnh không"""
22
+ return len(password) >= 6
23
+
24
+ # SỬA: Decorator đơn giản hơn để check admin
25
+ def require_admin(f):
26
+ """Decorator đơn giản để kiểm tra admin"""
27
+ @wraps(f)
28
+ @jwt_required()
29
+ def decorated_function(*args, **kwargs):
30
+ try:
31
+ user_id = get_jwt_identity()
32
+ user = User.find_by_id(user_id)
33
+
34
+ if not user:
35
+ return jsonify({
36
+ "success": False,
37
+ "error": "User không tồn tại"
38
+ }), 403
39
+
40
+ # Kiểm tra có phải admin không
41
+ if not user.is_admin():
42
+ return jsonify({
43
+ "success": False,
44
+ "error": "Không có quyền truy cập admin"
45
+ }), 403
46
+
47
+ # Thêm user vào request context
48
+ request.current_user = user
49
+ return f(*args, **kwargs)
50
+
51
+ except Exception as e:
52
+ logger.error(f"Lỗi xác thực admin: {e}")
53
+ return jsonify({
54
+ "success": False,
55
+ "error": "Lỗi xác thực"
56
+ }), 500
57
+ return decorated_function
58
+
59
+ def register_user(name, email, password, gender=None):
60
+ """Đăng ký người dùng mới"""
61
+ if not name or not email or not password:
62
+ return False, "Vui lòng nhập đầy đủ thông tin"
63
+
64
+ if not validate_email(email):
65
+ return False, "Email không hợp lệ"
66
+
67
+ if not validate_password(password):
68
+ return False, "Mật khẩu phải có ít nhất 6 ký tự"
69
+
70
+ success, result = User.register(name, email, password, gender)
71
+ if success:
72
+ return True, {"user_id": result}
73
+ else:
74
+ return False, result
75
+
76
+ def login_user(email, password):
77
+ """Đăng nhập người dùng"""
78
+ if not email or not password:
79
+ return False, "Vui lòng nhập đầy đủ thông tin đăng nhập"
80
+
81
+ success, result = User.login(email, password)
82
+ if success:
83
+ user = result
84
+
85
+ # Tạo JWT token với thời gian hết hạn 24 giờ
86
+ expires = datetime.timedelta(hours=24)
87
+ access_token = create_access_token(
88
+ identity=str(user.user_id),
89
+ expires_delta=expires,
90
+ additional_claims={
91
+ "name": user.name,
92
+ "email": user.email,
93
+ "gender": user.gender,
94
+ "role": user.role,
95
+ "permissions": user.permissions,
96
+ "is_admin": user.is_admin()
97
+ }
98
+ )
99
+
100
+ return True, {
101
+ "user_id": str(user.user_id),
102
+ "user": {
103
+ "id": str(user.user_id),
104
+ "name": user.name,
105
+ "email": user.email,
106
+ "gender": user.gender,
107
+ "role": user.role,
108
+ "permissions": user.permissions,
109
+ "is_admin": user.is_admin()
110
+ },
111
+ "access_token": access_token,
112
+ "expires_in": 86400
113
+ }
114
+ else:
115
+ return False, result
116
+
117
+ @auth_routes.route('/register', methods=['POST'])
118
+ def register():
119
+ """API endpoint để đăng ký người dùng mới"""
120
+ try:
121
+ data = request.json
122
+
123
+ name = data.get('fullName')
124
+ email = data.get('email')
125
+ password = data.get('password')
126
+ gender = data.get('gender')
127
+
128
+ success, result = register_user(name, email, password, gender)
129
+
130
+ if success:
131
+ return jsonify({
132
+ "success": True,
133
+ "message": "Đăng ký thành công",
134
+ "user_id": result.get("user_id")
135
+ })
136
+ else:
137
+ return jsonify({
138
+ "success": False,
139
+ "error": result
140
+ }), 400
141
+
142
+ except Exception as e:
143
+ logger.error(f"Lỗi đăng ký người dùng: {str(e)}")
144
+ return jsonify({
145
+ "success": False,
146
+ "error": str(e)
147
+ }), 500
148
+
149
+ @auth_routes.route('/login', methods=['POST'])
150
+ def login():
151
+ """API endpoint để đăng nhập"""
152
+ try:
153
+ data = request.json
154
+
155
+ email = data.get('email')
156
+ password = data.get('password')
157
+ remember_me = data.get('rememberMe', False)
158
+
159
+ success, result = login_user(email, password)
160
+
161
+ if success:
162
+ access_token = result.get("access_token")
163
+
164
+ response = make_response(jsonify({
165
+ "success": True,
166
+ "user_id": result.get("user_id"),
167
+ "user": result.get("user"),
168
+ "access_token": access_token,
169
+ "expires_in": result.get("expires_in")
170
+ }))
171
+
172
+ cookie_max_age = 86400 if remember_me else None
173
+ response.set_cookie(
174
+ 'access_token_cookie',
175
+ access_token,
176
+ max_age=cookie_max_age,
177
+ httponly=True,
178
+ path='/api',
179
+ samesite='Lax',
180
+ secure=False
181
+ )
182
+
183
+ return response
184
+ else:
185
+ return jsonify({
186
+ "success": False,
187
+ "error": result
188
+ }), 401
189
+
190
+ except Exception as e:
191
+ logger.error(f"Lỗi đăng nhập: {str(e)}")
192
+ return jsonify({
193
+ "success": False,
194
+ "error": str(e)
195
+ }), 500
196
+
197
+ @auth_routes.route('/logout', methods=['POST'])
198
+ def logout():
199
+ """API endpoint để đăng xuất"""
200
+ response = make_response(jsonify({
201
+ "success": True,
202
+ "message": "Đăng xuất thành công"
203
+ }))
204
+ response.delete_cookie('access_token_cookie', path='/api')
205
+ return response
206
+
207
+ @auth_routes.route('/verify-token', methods=['POST'])
208
+ @jwt_required()
209
+ def verify_token():
210
+ """API endpoint để kiểm tra token có hợp lệ không"""
211
+ try:
212
+ current_user_id = get_jwt_identity()
213
+
214
+ user = User.find_by_id(current_user_id)
215
+ if not user:
216
+ return jsonify({
217
+ "success": False,
218
+ "error": "User không tồn tại"
219
+ }), 401
220
+
221
+ return jsonify({
222
+ "success": True,
223
+ "user_id": current_user_id,
224
+ "user": {
225
+ "id": str(user.user_id),
226
+ "name": user.name,
227
+ "email": user.email,
228
+ "gender": user.gender,
229
+ "role": user.role,
230
+ "permissions": user.permissions,
231
+ "is_admin": user.is_admin()
232
+ }
233
+ })
234
+ except Exception as e:
235
+ return jsonify({
236
+ "success": False,
237
+ "error": str(e)
238
+ }), 401
239
+
240
+ @auth_routes.route('/profile', methods=['GET'])
241
+ @jwt_required()
242
+ def user_profile():
243
+ """API endpoint để lấy thông tin người dùng"""
244
+ try:
245
+ user_id = get_jwt_identity()
246
+ user = User.find_by_id(user_id)
247
+
248
+ if user:
249
+ profile = {
250
+ "id": str(user.user_id),
251
+ "name": user.name,
252
+ "email": user.email,
253
+ "gender": user.gender,
254
+ "role": user.role,
255
+ "permissions": user.permissions,
256
+ "is_admin": user.is_admin(),
257
+ "created_at": user.created_at.isoformat() if user.created_at else None,
258
+ "updated_at": user.updated_at.isoformat() if user.updated_at else None,
259
+ "last_login": user.last_login.isoformat() if user.last_login else None
260
+ }
261
+
262
+ return jsonify({
263
+ "success": True,
264
+ "user": profile
265
+ })
266
+ else:
267
+ return jsonify({
268
+ "success": False,
269
+ "error": "Không tìm thấy thông tin người dùng"
270
+ }), 404
271
+
272
+ except Exception as e:
273
+ logger.error(f"Lỗi lấy thông tin người dùng: {str(e)}")
274
+ return jsonify({
275
+ "success": False,
276
+ "error": str(e)
277
+ }), 500
278
+
279
+ @auth_routes.route('/profile', methods=['PUT'])
280
+ @jwt_required()
281
+ def update_profile():
282
+ """API endpoint để cập nhật thông tin người dùng"""
283
+ try:
284
+ data = request.json
285
+ user_id = get_jwt_identity()
286
+
287
+ user = User.find_by_id(user_id)
288
+ if not user:
289
+ return jsonify({
290
+ "success": False,
291
+ "error": "Không tìm thấy người dùng"
292
+ }), 404
293
+
294
+ if 'name' in data:
295
+ user.name = data['name']
296
+
297
+ if 'gender' in data:
298
+ user.gender = data['gender']
299
+
300
+ user.save()
301
+
302
+ return jsonify({
303
+ "success": True,
304
+ "message": "Cập nhật thông tin thành công"
305
+ })
306
+
307
+ except Exception as e:
308
+ logger.error(f"Lỗi cập nhật thông tin người dùng: {str(e)}")
309
+ return jsonify({
310
+ "success": False,
311
+ "error": str(e)
312
+ }), 500
313
+
314
+ @auth_routes.route('/change-password', methods=['POST'])
315
+ @jwt_required()
316
+ def update_password():
317
+ """API endpoint để đổi mật khẩu"""
318
+ try:
319
+ data = request.json
320
+ user_id = get_jwt_identity()
321
+
322
+ current_password = data.get('currentPassword')
323
+ new_password = data.get('newPassword')
324
+
325
+ user = User.find_by_id(user_id)
326
+ if not user:
327
+ return jsonify({
328
+ "success": False,
329
+ "error": "Không tìm thấy người dùng"
330
+ }), 404
331
+
332
+ if not User.check_password(user.password, current_password):
333
+ return jsonify({
334
+ "success": False,
335
+ "error": "Mật khẩu hiện tại không chính xác"
336
+ }), 400
337
+
338
+ if not validate_password(new_password):
339
+ return jsonify({
340
+ "success": False,
341
+ "error": "Mật khẩu mới phải có ít nhất 6 ký tự"
342
+ }), 400
343
+
344
+ user.password = User.hash_password(new_password)
345
+ user.save()
346
+
347
+ return jsonify({
348
+ "success": True,
349
+ "message": "Đổi mật khẩu thành công"
350
+ })
351
+
352
+ except Exception as e:
353
+ logger.error(f"Lỗi đổi mật khẩu: {str(e)}")
354
+ return jsonify({
355
+ "success": False,
356
+ "error": str(e)
357
+ }), 500
358
+
359
+ # === ADMIN ENDPOINTS - SỬA LẠI ĐỂ SỬ DỤNG CHUNG TOKEN ===
360
+
361
+ @auth_routes.route('/admin/stats/overview', methods=['GET'])
362
+ @require_admin
363
+ def get_admin_overview_stats():
364
+ """API endpoint để lấy thống kê tổng quan cho admin"""
365
+ try:
366
+ from models.conversation_model import get_db
367
+
368
+ db = get_db()
369
+
370
+ # Đếm tổng conversations
371
+ total_conversations = db.conversations.count_documents({})
372
+
373
+ # Conversations trong 24h qua
374
+ day_ago = datetime.datetime.now() - datetime.timedelta(days=1)
375
+ recent_conversations = db.conversations.count_documents({
376
+ "created_at": {"$gte": day_ago}
377
+ })
378
+
379
+ # Đếm tổng tin nhắn
380
+ pipeline = [
381
+ {"$project": {"message_count": {"$size": "$messages"}}},
382
+ {"$group": {"_id": None, "total_messages": {"$sum": "$message_count"}}}
383
+ ]
384
+ message_result = list(db.conversations.aggregate(pipeline))
385
+ total_messages = message_result[0]["total_messages"] if message_result else 0
386
+
387
+ # Mock users data (có thể thay bằng query thực tế)
388
+ total_users = db.users.count_documents({}) if hasattr(db, 'users') else 1
389
+
390
+ return jsonify({
391
+ "success": True,
392
+ "stats": {
393
+ "users": {
394
+ "total": total_users,
395
+ "new_today": 0
396
+ },
397
+ "conversations": {
398
+ "total": total_conversations,
399
+ "recent": recent_conversations
400
+ },
401
+ "data": {
402
+ "total_chunks": total_messages,
403
+ "total_tables": 0,
404
+ "total_figures": 0,
405
+ "total_items": total_messages,
406
+ "embeddings": 0
407
+ },
408
+ "admins": {
409
+ "total": 1
410
+ }
411
+ }
412
+ })
413
+
414
+ except Exception as e:
415
+ logger.error(f"Lỗi lấy thống kê admin: {str(e)}")
416
+ return jsonify({
417
+ "success": False,
418
+ "error": str(e)
419
+ }), 500
420
+
421
+ @auth_routes.route('/admin/recent-activities', methods=['GET'])
422
+ @require_admin
423
+ def get_admin_recent_activities():
424
+ """API endpoint để lấy hoạt động gần đây cho admin"""
425
+ try:
426
+ limit = int(request.args.get('limit', 10))
427
+
428
+ from models.conversation_model import get_db
429
+ db = get_db()
430
+
431
+ # Lấy conversations gần đây
432
+ recent_conversations = list(db.conversations.find(
433
+ {},
434
+ {"title": 1, "created_at": 1, "updated_at": 1}
435
+ ).sort("updated_at", -1).limit(limit))
436
+
437
+ activities = []
438
+ for conv in recent_conversations:
439
+ activities.append({
440
+ "type": "conversation_created",
441
+ "title": "Cuộc hội thoại mới",
442
+ "description": conv.get("title", "Cuộc hội thoại"),
443
+ "timestamp": conv.get("updated_at", datetime.datetime.now()).isoformat()
444
+ })
445
+
446
+ return jsonify({
447
+ "success": True,
448
+ "activities": activities
449
+ })
450
+
451
+ except Exception as e:
452
+ logger.error(f"Lỗi lấy hoạt động gần đây: {str(e)}")
453
+ return jsonify({
454
+ "success": False,
455
+ "error": str(e)
456
+ }), 500
457
+
458
+ @auth_routes.route('/admin/alerts', methods=['GET'])
459
+ @require_admin
460
+ def get_admin_system_alerts():
461
+ """API endpoint để lấy cảnh báo hệ thống cho admin"""
462
+ try:
463
+ # Mock alerts - có thể thay bằng logic thực tế
464
+ alerts = [
465
+ {
466
+ "type": "info",
467
+ "title": "Hệ thống hoạt động bình thường",
468
+ "message": "Tất cả các dịch vụ đang chạy ổn định",
469
+ "severity": "low"
470
+ }
471
+ ]
472
+
473
+ return jsonify({
474
+ "success": True,
475
+ "alerts": alerts
476
+ })
477
+
478
+ except Exception as e:
479
+ logger.error(f"Lỗi lấy cảnh báo hệ thống: {str(e)}")
480
+ return jsonify({
481
+ "success": False,
482
+ "error": str(e)
483
+ }), 500
484
+
485
+ @auth_routes.route('/init-admin', methods=['POST'])
486
+ def init_admin():
487
+ """API endpoint để khởi tạo admin mặc định"""
488
+ try:
489
+ success, result = User.create_default_admin()
490
+
491
+ if success:
492
+ return jsonify({
493
+ "success": True,
494
+ "message": "Khởi tạo admin thành công",
495
+ "data": result
496
+ })
497
+ else:
498
+ return jsonify({
499
+ "success": False,
500
+ "error": result
501
+ }), 400
502
+
503
+ except Exception as e:
504
+ logger.error(f"Lỗi khởi tạo admin: {str(e)}")
505
+ return jsonify({
506
+ "success": False,
507
+ "error": str(e)
508
+ }), 500
api/chat.py ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ import logging
3
+ from core.rag_pipeline import RAGPipeline
4
+ from core.embedding_model import get_embedding_model
5
+ from models.conversation_model import Conversation
6
+ from flask_jwt_extended import jwt_required, get_jwt_identity
7
+ import datetime
8
+
9
+ # Cấu hình logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Tạo blueprint
13
+ chat_routes = Blueprint('chat', __name__)
14
+
15
+ # Khởi tạo RAG Pipeline một lần duy nhất khi module được load
16
+ rag_pipeline = None
17
+
18
+ def get_rag_pipeline():
19
+ """Singleton pattern để tránh khởi tạo lại RAG Pipeline"""
20
+ global rag_pipeline
21
+ if rag_pipeline is None:
22
+ logger.info("Khởi tạo RAG Pipeline lần đầu")
23
+ rag_pipeline = RAGPipeline()
24
+ return rag_pipeline
25
+
26
+ # Hàm tạo tiêu đề từ tin nhắn
27
+ def create_title_from_message(message, max_length=50):
28
+ """Tạo tiêu đề cuộc trò chuyện từ tin nhắn đầu tiên của người dùng"""
29
+ message = message.strip().replace('\n', ' ')
30
+ if len(message) <= max_length:
31
+ return message
32
+ return message[:max_length-3] + "..."
33
+
34
+ @chat_routes.route('/chat', methods=['POST'])
35
+ @jwt_required()
36
+ def chat():
37
+ """API endpoint để xử lý tin nhắn chat"""
38
+ try:
39
+ data = request.json
40
+ message = data.get('message')
41
+ age = data.get('age', 1)
42
+ conversation_id = data.get('conversation_id')
43
+
44
+ user_id = get_jwt_identity()
45
+
46
+ if not message:
47
+ return jsonify({
48
+ "success": False,
49
+ "error": "Vui lòng nhập tin nhắn"
50
+ }), 400
51
+
52
+ logger.info(f"Nhận tin nhắn từ user {user_id}: {message[:50]}...")
53
+
54
+ # Xử lý conversation
55
+ if conversation_id:
56
+ conversation = Conversation.find_by_id(conversation_id)
57
+ if not conversation or str(conversation.user_id) != user_id:
58
+ return jsonify({
59
+ "success": False,
60
+ "error": "Không tìm thấy cuộc trò chuyện"
61
+ }), 404
62
+
63
+ if len(conversation.messages) == 0:
64
+ final_title = create_title_from_message(message, 50)
65
+ conversation.title = final_title
66
+ logger.info(f"Cập nhật title cho conversation {conversation_id}: '{final_title}'")
67
+ else:
68
+ final_title = create_title_from_message(message, 50)
69
+ conversation = Conversation.create(
70
+ user_id=user_id,
71
+ title=final_title,
72
+ age_context=age
73
+ )
74
+ logger.info(f"Tạo conversation mới với title: '{final_title}'")
75
+
76
+ # Thêm tin nhắn của user
77
+ conversation.add_message("user", message)
78
+
79
+ # Sử dụng RAG Pipeline để generate response
80
+ pipeline = get_rag_pipeline()
81
+
82
+ # Generate response sử dụng RAG
83
+ logger.info("Bắt đầu generate response với RAG Pipeline")
84
+ response_data = pipeline.generate_response(message, age)
85
+
86
+ if response_data.get("success"):
87
+ bot_response = response_data.get("response", "Xin lỗi, tôi không thể trả lời câu hỏi này.")
88
+ sources = response_data.get("sources", [])
89
+
90
+ # Thêm tin nhắn bot vào conversation
91
+ conversation.add_message("bot", bot_response, sources=sources)
92
+
93
+ logger.info(f"Đã generate response thành công cho conversation {conversation.conversation_id}")
94
+
95
+ return jsonify({
96
+ "success": True,
97
+ "response": bot_response,
98
+ "sources": sources,
99
+ "conversation_id": str(conversation.conversation_id)
100
+ })
101
+ else:
102
+ error_msg = response_data.get("error", "Không thể tạo phản hồi")
103
+ logger.error(f"Lỗi generate response: {error_msg}")
104
+
105
+ return jsonify({
106
+ "success": False,
107
+ "error": error_msg
108
+ }), 500
109
+
110
+ except Exception as e:
111
+ logger.error(f"Lỗi xử lý chat: {str(e)}")
112
+ return jsonify({
113
+ "success": False,
114
+ "error": f"Lỗi máy chủ: {str(e)}"
115
+ }), 500
116
+
117
+ @chat_routes.route('/messages/<message_id>/edit', methods=['PUT'])
118
+ @jwt_required()
119
+ def edit_message(message_id):
120
+ """API endpoint để chỉnh sửa tin nhắn"""
121
+ try:
122
+ data = request.json
123
+ new_content = data.get('content')
124
+ conversation_id = data.get('conversation_id')
125
+ age = data.get('age', 1)
126
+
127
+ user_id = get_jwt_identity()
128
+
129
+ if not new_content or not conversation_id:
130
+ return jsonify({
131
+ "success": False,
132
+ "error": "Thiếu thông tin cần thiết"
133
+ }), 400
134
+
135
+ logger.info(f"Edit message {message_id} in conversation {conversation_id}: {new_content[:50]}...")
136
+
137
+ # Tìm conversation
138
+ conversation = Conversation.find_by_id(conversation_id)
139
+ if not conversation or str(conversation.user_id) != user_id:
140
+ return jsonify({
141
+ "success": False,
142
+ "error": "Không tìm thấy cuộc trò chuyện"
143
+ }), 404
144
+
145
+ # Tìm message và kiểm tra quyền
146
+ message_found = False
147
+ for msg in conversation.messages:
148
+ if str(msg.get('_id', msg.get('id'))) == message_id:
149
+ if msg['role'] != 'user':
150
+ return jsonify({
151
+ "success": False,
152
+ "error": "Chỉ có thể chỉnh sửa tin nhắn của người dùng"
153
+ }), 400
154
+ message_found = True
155
+ break
156
+
157
+ if not message_found:
158
+ return jsonify({
159
+ "success": False,
160
+ "error": "Không tìm thấy tin nhắn"
161
+ }), 404
162
+
163
+ # Cập nhật tin nhắn và xóa tất cả tin nhắn sau nó
164
+ success, result_message = conversation.edit_message(message_id, new_content)
165
+
166
+ if not success:
167
+ return jsonify({
168
+ "success": False,
169
+ "error": result_message
170
+ }), 400
171
+
172
+ # Tạo phản hồi mới với RAG Pipeline
173
+ pipeline = get_rag_pipeline()
174
+ response_data = pipeline.generate_response(new_content, age)
175
+
176
+ if response_data.get("success"):
177
+ bot_response = response_data.get("response", "Xin lỗi, tôi không thể trả lời câu hỏi này.")
178
+ sources = response_data.get("sources", [])
179
+
180
+ # Thêm phản hồi bot mới
181
+ success, bot_message = conversation.regenerate_bot_response_after_edit(message_id, bot_response, sources)
182
+
183
+ if success:
184
+ # Trả về conversation đã cập nhật
185
+ updated_conversation = Conversation.find_by_id(conversation_id)
186
+ logger.info(f"Successfully edited message and generated new response")
187
+ return jsonify({
188
+ "success": True,
189
+ "message": "Đã chỉnh sửa tin nhắn và tạo phản hồi mới",
190
+ "conversation": updated_conversation.to_dict()
191
+ })
192
+ else:
193
+ return jsonify({
194
+ "success": False,
195
+ "error": bot_message
196
+ }), 500
197
+ else:
198
+ return jsonify({
199
+ "success": False,
200
+ "error": response_data.get("error", "Không thể tạo phản hồi mới")
201
+ }), 500
202
+
203
+ except Exception as e:
204
+ logger.error(f"Lỗi chỉnh sửa tin nhắn: {str(e)}")
205
+ return jsonify({
206
+ "success": False,
207
+ "error": f"Lỗi máy chủ: {str(e)}"
208
+ }), 500
209
+
210
+ @chat_routes.route('/messages/<message_id>/regenerate', methods=['POST'])
211
+ @jwt_required()
212
+ def regenerate_response(message_id):
213
+ """API endpoint để tạo lại phản hồi"""
214
+ try:
215
+ data = request.json
216
+ conversation_id = data.get('conversation_id')
217
+ age = data.get('age', 1)
218
+
219
+ user_id = get_jwt_identity()
220
+
221
+ if not conversation_id:
222
+ return jsonify({
223
+ "success": False,
224
+ "error": "Thiếu conversation_id"
225
+ }), 400
226
+
227
+ logger.info(f"Regenerate response for message {message_id} in conversation {conversation_id}")
228
+
229
+ # Tìm conversation
230
+ conversation = Conversation.find_by_id(conversation_id)
231
+ if not conversation or str(conversation.user_id) != user_id:
232
+ return jsonify({
233
+ "success": False,
234
+ "error": "Không tìm thấy cuộc trò chuyện"
235
+ }), 404
236
+
237
+ # Tìm tin nhắn và tin nhắn user trước đó
238
+ user_message = None
239
+ for i, msg in enumerate(conversation.messages):
240
+ if str(msg.get('_id', msg.get('id'))) == message_id:
241
+ if msg['role'] != 'bot':
242
+ return jsonify({
243
+ "success": False,
244
+ "error": "Chỉ có thể regenerate phản hồi của bot"
245
+ }), 400
246
+ # Tìm tin nhắn user trước đó
247
+ if i > 0 and conversation.messages[i-1]['role'] == 'user':
248
+ user_message = conversation.messages[i-1]['content']
249
+ break
250
+
251
+ if not user_message:
252
+ return jsonify({
253
+ "success": False,
254
+ "error": "Không tìm thấy tin nhắn người dùng tương ứng"
255
+ }), 404
256
+
257
+ # Sử dụng RAG Pipeline để generate response mới
258
+ pipeline = get_rag_pipeline()
259
+ response_data = pipeline.generate_response(user_message, age)
260
+
261
+ if response_data.get("success"):
262
+ bot_response = response_data.get("response", "Xin lỗi, tôi không thể trả lời câu hỏi này.")
263
+ sources = response_data.get("sources", [])
264
+
265
+ success, result_message = conversation.regenerate_response(message_id, bot_response, sources)
266
+
267
+ if success:
268
+ # Trả về conversation đã cập nhật
269
+ updated_conversation = Conversation.find_by_id(conversation_id)
270
+ logger.info(f"Successfully regenerated response")
271
+ return jsonify({
272
+ "success": True,
273
+ "conversation": updated_conversation.to_dict()
274
+ })
275
+ else:
276
+ return jsonify({
277
+ "success": False,
278
+ "error": result_message
279
+ }), 400
280
+ else:
281
+ return jsonify({
282
+ "success": False,
283
+ "error": response_data.get("error", "Không thể tạo phản hồi mới")
284
+ }), 500
285
+
286
+ except Exception as e:
287
+ logger.error(f"Lỗi regenerate response: {str(e)}")
288
+ return jsonify({
289
+ "success": False,
290
+ "error": f"Lỗi máy chủ: {str(e)}"
291
+ }), 500
292
+
293
+ @chat_routes.route('/messages/<message_id>/versions/<int:version>', methods=['PUT'])
294
+ @jwt_required()
295
+ def switch_message_version(message_id, version):
296
+ """API endpoint để chuyển đổi version của tin nhắn"""
297
+ try:
298
+ data = request.json
299
+ conversation_id = data.get('conversation_id')
300
+
301
+ user_id = get_jwt_identity()
302
+
303
+ if not conversation_id:
304
+ return jsonify({
305
+ "success": False,
306
+ "error": "Thiếu conversation_id"
307
+ }), 400
308
+
309
+ logger.info(f"Switching message {message_id} to version {version} in conversation {conversation_id}")
310
+
311
+ # Tìm conversation
312
+ conversation = Conversation.find_by_id(conversation_id)
313
+ if not conversation or str(conversation.user_id) != user_id:
314
+ return jsonify({
315
+ "success": False,
316
+ "error": "Không tìm thấy cuộc trò chuyện"
317
+ }), 404
318
+
319
+ # Debug: Log current conversation state before switch
320
+ logger.info(f"Before switch - Conversation has {len(conversation.messages)} messages")
321
+ for i, msg in enumerate(conversation.messages):
322
+ logger.info(f" Message {i}: {msg['role']} - {msg['content'][:30]}... (current_version: {msg.get('current_version', 1)})")
323
+
324
+ # Chuyển đổi version
325
+ success = conversation.switch_message_version(message_id, version)
326
+
327
+ if success:
328
+ # Reload conversation để đảm bảo có dữ liệu mới nhất
329
+ updated_conversation = Conversation.find_by_id(conversation_id)
330
+
331
+ # Debug: Log conversation state after switch
332
+ logger.info(f"After switch - Conversation has {len(updated_conversation.messages)} messages")
333
+ for i, msg in enumerate(updated_conversation.messages):
334
+ logger.info(f" Message {i}: {msg['role']} - {msg['content'][:30]}... (current_version: {msg.get('current_version', 1)})")
335
+
336
+ logger.info(f"Successfully switched message {message_id} to version {version}")
337
+ return jsonify({
338
+ "success": True,
339
+ "conversation": updated_conversation.to_dict()
340
+ })
341
+ else:
342
+ logger.error(f"Failed to switch message {message_id} to version {version}")
343
+ return jsonify({
344
+ "success": False,
345
+ "error": "Không thể chuyển đổi version"
346
+ }), 400
347
+
348
+ except Exception as e:
349
+ logger.error(f"Lỗi chuyển đổi version: {str(e)}")
350
+ return jsonify({
351
+ "success": False,
352
+ "error": f"Lỗi máy chủ: {str(e)}"
353
+ }), 500
354
+
355
+ @chat_routes.route('/messages/<message_id>', methods=['DELETE'])
356
+ @jwt_required()
357
+ def delete_message_and_following(message_id):
358
+ """API endpoint để xóa tin nhắn và tất cả tin nhắn sau nó"""
359
+ try:
360
+ data = request.json
361
+ conversation_id = data.get('conversation_id')
362
+
363
+ user_id = get_jwt_identity()
364
+
365
+ if not conversation_id:
366
+ return jsonify({
367
+ "success": False,
368
+ "error": "Thiếu conversation_id"
369
+ }), 400
370
+
371
+ # Tìm conversation
372
+ conversation = Conversation.find_by_id(conversation_id)
373
+ if not conversation or str(conversation.user_id) != user_id:
374
+ return jsonify({
375
+ "success": False,
376
+ "error": "Không tìm thấy cuộc trò chuyện"
377
+ }), 404
378
+
379
+ # Xóa tin nhắn và các tin nhắn theo sau
380
+ success = conversation.delete_message_and_following(message_id)
381
+
382
+ if success:
383
+ updated_conversation = Conversation.find_by_id(conversation_id)
384
+ return jsonify({
385
+ "success": True,
386
+ "conversation": updated_conversation.to_dict()
387
+ })
388
+ else:
389
+ return jsonify({
390
+ "success": False,
391
+ "error": "Không thể xóa tin nhắn"
392
+ }), 400
393
+
394
+ except Exception as e:
395
+ logger.error(f"Lỗi xóa tin nhắn: {str(e)}")
396
+ return jsonify({
397
+ "success": False,
398
+ "error": f"Lỗi máy chủ: {str(e)}"
399
+ }), 500
api/data.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify, send_from_directory
2
+ import os
3
+ import logging
4
+ from core.data_processor import DataProcessor
5
+ from flask_jwt_extended import jwt_required, get_jwt_identity
6
+
7
+ # Cấu hình logging
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # Tạo blueprint
11
+ data_routes = Blueprint('data', __name__)
12
+
13
+ # Đường dẫn đến thư mục dữ liệu
14
+ current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15
+ data_dir = os.path.join(current_dir, "..", "data")
16
+ data_dir = os.path.abspath(data_dir)
17
+
18
+ @data_routes.route('/figures/<path:bai_id>/<path:filename>', methods=['GET'])
19
+ def serve_figure(bai_id, filename):
20
+ """API endpoint để phục vụ hình ảnh theo bài"""
21
+ try:
22
+ # Đường dẫn chính xác đến thư mục figures
23
+ figure_dir = os.path.join(data_dir, bai_id, 'figures')
24
+ figure_dir = os.path.abspath(figure_dir)
25
+
26
+ # Kiểm tra xem thư mục có tồn tại không
27
+ if os.path.exists(figure_dir) and os.path.isdir(figure_dir):
28
+ for ext in ['', '.png', '.jpg', '.jpeg', '.gif', '.svg']:
29
+ # Kiểm tra với nhiều phần mở rộng khác nhau
30
+ file_to_check = os.path.join(figure_dir, filename + ext)
31
+ if os.path.exists(file_to_check):
32
+ return send_from_directory(figure_dir, filename + ext)
33
+
34
+ logger.error(f"Không tìm thấy hình ảnh: {os.path.join(figure_dir, filename)}")
35
+ return jsonify({"error": "Không tìm thấy hình ảnh"}), 404
36
+
37
+ except Exception as e:
38
+ logger.error(f"Lỗi khi tải hình ảnh: {str(e)}")
39
+ return jsonify({"error": f"Lỗi máy chủ: {str(e)}"}), 500
40
+
41
+ @data_routes.route('/metadata', methods=['GET'])
42
+ @jwt_required(optional=True)
43
+ def get_metadata():
44
+ """API endpoint để lấy thông tin metadata của tài liệu"""
45
+ try:
46
+ # Tạo data_processor mới để lấy metadata
47
+ data_processor = DataProcessor(data_dir=data_dir)
48
+ metadata = data_processor.get_all_metadata()
49
+ return jsonify({
50
+ "success": True,
51
+ "metadata": metadata
52
+ })
53
+ except Exception as e:
54
+ logger.error(f"Lỗi khi lấy metadata: {str(e)}")
55
+ return jsonify({
56
+ "success": False,
57
+ "error": str(e)
58
+ }), 500
api/feedback.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ import logging
3
+ from models.feedback_model import Feedback
4
+ from models.user_model import User
5
+ from flask_jwt_extended import jwt_required, get_jwt_identity
6
+
7
+ # Cấu hình logging
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # Tạo blueprint
11
+ feedback_routes = Blueprint('feedback', __name__)
12
+
13
+ @feedback_routes.route('/feedback/categories', methods=['GET'])
14
+ def get_feedback_categories():
15
+ """API endpoint để lấy danh sách categories"""
16
+ categories = [
17
+ {"value": "bug_report", "label": "Báo lỗi"},
18
+ {"value": "feature_request", "label": "Đề xuất tính năng"},
19
+ {"value": "content_feedback", "label": "Phản hồi nội dung"},
20
+ {"value": "user_experience", "label": "Trải nghiệm người dùng"},
21
+ {"value": "other", "label": "Khác"}
22
+ ]
23
+
24
+ return jsonify({
25
+ "success": True,
26
+ "categories": categories
27
+ })
28
+
29
+ @feedback_routes.route('/feedback', methods=['POST'])
30
+ @jwt_required()
31
+ def create_feedback():
32
+ """API endpoint để tạo feedback mới"""
33
+ try:
34
+ data = request.json
35
+ user_id = get_jwt_identity()
36
+
37
+ rating = data.get('rating', 5)
38
+ category = data.get('category', '')
39
+ title = data.get('title', '')
40
+ content = data.get('content', '')
41
+
42
+ if not category or not content.strip():
43
+ return jsonify({
44
+ "success": False,
45
+ "error": "Vui lòng điền đầy đủ thông tin"
46
+ }), 400
47
+
48
+ if len(content.strip()) < 10:
49
+ return jsonify({
50
+ "success": False,
51
+ "error": "Nội dung phải có ít nhất 10 ký tự"
52
+ }), 400
53
+
54
+ success, result = Feedback.create_feedback(
55
+ user_id=user_id,
56
+ rating=rating,
57
+ category=category,
58
+ title=title,
59
+ content=content
60
+ )
61
+
62
+ if success:
63
+ return jsonify({
64
+ "success": True,
65
+ "message": "Đã gửi feedback thành công",
66
+ "feedback_id": result["feedback_id"]
67
+ })
68
+ else:
69
+ return jsonify({
70
+ "success": False,
71
+ "error": result
72
+ }), 500
73
+
74
+ except Exception as e:
75
+ logger.error(f"Lỗi tạo feedback: {str(e)}")
76
+ return jsonify({
77
+ "success": False,
78
+ "error": str(e)
79
+ }), 500
80
+
81
+ @feedback_routes.route('/feedback', methods=['GET'])
82
+ @jwt_required()
83
+ def get_user_feedback():
84
+ """API endpoint để lấy feedback của user hiện tại"""
85
+ try:
86
+ user_id = get_jwt_identity()
87
+ page = int(request.args.get('page', 1))
88
+ per_page = int(request.args.get('per_page', 10))
89
+
90
+ skip = (page - 1) * per_page
91
+ feedbacks = Feedback.find_by_user(user_id, limit=per_page, skip=skip)
92
+
93
+ result = []
94
+ for feedback in feedbacks:
95
+ result.append({
96
+ "id": str(feedback.feedback_id),
97
+ "rating": feedback.rating,
98
+ "category": feedback.category,
99
+ "title": feedback.title,
100
+ "content": feedback.content,
101
+ "status": feedback.status,
102
+ "admin_response": feedback.admin_response,
103
+ "created_at": feedback.created_at.isoformat(),
104
+ "updated_at": feedback.updated_at.isoformat()
105
+ })
106
+
107
+ return jsonify({
108
+ "success": True,
109
+ "feedbacks": result
110
+ })
111
+
112
+ except Exception as e:
113
+ logger.error(f"Lỗi lấy feedback: {str(e)}")
114
+ return jsonify({
115
+ "success": False,
116
+ "error": str(e)
117
+ }), 500
api/history.py ADDED
@@ -0,0 +1,777 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ import logging
3
+ from bson.objectid import ObjectId
4
+ from models.conversation_model import Conversation
5
+ from models.user_model import User
6
+ from flask_jwt_extended import jwt_required, get_jwt_identity
7
+
8
+ # Cấu hình logging
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Tạo blueprint
12
+ history_routes = Blueprint('history', __name__)
13
+
14
+ # Hàm đơn giản để tạo tiêu đề từ tin nhắn
15
+ def create_title_from_message(message, max_length=50):
16
+ """Tạo tiêu đề cuộc trò chuyện từ tin nhắn đầu tiên của người dùng"""
17
+ # Loại bỏ ký tự xuống dòng và khoảng trắng thừa
18
+ message = message.strip().replace('\n', ' ')
19
+
20
+ # Nếu tin nhắn đủ ngắn, sử dụng làm tiêu đề luôn
21
+ if len(message) <= max_length:
22
+ return message
23
+
24
+ # Nếu tin nhắn quá dài, cắt ngắn và thêm dấu "..."
25
+ return message[:max_length-3] + "..."
26
+
27
+ @history_routes.route('/conversations', methods=['GET'])
28
+ @jwt_required()
29
+ def get_conversations():
30
+ """API endpoint để lấy danh sách cuộc hội thoại của người dùng"""
31
+ try:
32
+ user_id = get_jwt_identity()
33
+
34
+ # Lấy tham số phân trang từ query string
35
+ page = int(request.args.get('page', 1))
36
+ per_page = int(request.args.get('per_page', 50)) # ✅ TĂNG default per_page
37
+ include_archived = request.args.get('include_archived', 'false').lower() == 'true'
38
+
39
+ # Tính toán offset
40
+ skip = (page - 1) * per_page
41
+
42
+ logger.info(f"🔍 Getting conversations for user {user_id}, page {page}, per_page {per_page}, include_archived {include_archived}")
43
+
44
+ # Lấy danh sách cuộc hội thoại
45
+ conversations = Conversation.find_by_user(
46
+ user_id=user_id,
47
+ limit=per_page,
48
+ skip=skip,
49
+ include_archived=include_archived
50
+ )
51
+
52
+ # Đếm tổng số cuộc hội thoại
53
+ total_count = Conversation.count_by_user(
54
+ user_id=user_id,
55
+ include_archived=include_archived
56
+ )
57
+
58
+ logger.info(f"📊 Found {len(conversations)} conversations, total: {total_count}")
59
+
60
+ # Chuẩn bị dữ liệu phản hồi
61
+ result = []
62
+ for conversation in conversations:
63
+ # Chỉ lấy tin nhắn mới nhất để hiển thị xem trước
64
+ last_message = conversation.messages[-1]["content"] if conversation.messages else ""
65
+ message_count = len(conversation.messages)
66
+
67
+ result.append({
68
+ "id": str(conversation.conversation_id),
69
+ "title": conversation.title,
70
+ "created_at": conversation.created_at.isoformat(),
71
+ "updated_at": conversation.updated_at.isoformat(),
72
+ "age_context": conversation.age_context,
73
+ "is_archived": conversation.is_archived,
74
+ "last_message": last_message[:100] + "..." if len(last_message) > 100 else last_message,
75
+ "message_count": message_count
76
+ })
77
+
78
+ logger.info(f"✅ Returning {len(result)} conversations")
79
+
80
+ # Tạo phản hồi với thông tin phân trang
81
+ return jsonify({
82
+ "success": True,
83
+ "conversations": result,
84
+ "pagination": {
85
+ "page": page,
86
+ "per_page": per_page,
87
+ "total": total_count,
88
+ "pages": (total_count + per_page - 1) // per_page # Ceiling division
89
+ }
90
+ })
91
+
92
+ except Exception as e:
93
+ logger.error(f"❌ Lỗi khi lấy danh sách cuộc hội thoại: {str(e)}")
94
+ return jsonify({
95
+ "success": False,
96
+ "error": str(e)
97
+ }), 500
98
+
99
+ @history_routes.route('/conversations/<conversation_id>', methods=['GET'])
100
+ @jwt_required()
101
+ def get_conversation_detail(conversation_id):
102
+ """API endpoint để lấy chi tiết một cuộc hội thoại"""
103
+ try:
104
+ user_id = get_jwt_identity()
105
+
106
+ def safe_datetime_to_string(dt_obj):
107
+ """Safely convert datetime object to ISO string"""
108
+ if dt_obj is None:
109
+ return None
110
+ # Nếu đã là string, return nguyên
111
+ if isinstance(dt_obj, str):
112
+ return dt_obj
113
+ # Nếu là datetime object, convert sang string
114
+ if hasattr(dt_obj, 'isoformat'):
115
+ return dt_obj.isoformat()
116
+ # Fallback: convert to string
117
+ return str(dt_obj)
118
+
119
+ # Lấy thông tin cuộc hội thoại
120
+ conversation = Conversation.find_by_id(conversation_id)
121
+
122
+ if not conversation:
123
+ return jsonify({
124
+ "success": False,
125
+ "error": "Không tìm thấy cuộc hội thoại"
126
+ }), 404
127
+
128
+ # Kiểm tra quy���n truy cập
129
+ if str(conversation.user_id) != user_id:
130
+ return jsonify({
131
+ "success": False,
132
+ "error": "Bạn không có quyền truy cập cuộc hội thoại này"
133
+ }), 403
134
+
135
+ conversation_data = {
136
+ "id": str(conversation.conversation_id),
137
+ "title": conversation.title,
138
+ "created_at": safe_datetime_to_string(conversation.created_at),
139
+ "updated_at": safe_datetime_to_string(conversation.updated_at),
140
+ "age_context": conversation.age_context,
141
+ "is_archived": conversation.is_archived,
142
+ "messages": []
143
+ }
144
+
145
+ for message in conversation.messages:
146
+ message_data = {
147
+ "id": str(message["_id"]),
148
+ "_id": str(message["_id"]),
149
+ "role": message["role"],
150
+ "content": message["content"],
151
+ "timestamp": safe_datetime_to_string(message.get("timestamp")),
152
+ "current_version": message.get("current_version", 1),
153
+ "is_edited": message.get("is_edited", False)
154
+ }
155
+
156
+ if "versions" in message and message["versions"]:
157
+ message_data["versions"] = []
158
+ for version in message["versions"]:
159
+ version_data = {
160
+ "content": version["content"],
161
+ "timestamp": safe_datetime_to_string(version.get("timestamp")),
162
+ "version": version["version"]
163
+ }
164
+
165
+ # Thêm sources cho version nếu có
166
+ if "sources" in version:
167
+ version_data["sources"] = version["sources"]
168
+
169
+ # Thêm metadata cho version nếu có
170
+ if "metadata" in version:
171
+ version_data["metadata"] = version["metadata"]
172
+
173
+ # conversation_snapshot chỉ dùng để restore, không cần trả về frontend
174
+ message_data["versions"].append(version_data)
175
+ else:
176
+ # Nếu không có versions, tạo default version
177
+ message_data["versions"] = [{
178
+ "content": message["content"],
179
+ "timestamp": safe_datetime_to_string(message.get("timestamp")),
180
+ "version": 1
181
+ }]
182
+
183
+ # Thêm sources nếu có
184
+ if "sources" in message:
185
+ message_data["sources"] = message["sources"]
186
+
187
+ # Thêm metadata nếu có
188
+ if "metadata" in message:
189
+ message_data["metadata"] = message["metadata"]
190
+
191
+ conversation_data["messages"].append(message_data)
192
+
193
+ return jsonify({
194
+ "success": True,
195
+ "conversation": conversation_data
196
+ })
197
+
198
+ except Exception as e:
199
+ logger.error(f"Lỗi khi lấy chi tiết cuộc hội thoại: {str(e)}")
200
+ return jsonify({
201
+ "success": False,
202
+ "error": str(e)
203
+ }), 500
204
+
205
+ @history_routes.route('/conversations', methods=['POST'])
206
+ @jwt_required()
207
+ def create_conversation():
208
+ """API endpoint để tạo cuộc hội thoại mới"""
209
+ try:
210
+ data = request.json
211
+ user_id = get_jwt_identity()
212
+
213
+ # Lấy thông tin user
214
+ user = User.find_by_id(user_id)
215
+ if not user:
216
+ return jsonify({
217
+ "success": False,
218
+ "error": "Không tìm thấy thông tin người dùng"
219
+ }), 404
220
+
221
+ # Tạo cuộc hội thoại mới
222
+ title = data.get('title', 'Cuộc trò chuyện mới')
223
+ age_context = data.get('age_context')
224
+
225
+ # ✅ SỬA: Sử dụng Conversation.create thay vì khởi tạo trực tiếp
226
+ conversation_id = Conversation.create(
227
+ user_id=user_id,
228
+ title=title,
229
+ age_context=age_context
230
+ )
231
+
232
+ logger.info(f"✅ Created new conversation {conversation_id} for user {user_id}")
233
+
234
+ return jsonify({
235
+ "success": True,
236
+ "message": "Đã tạo cuộc hội thoại mới",
237
+ "conversation_id": str(conversation_id)
238
+ })
239
+
240
+ except Exception as e:
241
+ logger.error(f"❌ Lỗi khi tạo cuộc hội thoại mới: {str(e)}")
242
+ return jsonify({
243
+ "success": False,
244
+ "error": str(e)
245
+ }), 500
246
+
247
+ @history_routes.route('/conversations/<conversation_id>', methods=['PUT'])
248
+ @jwt_required()
249
+ def update_conversation(conversation_id):
250
+ """API endpoint để cập nhật thông tin cuộc hội thoại"""
251
+ try:
252
+ data = request.json
253
+ user_id = get_jwt_identity()
254
+
255
+ # Lấy thông tin cuộc hội thoại
256
+ conversation = Conversation.find_by_id(conversation_id)
257
+
258
+ if not conversation:
259
+ return jsonify({
260
+ "success": False,
261
+ "error": "Không tìm thấy cuộc hội thoại"
262
+ }), 404
263
+
264
+ # Kiểm tra quyền truy cập
265
+ if str(conversation.user_id) != user_id:
266
+ return jsonify({
267
+ "success": False,
268
+ "error": "Bạn không có quyền cập nhật cuộc hội thoại này"
269
+ }), 403
270
+
271
+ # Cập nhật thông tin
272
+ if 'title' in data:
273
+ conversation.title = data['title']
274
+
275
+ if 'age_context' in data:
276
+ conversation.age_context = data['age_context']
277
+
278
+ if 'is_archived' in data:
279
+ conversation.is_archived = data['is_archived']
280
+
281
+ # Lưu thay đổi
282
+ conversation.save()
283
+
284
+ logger.info(f"✅ Updated conversation {conversation_id}")
285
+
286
+ return jsonify({
287
+ "success": True,
288
+ "message": "Đã cập nhật thông tin cuộc hội thoại"
289
+ })
290
+
291
+ except Exception as e:
292
+ logger.error(f"Lỗi khi cập nhật thông tin cuộc hội thoại: {str(e)}")
293
+ return jsonify({
294
+ "success": False,
295
+ "error": str(e)
296
+ }), 500
297
+
298
+ @history_routes.route('/conversations/<conversation_id>', methods=['DELETE'])
299
+ @jwt_required()
300
+ def delete_conversation(conversation_id):
301
+ """API endpoint để xóa cuộc hội thoại"""
302
+ try:
303
+ user_id = get_jwt_identity()
304
+
305
+ # Lấy thông tin cuộc hội thoại
306
+ conversation = Conversation.find_by_id(conversation_id)
307
+
308
+ if not conversation:
309
+ return jsonify({
310
+ "success": False,
311
+ "error": "Không tìm thấy cuộc hội thoại"
312
+ }), 404
313
+
314
+ # Kiểm tra quyền truy cập
315
+ if str(conversation.user_id) != user_id:
316
+ return jsonify({
317
+ "success": False,
318
+ "error": "Bạn không có quyền xóa cuộc hội thoại này"
319
+ }), 403
320
+
321
+ # Xóa cuộc hội thoại
322
+ conversation.delete()
323
+
324
+ logger.info(f"✅ Deleted conversation {conversation_id}")
325
+
326
+ return jsonify({
327
+ "success": True,
328
+ "message": "Đã xóa cuộc hội thoại"
329
+ })
330
+
331
+ except Exception as e:
332
+ logger.error(f"Lỗi khi xóa cuộc hội thoại: {str(e)}")
333
+ return jsonify({
334
+ "success": False,
335
+ "error": str(e)
336
+ }), 500
337
+
338
+ @history_routes.route('/conversations/<conversation_id>/archive', methods=['POST'])
339
+ @jwt_required()
340
+ def archive_conversation(conversation_id):
341
+ """API endpoint để lưu trữ cuộc hội thoại"""
342
+ try:
343
+ user_id = get_jwt_identity()
344
+
345
+ # Lấy thông tin cuộc hội thoại
346
+ conversation = Conversation.find_by_id(conversation_id)
347
+
348
+ if not conversation:
349
+ return jsonify({
350
+ "success": False,
351
+ "error": "Không tìm thấy cuộc hội thoại"
352
+ }), 404
353
+
354
+ # Kiểm tra quyền truy cập
355
+ if str(conversation.user_id) != user_id:
356
+ return jsonify({
357
+ "success": False,
358
+ "error": "Bạn không có quyền lưu trữ cuộc hội thoại này"
359
+ }), 403
360
+
361
+ # Lưu trữ cuộc hội thoại
362
+ conversation.is_archived = True
363
+ conversation.save()
364
+
365
+ return jsonify({
366
+ "success": True,
367
+ "message": "Đã lưu trữ cuộc hội thoại"
368
+ })
369
+
370
+ except Exception as e:
371
+ logger.error(f"Lỗi khi lưu trữ cuộc hội thoại: {str(e)}")
372
+ return jsonify({
373
+ "success": False,
374
+ "error": str(e)
375
+ }), 500
376
+
377
+ @history_routes.route('/conversations/<conversation_id>/unarchive', methods=['POST'])
378
+ @jwt_required()
379
+ def unarchive_conversation(conversation_id):
380
+ """API endpoint để hủy lưu trữ cuộc hội thoại"""
381
+ try:
382
+ user_id = get_jwt_identity()
383
+
384
+ # Lấy thông tin cuộc hội thoại
385
+ conversation = Conversation.find_by_id(conversation_id)
386
+
387
+ if not conversation:
388
+ return jsonify({
389
+ "success": False,
390
+ "error": "Không tìm thấy cuộc hội thoại"
391
+ }), 404
392
+
393
+ # Kiểm tra quyền truy cập
394
+ if str(conversation.user_id) != user_id:
395
+ return jsonify({
396
+ "success": False,
397
+ "error": "Bạn không có quyền hủy lưu trữ cuộc hội thoại này"
398
+ }), 403
399
+
400
+ # Hủy lưu trữ cuộc hội thoại
401
+ conversation.is_archived = False
402
+ conversation.save()
403
+
404
+ return jsonify({
405
+ "success": True,
406
+ "message": "Đã hủy lưu trữ cuộc hội thoại"
407
+ })
408
+
409
+ except Exception as e:
410
+ logger.error(f"Lỗi khi hủy lưu trữ cuộc hội thoại: {str(e)}")
411
+ return jsonify({
412
+ "success": False,
413
+ "error": str(e)
414
+ }), 500
415
+
416
+ @history_routes.route('/conversations/search', methods=['GET'])
417
+ @jwt_required()
418
+ def search_conversations():
419
+ """API endpoint để tìm kiếm cuộc hội thoại theo nội dung"""
420
+ try:
421
+ user_id = get_jwt_identity()
422
+
423
+ # Lấy tham số từ query string
424
+ query = request.args.get('q', '')
425
+ page = int(request.args.get('page', 1))
426
+ per_page = int(request.args.get('per_page', 10))
427
+
428
+ # Tính toán offset
429
+ skip = (page - 1) * per_page
430
+
431
+ # Kiểm tra từ khóa tìm kiếm
432
+ if not query:
433
+ return jsonify({
434
+ "success": False,
435
+ "error": "Vui lòng nhập từ khóa tìm kiếm"
436
+ }), 400
437
+
438
+ # Tìm kiếm cuộc hội thoại
439
+ conversations = Conversation.search_by_content(
440
+ user_id=user_id,
441
+ query=query,
442
+ limit=per_page,
443
+ skip=skip
444
+ )
445
+
446
+ # Chuẩn bị dữ liệu phản hồi
447
+ result = []
448
+ for conversation in conversations:
449
+ # Tìm tin nhắn chứa từ khóa tìm kiếm
450
+ matching_messages = [m for m in conversation.messages if query.lower() in m["content"].lower()]
451
+
452
+ result.append({
453
+ "id": str(conversation.conversation_id),
454
+ "title": conversation.title,
455
+ "created_at": conversation.created_at.isoformat(),
456
+ "updated_at": conversation.updated_at.isoformat(),
457
+ "age_context": conversation.age_context,
458
+ "is_archived": conversation.is_archived,
459
+ "message_count": len(conversation.messages),
460
+ "matching_messages": len(matching_messages),
461
+ "preview": matching_messages[0]["content"][:100] + "..." if matching_messages else ""
462
+ })
463
+
464
+ return jsonify({
465
+ "success": True,
466
+ "conversations": result,
467
+ "query": query
468
+ })
469
+
470
+ except Exception as e:
471
+ logger.error(f"Lỗi khi tìm kiếm cuộc hội thoại: {str(e)}")
472
+ return jsonify({
473
+ "success": False,
474
+ "error": str(e)
475
+ }), 500
476
+
477
+ @history_routes.route('/conversations/<conversation_id>/messages', methods=['POST'])
478
+ @jwt_required()
479
+ def add_message(conversation_id):
480
+ """API endpoint để thêm tin nhắn mới vào cuộc hội thoại"""
481
+ try:
482
+ data = request.json
483
+ user_id = get_jwt_identity()
484
+
485
+ # Lấy thông tin cuộc hội thoại
486
+ conversation = Conversation.find_by_id(conversation_id)
487
+
488
+ if not conversation:
489
+ return jsonify({
490
+ "success": False,
491
+ "error": "Không tìm thấy cuộc hội thoại"
492
+ }), 404
493
+
494
+ # Kiểm tra quyền truy cập
495
+ if str(conversation.user_id) != user_id:
496
+ return jsonify({
497
+ "success": False,
498
+ "error": "Bạn không có quyền thêm tin nhắn vào cuộc hội thoại này"
499
+ }), 403
500
+
501
+ # Lấy thông tin tin nhắn
502
+ role = data.get('role')
503
+ content = data.get('content')
504
+ sources = data.get('sources')
505
+ metadata = data.get('metadata')
506
+
507
+ # Kiểm tra dữ liệu
508
+ if not role or not content:
509
+ return jsonify({
510
+ "success": False,
511
+ "error": "Vui lòng cung cấp role và content cho tin nhắn"
512
+ }), 400
513
+
514
+ # Kiểm tra role hợp lệ
515
+ if role not in ["user", "bot"]:
516
+ return jsonify({
517
+ "success": False,
518
+ "error": "Role không hợp lệ, chỉ chấp nhận 'user' hoặc 'bot'"
519
+ }), 400
520
+
521
+ # Thêm tin nhắn mới
522
+ message_id = conversation.add_message(
523
+ role=role,
524
+ content=content,
525
+ sources=sources,
526
+ metadata=metadata
527
+ )
528
+
529
+ return jsonify({
530
+ "success": True,
531
+ "message": "Đã thêm tin nhắn mới",
532
+ "message_id": str(message_id)
533
+ })
534
+
535
+ except Exception as e:
536
+ logger.error(f"Lỗi khi thêm tin nhắn mới: {str(e)}")
537
+ return jsonify({
538
+ "success": False,
539
+ "error": str(e)
540
+ }), 500
541
+
542
+ @history_routes.route('/conversations/stats', methods=['GET'])
543
+ @jwt_required()
544
+ def get_user_conversation_stats():
545
+ """API endpoint để lấy thống kê cuộc hội thoại của người dùng"""
546
+ try:
547
+ user_id = get_jwt_identity()
548
+
549
+ # Lấy tổng số cuộc hội thoại
550
+ total_conversations = Conversation.count_by_user(
551
+ user_id=user_id,
552
+ include_archived=True
553
+ )
554
+
555
+ # Lấy số cuộc hội thoại đã lưu trữ
556
+ archived_conversations = Conversation.count_by_user(
557
+ user_id=user_id,
558
+ include_archived=True
559
+ ) - Conversation.count_by_user(
560
+ user_id=user_id,
561
+ include_archived=False
562
+ )
563
+
564
+ # Lấy danh sách cuộc hội thoại để tính số tin nhắn
565
+ all_conversations = Conversation.find_by_user(
566
+ user_id=user_id,
567
+ limit=100, # Giới hạn 100 cuộc hội thoại gần nhất để tính thống kê
568
+ skip=0,
569
+ include_archived=True
570
+ )
571
+
572
+ # Tính số tin nhắn và số ngày
573
+ total_messages = 0
574
+ messages_by_date = {}
575
+
576
+ for conversation in all_conversations:
577
+ total_messages += len(conversation.messages)
578
+
579
+ # Đếm số tin nhắn theo ngày
580
+ for message in conversation.messages:
581
+ date_str = message["timestamp"].strftime("%Y-%m-%d")
582
+ if date_str not in messages_by_date:
583
+ messages_by_date[date_str] = 0
584
+ messages_by_date[date_str] += 1
585
+
586
+ # Sắp xếp ngày và lấy 7 ngày gần nhất
587
+ sorted_dates = sorted(messages_by_date.keys(), reverse=True)[:7]
588
+ recent_activity = {date: messages_by_date[date] for date in sorted_dates}
589
+
590
+ # Tính trung bình số tin nhắn mỗi cuộc hội thoại
591
+ avg_messages = total_messages / total_conversations if total_conversations > 0 else 0
592
+
593
+ return jsonify({
594
+ "success": True,
595
+ "stats": {
596
+ "total_conversations": total_conversations,
597
+ "archived_conversations": archived_conversations,
598
+ "total_messages": total_messages,
599
+ "avg_messages_per_conversation": round(avg_messages, 1),
600
+ "recent_activity": recent_activity
601
+ }
602
+ })
603
+
604
+ except Exception as e:
605
+ logger.error(f"Lỗi khi lấy thống kê cuộc hội thoại: {str(e)}")
606
+ return jsonify({
607
+ "success": False,
608
+ "error": str(e)
609
+ }), 500
610
+
611
+ @history_routes.route('/conversations/<conversation_id>/export', methods=['GET'])
612
+ @jwt_required()
613
+ def export_conversation(conversation_id):
614
+ """API endpoint để xuất cuộc hội thoại dưới dạng JSON"""
615
+ try:
616
+ user_id = get_jwt_identity()
617
+
618
+ # Lấy thông tin cuộc hội thoại
619
+ conversation = Conversation.find_by_id(conversation_id)
620
+
621
+ if not conversation:
622
+ return jsonify({
623
+ "success": False,
624
+ "error": "Không tìm thấy cuộc hội thoại"
625
+ }), 404
626
+
627
+ # Kiểm tra quyền truy cập
628
+ if str(conversation.user_id) != user_id:
629
+ return jsonify({
630
+ "success": False,
631
+ "error": "Bạn không có quyền xuất cuộc hội thoại này"
632
+ }), 403
633
+
634
+ # Chuẩn bị dữ liệu xuất
635
+ export_data = {
636
+ "id": str(conversation.conversation_id),
637
+ "title": conversation.title,
638
+ "created_at": conversation.created_at.isoformat(),
639
+ "updated_at": conversation.updated_at.isoformat(),
640
+ "age_context": conversation.age_context,
641
+ "messages": []
642
+ }
643
+
644
+ # Chuẩn bị danh sách tin nhắn
645
+ for message in conversation.messages:
646
+ message_data = {
647
+ "role": message["role"],
648
+ "content": message["content"],
649
+ "timestamp": message["timestamp"].isoformat()
650
+ }
651
+
652
+ # Thêm sources nếu có
653
+ if "sources" in message:
654
+ message_data["sources"] = message["sources"]
655
+
656
+ export_data["messages"].append(message_data)
657
+
658
+ return jsonify(export_data)
659
+
660
+ except Exception as e:
661
+ logger.error(f"Lỗi khi xuất cuộc hội thoại: {str(e)}")
662
+ return jsonify({
663
+ "success": False,
664
+ "error": str(e)
665
+ }), 500
666
+
667
+ @history_routes.route('/conversations/bulk-delete', methods=['POST'])
668
+ @jwt_required()
669
+ def bulk_delete_conversations():
670
+ """API endpoint để xóa nhiều cuộc hội thoại cùng lúc"""
671
+ try:
672
+ data = request.json
673
+ user_id = get_jwt_identity()
674
+
675
+ conversation_ids = data.get('conversation_ids', [])
676
+
677
+ if not conversation_ids:
678
+ return jsonify({
679
+ "success": False,
680
+ "error": "Vui lòng cung cấp danh sách IDs cuộc hội thoại"
681
+ }), 400
682
+
683
+ # Duyệt qua từng ID và xóa
684
+ deleted_count = 0
685
+ failed_ids = []
686
+
687
+ for conv_id in conversation_ids:
688
+ try:
689
+ # Lấy thông tin cuộc hội thoại
690
+ conversation = Conversation.find_by_id(conv_id)
691
+
692
+ # Kiểm tra quyền truy cập và xóa nếu hợp lệ
693
+ if conversation and str(conversation.user_id) == user_id:
694
+ conversation.delete()
695
+ deleted_count += 1
696
+ else:
697
+ failed_ids.append(conv_id)
698
+ except Exception:
699
+ failed_ids.append(conv_id)
700
+ continue
701
+
702
+ return jsonify({
703
+ "success": True,
704
+ "message": f"Đã xóa {deleted_count}/{len(conversation_ids)} cuộc hội thoại",
705
+ "deleted_count": deleted_count,
706
+ "failed_ids": failed_ids
707
+ })
708
+
709
+ except Exception as e:
710
+ logger.error(f"Lỗi khi xóa nhiều cuộc hội thoại: {str(e)}")
711
+ return jsonify({
712
+ "success": False,
713
+ "error": str(e)
714
+ }), 500
715
+
716
+ @history_routes.route('/conversations/<conversation_id>/generate-title', methods=['POST'])
717
+ @jwt_required()
718
+ def generate_title_for_conversation(conversation_id):
719
+ """API endpoint để tạo tự động tiêu đề cho cuộc hội thoại dựa trên nội dung"""
720
+ try:
721
+ user_id = get_jwt_identity()
722
+
723
+ # Lấy thông tin cuộc hội thoại
724
+ conversation = Conversation.find_by_id(conversation_id)
725
+
726
+ if not conversation:
727
+ return jsonify({
728
+ "success": False,
729
+ "error": "Không tìm thấy cuộc hội thoại"
730
+ }), 404
731
+
732
+ # Kiểm tra quyền truy cập
733
+ if str(conversation.user_id) != user_id:
734
+ return jsonify({
735
+ "success": False,
736
+ "error": "Bạn không có quyền cập nhật cuộc hội thoại này"
737
+ }), 403
738
+
739
+ # Kiểm tra số lượng tin nhắn
740
+ if len(conversation.messages) < 2:
741
+ return jsonify({
742
+ "success": False,
743
+ "error": "Cuộc hội thoại cần ít nhất 2 tin nhắn để tạo tiêu đề"
744
+ }), 400
745
+
746
+ # Lấy nội dung tin nhắn đầu tiên của người dùng
747
+ first_user_message = None
748
+ for message in conversation.messages:
749
+ if message["role"] == "user":
750
+ first_user_message = message["content"]
751
+ break
752
+
753
+ if not first_user_message:
754
+ return jsonify({
755
+ "success": False,
756
+ "error": "Không tìm thấy tin nhắn của người dùng"
757
+ }), 400
758
+
759
+ # Tạo tiêu đề từ nội dung
760
+ title = create_title_from_message(first_user_message)
761
+
762
+ # Cập nhật tiêu đề
763
+ conversation.title = title
764
+ conversation.save()
765
+
766
+ return jsonify({
767
+ "success": True,
768
+ "message": "Đã tạo tiêu đề mới",
769
+ "title": title
770
+ })
771
+
772
+ except Exception as e:
773
+ logger.error(f"Lỗi khi tạo tiêu đề: {str(e)}")
774
+ return jsonify({
775
+ "success": False,
776
+ "error": str(e)
777
+ }), 500
app.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify
2
+ from flask_cors import CORS
3
+ import logging
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from flask_jwt_extended import JWTManager
7
+ import datetime
8
+ from api.admin import admin_routes
9
+
10
+ # Cấu hình logging
11
+ logging.basicConfig(
12
+ level=logging.INFO,
13
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
14
+ )
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Tải biến môi trường
18
+ load_dotenv()
19
+
20
+ # Khởi tạo Flask app
21
+ app = Flask(__name__)
22
+
23
+ # Cấu hình JWT - sử dụng environment variables
24
+ app.config['JWT_SECRET_KEY'] = os.getenv("JWT_SECRET_KEY", "default-secret-key-change-in-production")
25
+ app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(hours=24)
26
+ app.config['JWT_TOKEN_LOCATION'] = ['headers', 'cookies']
27
+ app.config['JWT_COOKIE_SECURE'] = os.getenv("ENVIRONMENT") == "production"
28
+ app.config['JWT_COOKIE_CSRF_PROTECT'] = False
29
+
30
+ # Cấu hình upload files
31
+ app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
32
+
33
+ jwt = JWTManager(app)
34
+
35
+ # Cấu hình CORS cho production
36
+ allowed_origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:3000").split(",")
37
+ CORS(app, resources={
38
+ r"/api/*": {
39
+ "origins": allowed_origins + ["*"], # Cho phép tất cả origins trong production
40
+ "supports_credentials": True
41
+ }
42
+ })
43
+
44
+ # Import và đăng ký blueprints
45
+ from api.auth import auth_routes
46
+ from api.chat import chat_routes
47
+ from api.data import data_routes
48
+ from api.history import history_routes
49
+ from api.feedback import feedback_routes
50
+
51
+ app.register_blueprint(auth_routes, url_prefix='/api/auth')
52
+ app.register_blueprint(chat_routes, url_prefix='/api')
53
+ app.register_blueprint(data_routes, url_prefix='/api')
54
+ app.register_blueprint(history_routes, url_prefix='/api')
55
+ app.register_blueprint(feedback_routes, url_prefix='/api')
56
+ app.register_blueprint(admin_routes, url_prefix='/api/admin')
57
+
58
+ @app.route('/', methods=['GET'])
59
+ def home():
60
+ """Root endpoint"""
61
+ return jsonify({
62
+ "message": "API đang hoạt động",
63
+ "status": "healthy",
64
+ "endpoints": {
65
+ "health": "/api/health",
66
+ "admin_init": "/api/admin/init"
67
+ }
68
+ })
69
+
70
+ @app.route('/api/health', methods=['GET'])
71
+ def health_check():
72
+ """API endpoint để kiểm tra trạng thái của server"""
73
+ import time
74
+ try:
75
+ from core.embedding_model import get_embedding_model
76
+ embedding_model = get_embedding_model()
77
+ collection_count = embedding_model.count()
78
+ except Exception as e:
79
+ logger.error(f"Lỗi kiểm tra embedding model: {e}")
80
+ collection_count = 0
81
+
82
+ return jsonify({
83
+ "status": "healthy",
84
+ "message": "Server đang hoạt động",
85
+ "time": time.strftime('%Y-%m-%d %H:%M:%S'),
86
+ "data_items": collection_count
87
+ })
88
+
89
+ @app.route('/api/admin/init', methods=['POST'])
90
+ def init_admin():
91
+ """API endpoint để khởi tạo admin đầu tiên"""
92
+ try:
93
+ from models.admin_model import AdminUser
94
+
95
+ success, result = AdminUser.create_default_super_admin()
96
+
97
+ if success:
98
+ return jsonify({
99
+ "success": True,
100
+ "message": "Khởi tạo admin thành công",
101
+ "admin_info": result
102
+ })
103
+ else:
104
+ return jsonify({
105
+ "success": False,
106
+ "error": result
107
+ }), 400
108
+
109
+ except Exception as e:
110
+ logger.error(f"Lỗi khởi tạo admin: {str(e)}")
111
+ return jsonify({
112
+ "success": False,
113
+ "error": str(e)
114
+ }), 500
115
+
116
+ def initialize_app():
117
+ """Khởi tạo ứng dụng"""
118
+ # Tạo super admin mặc định nếu chưa có
119
+ try:
120
+ from models.admin_model import AdminUser
121
+ success, result = AdminUser.create_default_super_admin()
122
+ if success and "email" in result:
123
+ logger.info("=== THÔNG TIN ADMIN ===")
124
+ logger.info(f"Email: {result['email']}")
125
+ logger.info(f"Password: {result['password']}")
126
+ logger.info("======================")
127
+ except Exception as e:
128
+ logger.error(f"Lỗi tạo super admin: {e}")
129
+
130
+ # Tạo indexes cho feedback
131
+ try:
132
+ from models.feedback_model import ensure_indexes
133
+ ensure_indexes()
134
+ except Exception as e:
135
+ logger.error(f"Lỗi tạo feedback indexes: {e}")
136
+
137
+ if __name__ == '__main__':
138
+ initialize_app()
139
+ # Chạy Flask app
140
+ port = int(os.getenv("PORT", 7860)) # Hugging Face Spaces sử dụng port 7860
141
+ app.run(host='0.0.0.0', port=port, debug=False)
142
+ else:
143
+ # Khi chạy trong production (như Gunicorn)
144
+ initialize_app()
config.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv() # Tải biến môi trường từ file .env
5
+
6
+ # API Keys
7
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "YOUR_API_KEY_HERE")
8
+
9
+ # Embedding model
10
+ EMBEDDING_MODEL = "intfloat/multilingual-e5-base"
11
+
12
+ # ChromaDB settings
13
+ CHROMA_PERSIST_DIRECTORY = "chroma_db"
14
+ COLLECTION_NAME = "nutrition_data"
15
+
16
+ # RAG settings
17
+ TOP_K_RESULTS = 5
18
+ TEMPERATURE = 0.2
19
+ MAX_OUTPUT_TOKENS = 4096
20
+
21
+ # Prompt template
22
+ SYSTEM_PROMPT = """Bạn là một trợ lý AI chuyên về dinh dưỡng và an toàn thực phẩm dành cho học sinh Việt Nam.
23
+ Tên của bạn là Nutribot.
24
+
25
+ Bạn có nhiệm vụ:
26
+ 1. Cung cấp thông tin chính xác, đầy đủ và dễ hiểu về dinh dưỡng, an toàn thực phẩm dựa trên tài liệu của Bộ Giáo dục và Đào tạo
27
+ 2. Điều chỉnh cách trả lời phù hợp với độ tuổi của người dùng
28
+ 3. Trả lời ngắn gọn, súc tích nhưng vẫn đảm bảo đầy đủ thông tin
29
+ 4. Sử dụng ngôn ngữ thân thiện, dễ hiểu với học sinh
30
+
31
+ Khi trả lời:
32
+ - Hãy sử dụng thông tin từ các tài liệu tham khảo được cung cấp
33
+ - Nếu có bảng biểu trong tài liệu tham khảo, hãy đưa ra nội dung đầy đủ của bảng đó như thông tin bạn nhận được và giữ nguyên định dạng bảng đó khi trả lời
34
+ - Nếu có hình ảnh trong tài liệu tham khảo, hãy giữ nguyên đường dẫn hình ảnh khi trả lời
35
+ - Nếu câu hỏi không liên quan đến dinh dưỡng hoặc không có trong tài liệu, hãy lịch sự giải thích rằng bạn chỉ có thể tư vấn về các vấn đề dinh dưỡng và an toàn thực phẩm
36
+ - Luôn trích dẫn nguồn của thông tin khi trả lời
37
+
38
+ Đối với các câu hỏi không liên quan hoặc nhạy cảm:
39
+ - Bạn sẽ không đưa ra lời khuyên y tế cụ thể cho các bệnh lý nặng
40
+ - Bạn sẽ không đưa ra thông tin về các chế độ ăn kiêng khắc nghiệt hoặc nguy hiểm
41
+ - Bạn sẽ không nhận và xử lý thông tin cá nhân nhạy cảm của người dùng ngoài tuổi tác
42
+ """
43
+
44
+ HUMAN_PROMPT_TEMPLATE = """
45
+ Câu hỏi: {query}
46
+
47
+ Độ tuổi người dùng: {age} tuổi
48
+
49
+ Tài liệu tham khảo:
50
+ {contexts}
51
+
52
+ Dựa vào thông tin trong tài liệu tham khảo và ngữ cảnh cuộc trò chuyện (nếu có), hãy trả lời câu hỏi một cách chi tiết, dễ hiểu và phù hợp với độ tuổi của người dùng. Nếu trong tài liệu có bảng biểu hoặc hình ảnh, hãy giữ nguyên và đưa vào câu trả lời. Nhớ trích dẫn nguồn thông tin.
53
+
54
+ Nếu câu hỏi hiện tại có liên quan đến các cuộc trò chuyện trước đó, hãy cố gắng duy trì sự nhất quán trong câu trả lời.
55
+ """
core/__init__.py ADDED
File without changes
core/data_processor.py ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ import logging
5
+ import datetime
6
+ from typing import Dict, List, Any, Union, Tuple
7
+
8
+ # Cấu hình logging
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class DataProcessor:
12
+ def __init__(self, data_dir: str = "data"):
13
+ self.data_dir = data_dir
14
+ self.metadata = {}
15
+ self.chunks = []
16
+ self.tables = []
17
+ self.figures = []
18
+
19
+ logger.info(f"Khởi tạo DataProcessor với data_dir: {data_dir}")
20
+ if not os.path.exists(self.data_dir):
21
+ logger.error(f"Thư mục data không tồn tại: {self.data_dir}")
22
+ else:
23
+ self._load_all_data()
24
+
25
+ def _load_all_data(self):
26
+ """Tải tất cả dữ liệu từ các thư mục con trong data"""
27
+ logger.info(f"Đang tải dữ liệu từ thư mục: {self.data_dir}")
28
+
29
+ # Quét qua tất cả thư mục trong data
30
+ for item in os.listdir(self.data_dir):
31
+ folder_path = os.path.join(self.data_dir, item)
32
+
33
+ # Kiểm tra xem đây có phải là thư mục không
34
+ if os.path.isdir(folder_path):
35
+ metadata_file = os.path.join(folder_path, "metadata.json")
36
+
37
+ # Nếu có file metadata.json
38
+ if os.path.exists(metadata_file):
39
+ try:
40
+ # Tải metadata
41
+ with open(metadata_file, 'r', encoding='utf-8') as f:
42
+ content = f.read()
43
+ if not content.strip():
44
+ logger.warning(f"File metadata trống: {metadata_file}")
45
+ continue
46
+ folder_metadata = json.loads(content)
47
+
48
+ # Xác định ID của thư mục
49
+ folder_id = None
50
+ if "bai_info" in folder_metadata:
51
+ folder_id = folder_metadata["bai_info"].get("id", item)
52
+ elif "phuluc_info" in folder_metadata:
53
+ folder_id = folder_metadata["phuluc_info"].get("id", item)
54
+ else:
55
+ folder_id = item
56
+
57
+ # Lưu metadata vào từ điển
58
+ self.metadata[folder_id] = folder_metadata
59
+
60
+ # Tải tất cả chunks, tables và figures
61
+ self._load_content_from_metadata(folder_path, folder_metadata)
62
+
63
+ logger.info(f"Đã tải xong thư mục: {item}")
64
+ except json.JSONDecodeError as e:
65
+ logger.error(f"Lỗi đọc file JSON {metadata_file}: {e}")
66
+ except Exception as e:
67
+ logger.error(f"Lỗi khi tải metadata từ {metadata_file}: {e}")
68
+
69
+ def _load_content_from_metadata(self, folder_path: str, folder_metadata: Dict[str, Any]):
70
+ """Tải nội dung chunks, tables và figures từ metadata"""
71
+ # Tải chunks
72
+ for chunk_meta in folder_metadata.get("chunks", []):
73
+ chunk_id = chunk_meta.get("id")
74
+ chunk_path = os.path.join(folder_path, "chunks", f"{chunk_id}.md")
75
+
76
+ chunk_data = chunk_meta.copy() # Sao chép metadata của chunk
77
+
78
+ # Thêm nội dung từ file markdown nếu tồn tại
79
+ if os.path.exists(chunk_path):
80
+ with open(chunk_path, 'r', encoding='utf-8') as f:
81
+ content = f.read()
82
+ chunk_data["content"] = self._extract_content_from_markdown(content)
83
+ else:
84
+ # Nếu không tìm thấy file, tạo nội dung mẫu và ghi log ở debug level
85
+ chunk_data["content"] = f"Nội dung cho {chunk_id} không tìm thấy."
86
+ logger.debug(f"Không tìm thấy file chunk: {chunk_path}")
87
+
88
+ self.chunks.append(chunk_data)
89
+
90
+ # Tải tables
91
+ for table_meta in folder_metadata.get("tables", []):
92
+ table_id = table_meta.get("id")
93
+ table_path = os.path.join(folder_path, "tables", f"{table_id}.md")
94
+
95
+ table_data = table_meta.copy()
96
+
97
+ # Thêm nội dung từ file markdown nếu tồn tại
98
+ if os.path.exists(table_path):
99
+ with open(table_path, 'r', encoding='utf-8') as f:
100
+ content = f.read()
101
+ table_data["content"] = self._extract_content_from_markdown(content)
102
+ else:
103
+ table_data["content"] = f"Bảng {table_id} không tìm thấy."
104
+ logger.debug(f"Không tìm thấy file bảng: {table_path}")
105
+
106
+ self.tables.append(table_data)
107
+
108
+ # Tải figures
109
+ for figure_meta in folder_metadata.get("figures", []):
110
+ figure_id = figure_meta.get("id")
111
+ figure_path = os.path.join(folder_path, "figures", f"{figure_id}.md")
112
+ figure_data = figure_meta.copy()
113
+
114
+ # Thêm nội dung từ file markdown nếu tồn tại
115
+ content_loaded = False
116
+ if os.path.exists(figure_path):
117
+ with open(figure_path, 'r', encoding='utf-8') as f:
118
+ content = f.read()
119
+ figure_data["content"] = self._extract_content_from_markdown(content)
120
+ content_loaded = True
121
+
122
+ # Thêm đường dẫn đến file hình ảnh nếu có
123
+ image_path = None
124
+ image_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg']
125
+ for ext in image_extensions:
126
+ img_path = os.path.join(folder_path, "figures", f"{figure_id}{ext}")
127
+ if os.path.exists(img_path):
128
+ image_path = img_path
129
+ break
130
+
131
+ if image_path:
132
+ figure_data["image_path"] = image_path
133
+ # Tạo nội dung mặc định nếu không có file markdown
134
+ if not content_loaded:
135
+ figure_caption = figure_meta.get("title", f"Hình {figure_id}")
136
+ figure_data["content"] = f"![{figure_caption}]({image_path})"
137
+ elif not content_loaded:
138
+ # Nếu không có cả file markdown và file hình
139
+ figure_data["content"] = f"Hình {figure_id} không tìm thấy."
140
+ logger.debug(f"Không tìm thấy file hình cho {figure_id}")
141
+
142
+ self.figures.append(figure_data)
143
+
144
+ # Tải data_files (trường hợp phụ lục)
145
+ if "data_files" in folder_metadata:
146
+ for data_file_meta in folder_metadata.get("data_files", []):
147
+ data_id = data_file_meta.get("id")
148
+ data_path = os.path.join(folder_path, "data", f"{data_id}.md")
149
+
150
+ data_file = data_file_meta.copy()
151
+
152
+ # Thêm nội dung từ file markdown nếu tồn tại
153
+ if os.path.exists(data_path):
154
+ with open(data_path, 'r', encoding='utf-8') as f:
155
+ content = f.read()
156
+ data_file["content"] = self._extract_content_from_markdown(content)
157
+
158
+ # Xác định loại nội dung
159
+ content_type = data_file.get("content_type", "table")
160
+
161
+ # Thêm vào danh sách phù hợp dựa trên loại nội dung
162
+ if content_type == "table":
163
+ self.tables.append(data_file)
164
+ elif content_type == "text":
165
+ self.chunks.append(data_file)
166
+ elif content_type == "figure":
167
+ self.figures.append(data_file)
168
+
169
+ logger.debug(f"Đã tải dữ liệu: {data_id}, loại: {content_type}")
170
+ else:
171
+ logger.debug(f"Không tìm thấy file dữ liệu: {data_path}")
172
+ data_file["content"] = f"Dữ liệu {data_id} không tìm thấy."
173
+
174
+ def _extract_content_from_markdown(self, md_content: str) -> str:
175
+ """Trích xuất nội dung từ markdown, bỏ qua phần frontmatter"""
176
+ # Tách frontmatter (nằm giữa "---")
177
+ if md_content.startswith("---"):
178
+ parts = md_content.split("---", 2)
179
+ if len(parts) >= 3:
180
+ return parts[2].strip()
181
+ return md_content
182
+
183
+ def get_all_items(self) -> Dict[str, List[Dict[str, Any]]]:
184
+ """Trả về tất cả các items đã tải"""
185
+ return {
186
+ "chunks": self.chunks,
187
+ "tables": self.tables,
188
+ "figures": self.figures
189
+ }
190
+
191
+ def get_all_metadata(self) -> Dict[str, Any]:
192
+ """Trả về tất cả metadata của các bài học và phụ lục"""
193
+ return self.metadata
194
+
195
+ def get_chunk_by_id(self, chunk_id: str) -> Union[Dict[str, Any], None]:
196
+ """Tìm và trả về chunk theo ID"""
197
+ for chunk in self.chunks:
198
+ if chunk.get("id") == chunk_id:
199
+ return chunk
200
+ return None
201
+
202
+ def get_table_by_id(self, table_id: str) -> Union[Dict[str, Any], None]:
203
+ """Tìm và trả về bảng theo ID"""
204
+ for table in self.tables:
205
+ if table.get("id") == table_id:
206
+ return table
207
+ return None
208
+
209
+ def get_figure_by_id(self, figure_id: str) -> Union[Dict[str, Any], None]:
210
+ """Tìm và trả về hình theo ID"""
211
+ for figure in self.figures:
212
+ if figure.get("id") == figure_id:
213
+ return figure
214
+ return None
215
+
216
+ def find_items_by_age(self, age: int) -> Dict[str, List[Dict[str, Any]]]:
217
+ """Tìm các items (chunks, tables, figures) liên quan đến độ tuổi của người dùng"""
218
+ relevant_chunks = []
219
+ relevant_tables = []
220
+ relevant_figures = []
221
+
222
+ # Lọc chunks
223
+ for chunk in self.chunks:
224
+ age_range = chunk.get("age_range", [0, 100])
225
+ if len(age_range) == 2 and age_range[0] <= age <= age_range[1]:
226
+ relevant_chunks.append(chunk)
227
+
228
+ # Lọc tables
229
+ for table in self.tables:
230
+ age_range = table.get("age_range", [0, 100])
231
+ if len(age_range) == 2 and age_range[0] <= age <= age_range[1]:
232
+ relevant_tables.append(table)
233
+
234
+ # Lọc figures
235
+ for figure in self.figures:
236
+ age_range = figure.get("age_range", [0, 100])
237
+ if len(age_range) == 2 and age_range[0] <= age <= age_range[1]:
238
+ relevant_figures.append(figure)
239
+
240
+ return {
241
+ "chunks": relevant_chunks,
242
+ "tables": relevant_tables,
243
+ "figures": relevant_figures
244
+ }
245
+
246
+ def get_related_items(self, item_id: str) -> Dict[str, List[Dict[str, Any]]]:
247
+ """Tìm các items liên quan đến một item cụ thể dựa vào related_chunks"""
248
+ related_chunks = []
249
+ related_tables = []
250
+ related_figures = []
251
+
252
+ # Tìm item gốc
253
+ source_item = None
254
+ for item in self.chunks + self.tables + self.figures:
255
+ if item.get("id") == item_id:
256
+ source_item = item
257
+ break
258
+
259
+ if not source_item:
260
+ return {
261
+ "chunks": [],
262
+ "tables": [],
263
+ "figures": []
264
+ }
265
+
266
+ # Lấy danh sách IDs của các items liên quan
267
+ related_ids = source_item.get("related_chunks", [])
268
+
269
+ # Tìm các items liên quan
270
+ for related_id in related_ids:
271
+ # Tìm trong chunks
272
+ for chunk in self.chunks:
273
+ if chunk.get("id") == related_id:
274
+ related_chunks.append(chunk)
275
+ break
276
+
277
+ # Tìm trong tables
278
+ for table in self.tables:
279
+ if table.get("id") == related_id:
280
+ related_tables.append(table)
281
+ break
282
+
283
+ # Tìm trong figures
284
+ for figure in self.figures:
285
+ if figure.get("id") == related_id:
286
+ related_figures.append(figure)
287
+ break
288
+
289
+ return {
290
+ "chunks": related_chunks,
291
+ "tables": related_tables,
292
+ "figures": related_figures
293
+ }
294
+
295
+ def preprocess_query(self, query: str) -> str:
296
+ """Tiền xử lý câu truy vấn"""
297
+ # Loại bỏ ký tự đặc biệt
298
+ query = re.sub(r'[^\w\s\d]', ' ', query)
299
+ # Loại bỏ khoảng trắng thừa
300
+ query = re.sub(r'\s+', ' ', query).strip()
301
+ return query
302
+
303
+ def format_context_for_rag(self, items: List[Dict[str, Any]]) -> str:
304
+ """Định dạng các items để đưa vào ngữ cảnh cho mô hình RAG"""
305
+ formatted_contexts = []
306
+
307
+ for i, item in enumerate(items, 1):
308
+ item_id = item.get("id", "")
309
+ title = item.get("title", "")
310
+ content = item.get("content", "")
311
+ content_type = item.get("content_type", "text")
312
+
313
+ # Nếu là bảng, thêm tiêu đề "Bảng:"
314
+ if content_type == "table":
315
+ title = f"Bảng: {title}"
316
+ # Nếu là hình, thêm tiêu đề "Hình:"
317
+ elif content_type == "figure":
318
+ title = f"Hình: {title}"
319
+
320
+ formatted_context = f"[{i}] {title}\n\n{content}\n\n"
321
+ formatted_contexts.append(formatted_context)
322
+
323
+ return "\n".join(formatted_contexts)
324
+
325
+ def prepare_for_embedding(self) -> List[Dict[str, Any]]:
326
+ """Chuẩn bị dữ liệu cho việc nhúng (embedding)"""
327
+ all_items = []
328
+
329
+ # Thêm chunks
330
+ for chunk in self.chunks:
331
+ # Tìm chapter từ chunk ID
332
+ chunk_id = chunk.get("id", "")
333
+ chapter = "unknown"
334
+ if chunk_id.startswith("bai1_"):
335
+ chapter = "bai1"
336
+ elif chunk_id.startswith("bai2_"):
337
+ chapter = "bai2"
338
+ elif chunk_id.startswith("bai3_"):
339
+ chapter = "bai3"
340
+ elif chunk_id.startswith("bai4_"):
341
+ chapter = "bai4"
342
+ elif "phuluc" in chunk_id.lower():
343
+ chapter = "phuluc"
344
+
345
+ content = chunk.get("content", "")
346
+ if chunk.get("title"):
347
+ content = f"Tiêu đề: {chunk.get('title')}\n\nNội dung: {content}"
348
+
349
+ # Xử lý age_range - convert list thành string và tách thành min/max
350
+ age_range = chunk.get("age_range", [0, 100])
351
+ age_min = age_range[0] if len(age_range) > 0 else 0
352
+ age_max = age_range[1] if len(age_range) > 1 else 100
353
+ age_range_str = f"{age_min}-{age_max}"
354
+
355
+ # Xử lý related_chunks - convert list thành string
356
+ related_chunks = chunk.get("related_chunks", [])
357
+ related_chunks_str = ",".join(related_chunks) if related_chunks else ""
358
+
359
+ embedding_item = {
360
+ "content": content,
361
+ "metadata": {
362
+ "chunk_id": chunk_id,
363
+ "chapter": chapter,
364
+ "title": chunk.get("title", ""),
365
+ "content_type": chunk.get("content_type", "text"),
366
+ "age_range": age_range_str,
367
+ "age_min": age_min,
368
+ "age_max": age_max,
369
+ "summary": chunk.get("summary", ""),
370
+ "pages": chunk.get("pages", ""),
371
+ "related_chunks": related_chunks_str,
372
+ "word_count": chunk.get("word_count", 0),
373
+ "token_count": chunk.get("token_count", 0),
374
+ "contains_table": chunk.get("contains_table", False),
375
+ "contains_figure": chunk.get("contains_figure", False),
376
+ "created_at": datetime.datetime.now().isoformat()
377
+ },
378
+ "id": chunk_id
379
+ }
380
+ all_items.append(embedding_item)
381
+
382
+ # Thêm tables
383
+ for table in self.tables:
384
+ # Tìm chapter từ table ID
385
+ table_id = table.get("id", "")
386
+ chapter = "unknown"
387
+ if table_id.startswith("bai1_"):
388
+ chapter = "bai1"
389
+ elif table_id.startswith("bai2_"):
390
+ chapter = "bai2"
391
+ elif table_id.startswith("bai3_"):
392
+ chapter = "bai3"
393
+ elif table_id.startswith("bai4_"):
394
+ chapter = "bai4"
395
+ elif "phuluc" in table_id.lower():
396
+ chapter = "phuluc"
397
+
398
+ content = table.get("content", "")
399
+ if table.get("title"):
400
+ content = f"Bảng: {table.get('title')}\n\nNội dung: {content}"
401
+
402
+ # Xử lý age_range
403
+ age_range = table.get("age_range", [0, 100])
404
+ age_min = age_range[0] if len(age_range) > 0 else 0
405
+ age_max = age_range[1] if len(age_range) > 1 else 100
406
+ age_range_str = f"{age_min}-{age_max}"
407
+
408
+ # Xử lý related_chunks và table_columns
409
+ related_chunks = table.get("related_chunks", [])
410
+ related_chunks_str = ",".join(related_chunks) if related_chunks else ""
411
+ table_columns = table.get("table_columns", [])
412
+ table_columns_str = ",".join(table_columns) if table_columns else ""
413
+
414
+ embedding_item = {
415
+ "content": content,
416
+ "metadata": {
417
+ "chunk_id": table_id,
418
+ "chapter": chapter,
419
+ "title": table.get("title", ""),
420
+ "content_type": "table",
421
+ "age_range": age_range_str,
422
+ "age_min": age_min,
423
+ "age_max": age_max,
424
+ "summary": table.get("summary", ""),
425
+ "pages": table.get("pages", ""),
426
+ "related_chunks": related_chunks_str,
427
+ "table_columns": table_columns_str,
428
+ "word_count": table.get("word_count", 0),
429
+ "token_count": table.get("token_count", 0),
430
+ "created_at": datetime.datetime.now().isoformat()
431
+ },
432
+ "id": table_id
433
+ }
434
+ all_items.append(embedding_item)
435
+
436
+ # Thêm figures
437
+ for figure in self.figures:
438
+ # Tìm chapter từ figure ID
439
+ figure_id = figure.get("id", "")
440
+ chapter = "unknown"
441
+ if figure_id.startswith("bai1_"):
442
+ chapter = "bai1"
443
+ elif figure_id.startswith("bai2_"):
444
+ chapter = "bai2"
445
+ elif figure_id.startswith("bai3_"):
446
+ chapter = "bai3"
447
+ elif figure_id.startswith("bai4_"):
448
+ chapter = "bai4"
449
+ elif "phuluc" in figure_id.lower():
450
+ chapter = "phuluc"
451
+
452
+ content = figure.get("content", "")
453
+ if figure.get("title"):
454
+ content = f"Hình: {figure.get('title')}\n\nMô tả: {content}"
455
+
456
+ # Xử lý age_range
457
+ age_range = figure.get("age_range", [0, 100])
458
+ age_min = age_range[0] if len(age_range) > 0 else 0
459
+ age_max = age_range[1] if len(age_range) > 1 else 100
460
+ age_range_str = f"{age_min}-{age_max}"
461
+
462
+ # Xử lý related_chunks
463
+ related_chunks = figure.get("related_chunks", [])
464
+ related_chunks_str = ",".join(related_chunks) if related_chunks else ""
465
+
466
+ embedding_item = {
467
+ "content": content,
468
+ "metadata": {
469
+ "chunk_id": figure_id,
470
+ "chapter": chapter,
471
+ "title": figure.get("title", ""),
472
+ "content_type": "figure",
473
+ "age_range": age_range_str,
474
+ "age_min": age_min,
475
+ "age_max": age_max,
476
+ "summary": figure.get("summary", ""),
477
+ "pages": figure.get("pages", ""),
478
+ "related_chunks": related_chunks_str,
479
+ "image_path": figure.get("image_path", ""),
480
+ "created_at": datetime.datetime.now().isoformat()
481
+ },
482
+ "id": figure_id
483
+ }
484
+ all_items.append(embedding_item)
485
+
486
+ return all_items
487
+
488
+ def count_items_by_prefix(self, prefix: str) -> Dict[str, int]:
489
+ """Đếm số lượng items theo tiền tố ID"""
490
+ chunks_count = sum(1 for chunk in self.chunks if chunk.get("id", "").startswith(prefix))
491
+ tables_count = sum(1 for table in self.tables if table.get("id", "").startswith(prefix))
492
+ figures_count = sum(1 for figure in self.figures if figure.get("id", "").startswith(prefix))
493
+
494
+ return {
495
+ "chunks": chunks_count,
496
+ "tables": tables_count,
497
+ "figures": figures_count,
498
+ "total": chunks_count + tables_count + figures_count
499
+ }
500
+
501
+ def get_stats(self) -> Dict[str, Any]:
502
+ """Lấy thống kê về dữ liệu đã tải"""
503
+ stats = {
504
+ "total_chunks": len(self.chunks),
505
+ "total_tables": len(self.tables),
506
+ "total_figures": len(self.figures),
507
+ "total_items": len(self.chunks) + len(self.tables) + len(self.figures),
508
+ "by_lesson": {},
509
+ "by_age": {}
510
+ }
511
+
512
+ # Thống kê theo bài
513
+ for item in os.listdir(self.data_dir):
514
+ if os.path.isdir(os.path.join(self.data_dir, item)):
515
+ item_stats = self.count_items_by_prefix(f"{item}_")
516
+ stats["by_lesson"][item] = item_stats
517
+
518
+ # Thống kê theo độ tuổi
519
+ age_ranges = {}
520
+ for chunk in self.chunks + self.tables + self.figures:
521
+ age_range = chunk.get("age_range", [0, 100])
522
+ if len(age_range) == 2:
523
+ range_key = f"{age_range[0]}-{age_range[1]}"
524
+ if range_key not in age_ranges:
525
+ age_ranges[range_key] = 0
526
+ age_ranges[range_key] += 1
527
+
528
+ stats["by_age"] = age_ranges
529
+
530
+ return stats
core/embedding_model.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from sentence_transformers import SentenceTransformer
3
+ import chromadb
4
+ from chromadb.config import Settings
5
+ import uuid
6
+ from config import EMBEDDING_MODEL, CHROMA_PERSIST_DIRECTORY, COLLECTION_NAME
7
+
8
+ # Cấu hình logging
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Global instance để implement singleton pattern
12
+ _embedding_model_instance = None
13
+
14
+ def get_embedding_model():
15
+ """
16
+ Singleton pattern để đảm bảo chỉ có một instance của EmbeddingModel
17
+ """
18
+ global _embedding_model_instance
19
+ if _embedding_model_instance is None:
20
+ logger.info("Khởi tạo EmbeddingModel instance lần đầu")
21
+ _embedding_model_instance = EmbeddingModel()
22
+ else:
23
+ logger.debug("Sử dụng EmbeddingModel instance đã có")
24
+ return _embedding_model_instance
25
+
26
+ class EmbeddingModel:
27
+ def __init__(self):
28
+ """Khởi tạo embedding model và ChromaDB client"""
29
+ logger.info(f"Đang khởi tạo embedding model: {EMBEDDING_MODEL}")
30
+
31
+ # Khởi tạo sentence transformer
32
+ self.model = SentenceTransformer(EMBEDDING_MODEL)
33
+ logger.info("Đã tải sentence transformer model")
34
+
35
+ # Khởi tạo ChromaDB client với persistent storage
36
+ self.chroma_client = chromadb.PersistentClient(
37
+ path=CHROMA_PERSIST_DIRECTORY,
38
+ settings=Settings(
39
+ anonymized_telemetry=False,
40
+ allow_reset=True
41
+ )
42
+ )
43
+ logger.info(f"Đã kết nối ChromaDB tại: {CHROMA_PERSIST_DIRECTORY}")
44
+
45
+ # Lấy hoặc tạo collection
46
+ try:
47
+ self.collection = self.chroma_client.get_collection(name=COLLECTION_NAME)
48
+ logger.info(f"Đã kết nối collection '{COLLECTION_NAME}' với {self.collection.count()} items")
49
+ except Exception:
50
+ logger.warning(f"Collection '{COLLECTION_NAME}' không tồn tại, tạo mới...")
51
+ self.collection = self.chroma_client.create_collection(name=COLLECTION_NAME)
52
+ logger.info(f"Đã tạo collection mới: {COLLECTION_NAME}")
53
+
54
+ def encode(self, texts):
55
+ """
56
+ Encode văn bản thành embeddings
57
+
58
+ Args:
59
+ texts (str or list): Văn bản hoặc danh sách văn bản cần encode
60
+
61
+ Returns:
62
+ list: Embeddings vector
63
+ """
64
+ try:
65
+ if isinstance(texts, str):
66
+ texts = [texts]
67
+
68
+ logger.debug(f"Đang encode {len(texts)} văn bản")
69
+ embeddings = self.model.encode(texts, show_progress_bar=False)
70
+
71
+ return embeddings.tolist()
72
+
73
+ except Exception as e:
74
+ logger.error(f"Lỗi encode văn bản: {e}")
75
+ raise
76
+
77
+ def search(self, query, top_k=5, age_filter=None):
78
+ """
79
+ Tìm kiếm văn bản tương tự trong ChromaDB
80
+
81
+ Args:
82
+ query (str): Câu hỏi cần tìm kiếm
83
+ top_k (int): Số lượng kết quả trả về
84
+ age_filter (int): Lọc theo độ tuổi (optional)
85
+
86
+ Returns:
87
+ list: Danh sách kết quả tìm kiếm
88
+ """
89
+ try:
90
+ logger.debug(f"Dang tim kiem cho query: {query[:50]}...")
91
+
92
+ # Encode query thành embedding
93
+ query_embedding = self.encode(query)[0]
94
+
95
+ # Tạo where clause cho age filter
96
+ where_clause = None
97
+ if age_filter:
98
+ where_clause = {
99
+ "$and": [
100
+ {"age_min": {"$lte": age_filter}},
101
+ {"age_max": {"$gte": age_filter}}
102
+ ]
103
+ }
104
+
105
+ # Thực hiện search trong ChromaDB
106
+ search_results = self.collection.query(
107
+ query_embeddings=[query_embedding],
108
+ n_results=top_k,
109
+ where=where_clause,
110
+ include=['documents', 'metadatas', 'distances']
111
+ )
112
+
113
+ if not search_results or not search_results['documents']:
114
+ logger.warning("Khong tim thay ket qua nao")
115
+ return []
116
+
117
+ # Format kết quả
118
+ results = []
119
+ documents = search_results['documents'][0]
120
+ metadatas = search_results['metadatas'][0]
121
+ distances = search_results['distances'][0]
122
+
123
+ for i, (doc, metadata, distance) in enumerate(zip(documents, metadatas, distances)):
124
+ results.append({
125
+ 'document': doc,
126
+ 'metadata': metadata or {},
127
+ 'distance': distance,
128
+ 'similarity': 1 - distance, # Chuyển distance thành similarity
129
+ 'rank': i + 1
130
+ })
131
+
132
+ logger.info(f"Tim thay {len(results)} ket qua cho query")
133
+ return results
134
+
135
+ except Exception as e:
136
+ logger.error(f"Loi tim kiem: {e}")
137
+ return []
138
+
139
+ def add_documents(self, documents, metadatas=None, ids=None):
140
+ """
141
+ Thêm documents vào ChromaDB
142
+
143
+ Args:
144
+ documents (list): Danh sách văn bản
145
+ metadatas (list): Danh sách metadata tương ứng
146
+ ids (list): Danh sách ID tương ứng (optional)
147
+
148
+ Returns:
149
+ bool: True nếu thành công
150
+ """
151
+ try:
152
+ if not documents:
153
+ logger.warning("Không có documents để thêm")
154
+ return False
155
+
156
+ # Tạo IDs nếu không được cung cấp
157
+ if not ids:
158
+ ids = [str(uuid.uuid4()) for _ in documents]
159
+
160
+ # Tạo metadatas rỗng nếu không được cung cấp
161
+ if not metadatas:
162
+ metadatas = [{} for _ in documents]
163
+
164
+ logger.info(f"Đang thêm {len(documents)} documents vào ChromaDB")
165
+
166
+ # Encode documents thành embeddings
167
+ embeddings = self.encode(documents)
168
+
169
+ # Thêm vào collection
170
+ self.collection.add(
171
+ embeddings=embeddings,
172
+ documents=documents,
173
+ metadatas=metadatas,
174
+ ids=ids
175
+ )
176
+
177
+ logger.info(f"Đã thêm thành công {len(documents)} documents")
178
+ return True
179
+
180
+ except Exception as e:
181
+ logger.error(f"Lỗi thêm documents: {e}")
182
+ return False
183
+
184
+ def index_chunks(self, chunks):
185
+ """
186
+ Index các chunks dữ liệu vào ChromaDB
187
+
188
+ Args:
189
+ chunks (list): Danh sách chunks với format:
190
+ [
191
+ {
192
+ "content": "nội dung văn bản",
193
+ "metadata": {"chapter": "bai1", "age_group": "1-3", ...},
194
+ "id": "unique_id" (optional)
195
+ }
196
+ ]
197
+
198
+ Returns:
199
+ bool: True nếu thành công
200
+ """
201
+ try:
202
+ if not chunks:
203
+ logger.warning("Không có chunks để index")
204
+ return False
205
+
206
+ documents = []
207
+ metadatas = []
208
+ ids = []
209
+
210
+ for chunk in chunks:
211
+ if not chunk.get('content'):
212
+ logger.warning(f"Chunk thiếu content: {chunk}")
213
+ continue
214
+
215
+ documents.append(chunk['content'])
216
+
217
+ # Lấy metadata đã được chuẩn bị sẵn
218
+ metadata = chunk.get('metadata', {})
219
+ metadatas.append(metadata)
220
+
221
+ # Sử dụng ID có sẵn hoặc tạo mới
222
+ chunk_id = chunk.get('id') or str(uuid.uuid4())
223
+ ids.append(chunk_id)
224
+
225
+ if not documents:
226
+ logger.warning("Không có documents hợp lệ để index")
227
+ return False
228
+
229
+ # Batch processing để tránh overload
230
+ batch_size = 100
231
+ total_batches = (len(documents) + batch_size - 1) // batch_size
232
+
233
+ for i in range(0, len(documents), batch_size):
234
+ batch_docs = documents[i:i + batch_size]
235
+ batch_metas = metadatas[i:i + batch_size]
236
+ batch_ids = ids[i:i + batch_size]
237
+
238
+ batch_num = (i // batch_size) + 1
239
+ logger.info(f"Đang xử lý batch {batch_num}/{total_batches} ({len(batch_docs)} items)")
240
+
241
+ success = self.add_documents(batch_docs, batch_metas, batch_ids)
242
+ if not success:
243
+ logger.error(f"Lỗi xử lý batch {batch_num}")
244
+ return False
245
+
246
+ logger.info(f"Đã index thành công {len(documents)} chunks")
247
+ return True
248
+
249
+ except Exception as e:
250
+ logger.error(f"Lỗi index chunks: {e}")
251
+ return False
252
+
253
+ def count(self):
254
+ """
255
+ Đếm số lượng documents trong collection
256
+
257
+ Returns:
258
+ int: Số lượng documents
259
+ """
260
+ try:
261
+ return self.collection.count()
262
+ except Exception as e:
263
+ logger.error(f"Lỗi đếm documents: {e}")
264
+ return 0
265
+
266
+ def delete_collection(self):
267
+ """
268
+ Xóa collection hiện tại
269
+
270
+ Returns:
271
+ bool: True nếu thành công
272
+ """
273
+ try:
274
+ logger.warning(f"Đang xóa collection: {COLLECTION_NAME}")
275
+ self.chroma_client.delete_collection(name=COLLECTION_NAME)
276
+
277
+ # Tạo lại collection mới
278
+ self.collection = self.chroma_client.create_collection(name=COLLECTION_NAME)
279
+ logger.info("Đã tạo lại collection mới")
280
+
281
+ return True
282
+
283
+ except Exception as e:
284
+ logger.error(f"Lỗi xóa collection: {e}")
285
+ return False
286
+
287
+ def get_stats(self):
288
+ """
289
+ Lấy thống kê về collection
290
+
291
+ Returns:
292
+ dict: Thông tin thống kê
293
+ """
294
+ try:
295
+ total_count = self.count()
296
+
297
+ # Lấy sample để phân tích metadata
298
+ sample_results = self.collection.get(limit=min(100, total_count))
299
+
300
+ # Thống kê content types
301
+ content_types = {}
302
+ chapters = {}
303
+ age_groups = {}
304
+
305
+ if sample_results and sample_results.get('metadatas'):
306
+ for metadata in sample_results['metadatas']:
307
+ if not metadata:
308
+ continue
309
+
310
+ # Content type stats
311
+ content_type = metadata.get('content_type', 'unknown')
312
+ content_types[content_type] = content_types.get(content_type, 0) + 1
313
+
314
+ # Chapter stats
315
+ chapter = metadata.get('chapter', 'unknown')
316
+ chapters[chapter] = chapters.get(chapter, 0) + 1
317
+
318
+ # Age group stats
319
+ age_group = metadata.get('age_group', 'unknown')
320
+ age_groups[age_group] = age_groups.get(age_group, 0) + 1
321
+
322
+ return {
323
+ 'total_documents': total_count,
324
+ 'content_types': content_types,
325
+ 'chapters': chapters,
326
+ 'age_groups': age_groups,
327
+ 'collection_name': COLLECTION_NAME,
328
+ 'embedding_model': EMBEDDING_MODEL
329
+ }
330
+
331
+ except Exception as e:
332
+ logger.error(f"Lỗi lấy stats: {e}")
333
+ return {
334
+ 'total_documents': 0,
335
+ 'error': str(e)
336
+ }
core/rag_pipeline.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import google.generativeai as genai
3
+ from core.embedding_model import get_embedding_model
4
+ from config import GEMINI_API_KEY, HUMAN_PROMPT_TEMPLATE, SYSTEM_PROMPT, TOP_K_RESULTS, TEMPERATURE, MAX_OUTPUT_TOKENS
5
+ import os
6
+ import re
7
+
8
+ # Cấu hình logging
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Cấu hình Gemini
12
+ genai.configure(api_key=GEMINI_API_KEY)
13
+
14
+ class RAGPipeline:
15
+ def __init__(self):
16
+ """Khởi tạo RAG Pipeline chỉ với embedding model"""
17
+ logger.info("Khởi tạo RAG Pipeline")
18
+
19
+ self.embedding_model = get_embedding_model()
20
+
21
+ # Khởi tạo Gemini model
22
+ self.gemini_model = genai.GenerativeModel('gemini-2.0-flash')
23
+
24
+ logger.info("RAG Pipeline đã sẵn sàng")
25
+
26
+ def generate_response(self, query, age=1):
27
+ """
28
+ Generate response cho user query sử dụng RAG
29
+
30
+ Args:
31
+ query (str): Câu hỏi của người dùng
32
+ age (int): Tuổi của người dùng (1-19)
33
+
34
+ Returns:
35
+ dict: Response data with success status
36
+ """
37
+ try:
38
+ logger.info(f"Bắt đầu generate response cho query: {query[:50]}... (age: {age})")
39
+
40
+ # SỬA: Chỉ search trong ChromaDB, không load lại dữ liệu
41
+ logger.info("Đang tìm kiếm thông tin liên quan...")
42
+ search_results = self.embedding_model.search(query, top_k=TOP_K_RESULTS)
43
+
44
+ if not search_results or len(search_results) == 0:
45
+ logger.warning("Không tìm thấy thông tin liên quan")
46
+ return {
47
+ "success": True,
48
+ "response": "Xin lỗi, tôi không tìm thấy thông tin liên quan đến câu hỏi của bạn trong tài liệu.",
49
+ "sources": []
50
+ }
51
+
52
+ # Chuẩn bị contexts từ kết quả tìm kiếm
53
+ contexts = []
54
+ sources = []
55
+
56
+ for result in search_results:
57
+ # Lấy thông tin từ metadata
58
+ metadata = result.get('metadata', {})
59
+ content = result.get('document', '')
60
+
61
+ # Thêm context
62
+ contexts.append({
63
+ "content": content,
64
+ "metadata": metadata
65
+ })
66
+
67
+ # Thêm source reference
68
+ source_info = {
69
+ "title": metadata.get('title', metadata.get('chapter', 'Tài liệu dinh dưỡng')),
70
+ "pages": metadata.get('pages'),
71
+ "content_type": metadata.get('content_type', 'text')
72
+ }
73
+
74
+ if source_info not in sources:
75
+ sources.append(source_info)
76
+
77
+ # Format contexts cho prompt
78
+ formatted_contexts = self._format_contexts(contexts)
79
+
80
+ # Tạo prompt với age context
81
+ full_prompt = self._create_prompt_with_age_context(query, age, formatted_contexts)
82
+
83
+ # Generate response với Gemini
84
+ logger.info("Đang tạo phản hồi với Gemini...")
85
+ response = self.gemini_model.generate_content(
86
+ full_prompt,
87
+ generation_config=genai.types.GenerationConfig(
88
+ temperature=TEMPERATURE,
89
+ max_output_tokens=MAX_OUTPUT_TOKENS
90
+ )
91
+ )
92
+
93
+ if not response or not response.text:
94
+ logger.error("Gemini không trả về response")
95
+ return {
96
+ "success": False,
97
+ "error": "Không thể tạo phản hồi"
98
+ }
99
+
100
+ response_text = response.text.strip()
101
+
102
+ # Post-process response để xử lý hình ảnh
103
+ response_text = self._process_image_links(response_text)
104
+
105
+ logger.info("Đã tạo phản hồi thành công")
106
+
107
+ return {
108
+ "success": True,
109
+ "response": response_text,
110
+ "sources": sources[:3] # Giới hạn 3 sources để không quá dài
111
+ }
112
+
113
+ except Exception as e:
114
+ logger.error(f"Lỗi generate response: {str(e)}")
115
+ return {
116
+ "success": False,
117
+ "error": f"Lỗi tạo phản hồi: {str(e)}"
118
+ }
119
+
120
+ def _format_contexts(self, contexts):
121
+ """Format contexts thành string cho prompt"""
122
+ formatted = []
123
+
124
+ for i, context in enumerate(contexts, 1):
125
+ content = context['content']
126
+ metadata = context['metadata']
127
+
128
+ # Thêm thông tin metadata
129
+ context_str = f"[Tài liệu {i}]"
130
+ if metadata.get('title'):
131
+ context_str += f" - {metadata['title']}"
132
+ if metadata.get('pages'):
133
+ context_str += f" (Trang {metadata['pages']})"
134
+
135
+ context_str += f"\n{content}\n"
136
+ formatted.append(context_str)
137
+
138
+ return "\n".join(formatted)
139
+
140
+ def _create_prompt_with_age_context(self, query, age, contexts):
141
+ """Tạo prompt với age context"""
142
+ # Xác định age group
143
+ if age <= 3:
144
+ age_guidance = "Sử dụng ngôn ngữ đơn giản, dễ hiểu cho phụ huynh có con nhỏ."
145
+ elif age <= 6:
146
+ age_guidance = "Tập trung vào dinh dưỡng cho trẻ mầm non, ngôn ngữ phù hợp với phụ huynh."
147
+ elif age <= 12:
148
+ age_guidance = "Nội dung phù hợp cho trẻ tiểu học, có thể giải thích đơn giản cho trẻ hiểu."
149
+ elif age <= 15:
150
+ age_guidance = "Thông tin chi tiết hơn, phù hợp cho học sinh trung học cơ sở."
151
+ else:
152
+ age_guidance = "Thông tin đầy đủ, chi tiết cho học sinh trung học phổ thông."
153
+
154
+ # Tạo system prompt với age context
155
+ age_aware_system_prompt = f"""{SYSTEM_PROMPT}
156
+
157
+ QUAN TRỌNG - Hướng dẫn theo độ tuổi:
158
+ Người dùng hiện tại {age} tuổi. {age_guidance}
159
+ - Điều chỉnh ngôn ngữ và nội dung cho phù hợp
160
+ - Đưa ra lời khuyên cụ thể cho độ tuổi này
161
+ - Tránh thông tin quá phức tạp hoặc không phù hợp
162
+ """
163
+
164
+ # Tạo human prompt
165
+ human_prompt = HUMAN_PROMPT_TEMPLATE.format(
166
+ query=query,
167
+ age=age,
168
+ contexts=contexts
169
+ )
170
+
171
+ return f"{age_aware_system_prompt}\n\n{human_prompt}"
172
+
173
+ def _process_image_links(self, response_text):
174
+ """Xử lý các đường dẫn hình ảnh trong response"""
175
+ try:
176
+ import re
177
+
178
+ # Tìm các pattern markdown image
179
+ image_pattern = r'!\[([^\]]*)\]\(([^)]+)\)'
180
+
181
+ def replace_image_path(match):
182
+ alt_text = match.group(1)
183
+ image_path = match.group(2)
184
+
185
+ # Xử lý đường dẫn local Windows/Linux
186
+ if '\\' in image_path or image_path.startswith('/') or ':' in image_path:
187
+ # Extract filename từ đường dẫn local
188
+ filename = image_path.split('\\')[-1].split('/')[-1]
189
+
190
+ # Tìm bai_id từ filename
191
+ bai_match = re.match(r'^(bai\d+)_', filename)
192
+ if bai_match:
193
+ bai_id = bai_match.group(1)
194
+ else:
195
+ bai_id = 'bai1' # default
196
+
197
+ # Tạo API URL
198
+ api_url = f"/api/figures/{bai_id}/{filename}"
199
+ return f"![{alt_text}]({api_url})"
200
+
201
+ # Nếu đã là đường dẫn API, giữ nguyên
202
+ elif image_path.startswith('/api/figures/'):
203
+ return match.group(0)
204
+
205
+ # Xử lý đường dẫn tương đối
206
+ elif '../figures/' in image_path:
207
+ filename = image_path.split('../figures/')[-1]
208
+ bai_match = re.match(r'^(bai\d+)_', filename)
209
+ if bai_match:
210
+ bai_id = bai_match.group(1)
211
+ else:
212
+ bai_id = 'bai1'
213
+
214
+ api_url = f"/api/figures/{bai_id}/{filename}"
215
+ return f"![{alt_text}]({api_url})"
216
+
217
+ # Các trường hợp khác, giữ nguyên
218
+ return match.group(0)
219
+
220
+ # Thay thế tất cả image links
221
+ processed_text = re.sub(image_pattern, replace_image_path, response_text)
222
+
223
+ logger.info(f"Processed {len(re.findall(image_pattern, response_text))} image links")
224
+ return processed_text
225
+
226
+ except Exception as e:
227
+ logger.error(f"Lỗi xử lý image links: {e}")
228
+ return response_text
229
+
230
+ def generate_follow_up_questions(self, query, answer, age=1):
231
+ """
232
+ Tạo câu hỏi gợi ý dựa trên query và answer
233
+
234
+ Args:
235
+ query (str): Câu hỏi gốc
236
+ answer (str): Câu trả lời đã được tạo
237
+ age (int): Tuổi người dùng
238
+
239
+ Returns:
240
+ dict: Response data với danh sách câu hỏi gợi ý
241
+ """
242
+ try:
243
+ logger.info("Đang tạo câu hỏi follow-up...")
244
+
245
+ follow_up_prompt = f"""
246
+ Dựa trên cuộc hội thoại sau, hãy tạo 3-5 câu hỏi gợi ý phù hợp cho người dùng {age} tuổi về chủ đề dinh dưỡng:
247
+
248
+ Câu hỏi gốc: {query}
249
+ Câu trả lời: {answer}
250
+
251
+ Hãy tạo các câu hỏi:
252
+ 1. Liên quan trực tiếp đến chủ đề
253
+ 2. Phù hợp với độ tuổi {age}
254
+ 3. Thực tế và hữu ích
255
+ 4. Ngắn gọn, dễ hiểu
256
+
257
+ Trả về danh sách câu hỏi, mỗi câu một dòng, không đánh số.
258
+ """
259
+
260
+ response = self.gemini_model.generate_content(
261
+ follow_up_prompt,
262
+ generation_config=genai.types.GenerationConfig(
263
+ temperature=0.7,
264
+ max_output_tokens=500
265
+ )
266
+ )
267
+
268
+ if not response or not response.text:
269
+ return {
270
+ "success": False,
271
+ "error": "Không thể tạo câu hỏi gợi ý"
272
+ }
273
+
274
+ # Parse response thành list câu hỏi
275
+ questions = []
276
+ lines = response.text.strip().split('\n')
277
+
278
+ for line in lines:
279
+ line = line.strip()
280
+ if line and not line.startswith('#') and len(line) > 10:
281
+ # Loại bỏ số thứ tự nếu có
282
+ line = re.sub(r'^\d+[\.\)]\s*', '', line)
283
+ questions.append(line)
284
+
285
+ # Giới hạn 5 câu hỏi
286
+ questions = questions[:5]
287
+
288
+ return {
289
+ "success": True,
290
+ "questions": questions
291
+ }
292
+
293
+ except Exception as e:
294
+ logger.error(f"Lỗi tạo follow-up questions: {str(e)}")
295
+ return {
296
+ "success": False,
297
+ "error": f"Lỗi tạo câu hỏi gợi ý: {str(e)}"
298
+ }
embed_data.log ADDED
File without changes
models/__init__.py ADDED
File without changes
models/admin_model.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import datetime
3
+ from pymongo import MongoClient
4
+ from bson.objectid import ObjectId
5
+ import bcrypt
6
+ import logging
7
+ from dotenv import load_dotenv
8
+
9
+ # Cấu hình logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Tải biến môi trường
13
+ load_dotenv()
14
+
15
+ # Kết nối MongoDB (sử dụng chung với user_model)
16
+ MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/")
17
+ DATABASE_NAME = os.getenv("MONGO_DB_NAME", "nutribot_db")
18
+
19
+ # Singleton pattern cho kết nối MongoDB
20
+ _mongo_client = None
21
+ _db = None
22
+
23
+ def get_db():
24
+ """Trả về instance của MongoDB database (singleton pattern)"""
25
+ global _mongo_client, _db
26
+ if _mongo_client is None:
27
+ try:
28
+ _mongo_client = MongoClient(MONGO_URI)
29
+ _db = _mongo_client[DATABASE_NAME]
30
+ logger.info(f"Đã kết nối đến database: {DATABASE_NAME}")
31
+ except Exception as e:
32
+ logger.error(f"Lỗi kết nối MongoDB: {e}")
33
+ raise
34
+ return _db
35
+
36
+ class AdminUser:
37
+ def __init__(self, name, email, password, role="admin", permissions=None,
38
+ user_id=None, created_at=None, updated_at=None, last_login=None):
39
+ self.user_id = user_id
40
+ self.name = name
41
+ self.email = email
42
+ self.password = password # Mật khẩu đã mã hóa
43
+ self.role = role # admin, super_admin
44
+ self.permissions = permissions or self._get_default_permissions(role)
45
+ self.created_at = created_at or datetime.datetime.now()
46
+ self.updated_at = updated_at or datetime.datetime.now()
47
+ self.last_login = last_login
48
+ self.is_active = True
49
+
50
+ def _get_default_permissions(self, role):
51
+ """Lấy quyền mặc định theo role"""
52
+ if role == "super_admin":
53
+ return {
54
+ "users": {"read": True, "write": True, "delete": True},
55
+ "documents": {"read": True, "write": True, "delete": True},
56
+ "conversations": {"read": True, "write": True, "delete": True},
57
+ "system": {"read": True, "write": True, "delete": True},
58
+ "analytics": {"read": True, "write": True, "delete": True}
59
+ }
60
+ elif role == "admin":
61
+ return {
62
+ "users": {"read": True, "write": True, "delete": False},
63
+ "documents": {"read": True, "write": True, "delete": True},
64
+ "conversations": {"read": True, "write": False, "delete": True},
65
+ "system": {"read": True, "write": False, "delete": False},
66
+ "analytics": {"read": True, "write": False, "delete": False}
67
+ }
68
+ else:
69
+ return {}
70
+
71
+ @staticmethod
72
+ def hash_password(password):
73
+ """Mã hóa mật khẩu sử dụng bcrypt"""
74
+ salt = bcrypt.gensalt()
75
+ hashed_pw = bcrypt.hashpw(password.encode('utf-8'), salt)
76
+ return hashed_pw
77
+
78
+ @staticmethod
79
+ def check_password(hashed_password, password):
80
+ """Kiểm tra mật khẩu"""
81
+ return bcrypt.checkpw(password.encode('utf-8'), hashed_password)
82
+
83
+ def to_dict(self):
84
+ """Chuyển đổi thông tin admin thành dictionary"""
85
+ admin_dict = {
86
+ "name": self.name,
87
+ "email": self.email,
88
+ "password": self.password,
89
+ "role": self.role,
90
+ "permissions": self.permissions,
91
+ "created_at": self.created_at,
92
+ "updated_at": self.updated_at,
93
+ "last_login": self.last_login,
94
+ "is_active": self.is_active
95
+ }
96
+ if self.user_id:
97
+ admin_dict["_id"] = self.user_id
98
+ return admin_dict
99
+
100
+ @classmethod
101
+ def from_dict(cls, admin_dict):
102
+ """Tạo đối tượng AdminUser từ dictionary"""
103
+ if not admin_dict:
104
+ return None
105
+
106
+ return cls(
107
+ user_id=admin_dict.get("_id"),
108
+ name=admin_dict.get("name"),
109
+ email=admin_dict.get("email"),
110
+ password=admin_dict.get("password"),
111
+ role=admin_dict.get("role", "admin"),
112
+ permissions=admin_dict.get("permissions"),
113
+ created_at=admin_dict.get("created_at"),
114
+ updated_at=admin_dict.get("updated_at"),
115
+ last_login=admin_dict.get("last_login")
116
+ )
117
+
118
+ @classmethod
119
+ def find_by_email(cls, email):
120
+ """Tìm admin theo email"""
121
+ try:
122
+ db = get_db()
123
+ admins_collection = db.admins
124
+ admin_data = admins_collection.find_one({"email": email, "is_active": True})
125
+ return cls.from_dict(admin_data)
126
+ except Exception as e:
127
+ logger.error(f"Lỗi tìm admin theo email: {e}")
128
+ return None
129
+
130
+ @classmethod
131
+ def find_by_id(cls, user_id):
132
+ """Tìm admin theo ID"""
133
+ try:
134
+ db = get_db()
135
+ admins_collection = db.admins
136
+ admin_data = admins_collection.find_one({"_id": ObjectId(user_id), "is_active": True})
137
+ return cls.from_dict(admin_data)
138
+ except Exception as e:
139
+ logger.error(f"Lỗi tìm admin theo ID: {e}")
140
+ return None
141
+
142
+ @classmethod
143
+ def get_all(cls, page=1, per_page=10):
144
+ """Lấy danh sách tất cả admin với phân trang"""
145
+ try:
146
+ db = get_db()
147
+ admins_collection = db.admins
148
+
149
+ skip = (page - 1) * per_page
150
+ admins_data = admins_collection.find(
151
+ {"is_active": True}
152
+ ).sort("created_at", -1).skip(skip).limit(per_page)
153
+
154
+ total = admins_collection.count_documents({"is_active": True})
155
+
156
+ return [cls.from_dict(admin) for admin in admins_data], total
157
+ except Exception as e:
158
+ logger.error(f"Lỗi lấy danh sách admin: {e}")
159
+ return [], 0
160
+
161
+ def save(self):
162
+ """Lưu thông tin admin vào database"""
163
+ try:
164
+ db = get_db()
165
+ admins_collection = db.admins
166
+
167
+ if not self.user_id:
168
+ # Đây là admin mới
169
+ insert_result = admins_collection.insert_one(self.to_dict())
170
+ self.user_id = insert_result.inserted_id
171
+ logger.info(f"Đã tạo admin mới với ID: {self.user_id}")
172
+ return self.user_id
173
+ else:
174
+ # Cập nhật admin đã tồn tại
175
+ self.updated_at = datetime.datetime.now()
176
+ admins_collection.update_one(
177
+ {"_id": self.user_id},
178
+ {"$set": self.to_dict()}
179
+ )
180
+ logger.info(f"Đã cập nhật thông tin admin: {self.user_id}")
181
+ return self.user_id
182
+ except Exception as e:
183
+ logger.error(f"Lỗi khi lưu thông tin admin: {e}")
184
+ raise
185
+
186
+ def update_last_login(self):
187
+ """Cập nhật thời gian đăng nhập cuối"""
188
+ try:
189
+ self.last_login = datetime.datetime.now()
190
+ self.save()
191
+ except Exception as e:
192
+ logger.error(f"Lỗi cập nhật last_login: {e}")
193
+
194
+ def has_permission(self, resource, action):
195
+ """Kiểm tra quyền truy cập"""
196
+ if not self.permissions:
197
+ return False
198
+
199
+ resource_perms = self.permissions.get(resource, {})
200
+ return resource_perms.get(action, False)
201
+
202
+ def delete(self):
203
+ """Xóa mềm admin (đánh dấu is_active = False)"""
204
+ try:
205
+ self.is_active = False
206
+ self.save()
207
+ logger.info(f"Đã xóa admin: {self.user_id}")
208
+ return True
209
+ except Exception as e:
210
+ logger.error(f"Lỗi khi xóa admin: {e}")
211
+ return False
212
+
213
+ @staticmethod
214
+ def login(email, password):
215
+ """Đăng nhập admin"""
216
+ try:
217
+ if not email or not password:
218
+ return False, "Vui lòng nhập đầy đủ thông tin đăng nhập"
219
+
220
+ admin = AdminUser.find_by_email(email)
221
+ if not admin:
222
+ return False, "Email không tồn tại hoặc không có quyền admin"
223
+
224
+ if not AdminUser.check_password(admin.password, password):
225
+ return False, "Mật khẩu không chính xác"
226
+
227
+ # Cập nhật thời gian đăng nhập
228
+ admin.update_last_login()
229
+
230
+ return True, admin
231
+ except Exception as e:
232
+ logger.error(f"Lỗi đăng nhập admin: {e}")
233
+ return False, f"Lỗi đăng nhập: {str(e)}"
234
+
235
+ @staticmethod
236
+ def create_admin(name, email, password, role="admin", permissions=None):
237
+ """Tạo admin mới"""
238
+ try:
239
+ # Kiểm tra email đã tồn tại chưa
240
+ existing_admin = AdminUser.find_by_email(email)
241
+ if existing_admin:
242
+ return False, "Email đã được sử dụng"
243
+
244
+ # Mã hóa mật khẩu
245
+ hashed_password = AdminUser.hash_password(password)
246
+
247
+ # Tạo admin mới
248
+ new_admin = AdminUser(
249
+ name=name,
250
+ email=email,
251
+ password=hashed_password,
252
+ role=role,
253
+ permissions=permissions
254
+ )
255
+
256
+ # Lưu vào database
257
+ admin_id = new_admin.save()
258
+
259
+ return True, {"admin_id": str(admin_id)}
260
+ except Exception as e:
261
+ logger.error(f"Lỗi tạo admin: {e}")
262
+ return False, f"Lỗi tạo admin: {str(e)}"
263
+
264
+ @staticmethod
265
+ def create_default_super_admin():
266
+ """Tạo super admin mặc định nếu chưa có"""
267
+ try:
268
+ db = get_db()
269
+ admins_collection = db.admins
270
+
271
+ # Kiểm tra xem đã có super admin chưa
272
+ existing_super_admin = admins_collection.find_one({
273
+ "role": "super_admin",
274
+ "is_active": True
275
+ })
276
+
277
+ if not existing_super_admin:
278
+ # Tạo super admin mặc định
279
+ default_email = "[email protected]"
280
+ default_password = "NutribotAdmin2024!"
281
+
282
+ success, result = AdminUser.create_admin(
283
+ name="Super Admin",
284
+ email=default_email,
285
+ password=default_password,
286
+ role="super_admin"
287
+ )
288
+
289
+ if success:
290
+ logger.info(f"Đã tạo super admin mặc định: {default_email}")
291
+ logger.info(f"Mật khẩu mặc định: {default_password}")
292
+ return True, {
293
+ "email": default_email,
294
+ "password": default_password,
295
+ "admin_id": result["admin_id"]
296
+ }
297
+ else:
298
+ logger.error(f"Lỗi tạo super admin mặc định: {result}")
299
+ return False, result
300
+ else:
301
+ logger.info("Super admin đã tồn tại")
302
+ return True, {"message": "Super admin đã tồn tại"}
303
+
304
+ except Exception as e:
305
+ logger.error(f"Lỗi tạo super admin mặc định: {e}")
306
+ return False, str(e)
models/conversation_model.py ADDED
@@ -0,0 +1,731 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import datetime
3
+ from bson.objectid import ObjectId
4
+ import logging
5
+ from pymongo import MongoClient, DESCENDING
6
+ from dotenv import load_dotenv
7
+ import copy
8
+
9
+ # Cấu hình logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Tải biến môi trường
13
+ load_dotenv()
14
+
15
+ # Kết nối MongoDB
16
+ MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/")
17
+ DATABASE_NAME = os.getenv("MONGO_DB_NAME", "nutribot_db")
18
+
19
+ # Singleton pattern cho kết nối MongoDB
20
+ _mongo_client = None
21
+ _db = None
22
+
23
+ def get_db():
24
+ """Trả về instance của MongoDB database (singleton pattern)"""
25
+ global _mongo_client, _db
26
+ if _mongo_client is None:
27
+ try:
28
+ _mongo_client = MongoClient(MONGO_URI)
29
+ _db = _mongo_client[DATABASE_NAME]
30
+ logger.info(f"Đã kết nối đến database: {DATABASE_NAME}")
31
+ except Exception as e:
32
+ logger.error(f"Lỗi kết nối MongoDB: {e}")
33
+ raise
34
+ return _db
35
+
36
+ def safe_isoformat(timestamp_obj):
37
+ """Safely convert timestamp to isoformat string"""
38
+ if timestamp_obj is None:
39
+ return None
40
+
41
+ if isinstance(timestamp_obj, str):
42
+ return timestamp_obj
43
+
44
+ if hasattr(timestamp_obj, 'isoformat'):
45
+ return timestamp_obj.isoformat()
46
+
47
+ try:
48
+ return str(timestamp_obj)
49
+ except:
50
+ return None
51
+
52
+ def safe_datetime(timestamp_obj):
53
+ """Safely convert various timestamp formats to datetime object"""
54
+ if timestamp_obj is None:
55
+ return datetime.datetime.now()
56
+
57
+ if isinstance(timestamp_obj, datetime.datetime):
58
+ return timestamp_obj
59
+
60
+ if isinstance(timestamp_obj, str):
61
+ try:
62
+ return datetime.datetime.fromisoformat(timestamp_obj.replace('Z', '+00:00'))
63
+ except:
64
+ return datetime.datetime.now()
65
+
66
+ return datetime.datetime.now()
67
+
68
+ class Conversation:
69
+ def __init__(self, user_id=None, title="Cuộc trò chuyện mới", age_context=None,
70
+ created_at=None, updated_at=None, is_archived=False, messages=None,
71
+ conversation_id=None):
72
+ self.conversation_id = conversation_id
73
+ self.user_id = user_id
74
+ self.title = title
75
+ self.age_context = age_context
76
+ self.created_at = safe_datetime(created_at)
77
+ self.updated_at = safe_datetime(updated_at)
78
+ self.is_archived = is_archived
79
+ self.messages = messages or []
80
+
81
+ @classmethod
82
+ def create(cls, user_id, title="Cuộc trò chuyện mới", age_context=None):
83
+ """Tạo và lưu conversation mới vào database"""
84
+ try:
85
+ if isinstance(user_id, str):
86
+ user_id = ObjectId(user_id)
87
+
88
+ conversation = cls(
89
+ user_id=user_id,
90
+ title=title,
91
+ age_context=age_context
92
+ )
93
+
94
+ conversation_id = conversation.save()
95
+ conversation.conversation_id = conversation_id
96
+
97
+ logger.info(f"Created new conversation: {conversation_id}")
98
+ return conversation_id
99
+
100
+ except Exception as e:
101
+ logger.error(f"Error creating conversation: {e}")
102
+ raise
103
+
104
+ def serialize_message_for_following(self, message):
105
+ """Serialize message để lưu vào following_messages"""
106
+ serialized = {
107
+ "role": message["role"],
108
+ "content": message["content"],
109
+ "timestamp": safe_datetime(message.get("timestamp")),
110
+ "current_version": message.get("current_version", 1),
111
+ "is_edited": message.get("is_edited", False)
112
+ }
113
+
114
+ if "sources" in message:
115
+ serialized["sources"] = message["sources"]
116
+ if "metadata" in message:
117
+ serialized["metadata"] = message["metadata"]
118
+
119
+ # Serialize versions
120
+ if "versions" in message and message["versions"]:
121
+ serialized["versions"] = []
122
+ for version in message["versions"]:
123
+ version_data = {
124
+ "version": version["version"],
125
+ "content": version["content"],
126
+ "timestamp": safe_datetime(version.get("timestamp")),
127
+ "following_messages": version.get("following_messages", [])
128
+ }
129
+ if "sources" in version:
130
+ version_data["sources"] = version["sources"]
131
+ if "metadata" in version:
132
+ version_data["metadata"] = version["metadata"]
133
+ serialized["versions"].append(version_data)
134
+
135
+ return serialized
136
+
137
+ def deserialize_message_from_following(self, serialized_message):
138
+ """Deserialize message từ following_messages"""
139
+ message = {
140
+ "_id": ObjectId(),
141
+ "role": serialized_message["role"],
142
+ "content": serialized_message["content"],
143
+ "timestamp": safe_datetime(serialized_message.get("timestamp")),
144
+ "current_version": serialized_message.get("current_version", 1),
145
+ "is_edited": serialized_message.get("is_edited", False)
146
+ }
147
+
148
+ if "sources" in serialized_message:
149
+ message["sources"] = serialized_message["sources"]
150
+ if "metadata" in serialized_message:
151
+ message["metadata"] = serialized_message["metadata"]
152
+
153
+ # Deserialize versions
154
+ if "versions" in serialized_message and serialized_message["versions"]:
155
+ message["versions"] = []
156
+ for version_data in serialized_message["versions"]:
157
+ version = {
158
+ "version": version_data["version"],
159
+ "content": version_data["content"],
160
+ "timestamp": safe_datetime(version_data.get("timestamp")),
161
+ "following_messages": version_data.get("following_messages", [])
162
+ }
163
+ if "sources" in version_data:
164
+ version["sources"] = version_data["sources"]
165
+ if "metadata" in version_data:
166
+ version["metadata"] = version_data["metadata"]
167
+ message["versions"].append(version)
168
+ else:
169
+ # Tạo version mặc định
170
+ message["versions"] = [{
171
+ "version": 1,
172
+ "content": message["content"],
173
+ "timestamp": message["timestamp"],
174
+ "following_messages": []
175
+ }]
176
+ if "sources" in message:
177
+ message["versions"][0]["sources"] = message["sources"]
178
+ if "metadata" in message:
179
+ message["versions"][0]["metadata"] = message["metadata"]
180
+
181
+ return message
182
+
183
+ def capture_following_messages(self, from_message_index):
184
+ """Capture tất cả messages sau from_message_index"""
185
+ following_messages = []
186
+
187
+ for i in range(from_message_index + 1, len(self.messages)):
188
+ message = self.messages[i]
189
+ serialized = self.serialize_message_for_following(message)
190
+ following_messages.append(serialized)
191
+
192
+ logger.info(f"Captured {len(following_messages)} following messages from index {from_message_index}")
193
+ return following_messages
194
+
195
+ def restore_following_messages(self, target_message_index, following_messages):
196
+ """Restore following messages sau target_message_index"""
197
+ # Cắt conversation tại target message
198
+ self.messages = self.messages[:target_message_index + 1]
199
+
200
+ # Restore messages
201
+ restored_count = 0
202
+ for serialized_message in following_messages:
203
+ message = self.deserialize_message_from_following(serialized_message)
204
+ self.messages.append(message)
205
+ restored_count += 1
206
+
207
+ logger.info(f"Restored {restored_count} following messages after index {target_message_index}")
208
+ return restored_count
209
+
210
+ def to_dict(self):
211
+ """Convert conversation object sang dictionary cho JSON serialization"""
212
+ try:
213
+ result = {
214
+ "id": str(self.conversation_id),
215
+ "user_id": str(self.user_id),
216
+ "title": self.title,
217
+ "age_context": self.age_context,
218
+ "created_at": self.created_at.isoformat() if self.created_at else None,
219
+ "updated_at": self.updated_at.isoformat() if self.updated_at else None,
220
+ "is_archived": self.is_archived,
221
+ "messages": []
222
+ }
223
+
224
+ for message in self.messages:
225
+ timestamp_str = None
226
+ if "timestamp" in message:
227
+ if hasattr(message["timestamp"], 'isoformat'):
228
+ timestamp_str = message["timestamp"].isoformat()
229
+ else:
230
+ timestamp_str = str(message["timestamp"])
231
+
232
+ message_data = {
233
+ "id": str(message["_id"]),
234
+ "_id": str(message["_id"]),
235
+ "role": message["role"],
236
+ "content": message["content"],
237
+ "timestamp": timestamp_str,
238
+ "current_version": message.get("current_version", 1),
239
+ "is_edited": message.get("is_edited", False)
240
+ }
241
+
242
+ if "versions" in message and message["versions"]:
243
+ message_data["versions"] = []
244
+ for version in message["versions"]:
245
+ version_timestamp = None
246
+ if "timestamp" in version:
247
+ if hasattr(version["timestamp"], 'isoformat'):
248
+ version_timestamp = version["timestamp"].isoformat()
249
+ else:
250
+ version_timestamp = str(version["timestamp"])
251
+
252
+ version_data = {
253
+ "content": version["content"],
254
+ "timestamp": version_timestamp,
255
+ "version": version["version"]
256
+ }
257
+ if "sources" in version:
258
+ version_data["sources"] = version["sources"]
259
+ message_data["versions"].append(version_data)
260
+
261
+ if "sources" in message:
262
+ message_data["sources"] = message["sources"]
263
+
264
+ result["messages"].append(message_data)
265
+
266
+ return result
267
+ except Exception as e:
268
+ logger.error(f"Lỗi khi convert conversation to dict: {e}")
269
+ return None
270
+
271
+ @classmethod
272
+ def from_dict(cls, conversation_dict):
273
+ """Tạo đối tượng Conversation từ dictionary"""
274
+ if not conversation_dict:
275
+ return None
276
+
277
+ return cls(
278
+ conversation_id=conversation_dict.get("_id"),
279
+ user_id=conversation_dict.get("user_id"),
280
+ title=conversation_dict.get("title"),
281
+ age_context=conversation_dict.get("age_context"),
282
+ created_at=conversation_dict.get("created_at"),
283
+ updated_at=conversation_dict.get("updated_at"),
284
+ is_archived=conversation_dict.get("is_archived", False),
285
+ messages=conversation_dict.get("messages", [])
286
+ )
287
+
288
+ def save(self):
289
+ """Lưu thông tin cuộc hội thoại vào database"""
290
+ try:
291
+ db = get_db()
292
+ conversations_collection = db.conversations
293
+
294
+ self.updated_at = datetime.datetime.now()
295
+
296
+ save_dict = {
297
+ "user_id": self.user_id,
298
+ "title": self.title,
299
+ "age_context": self.age_context,
300
+ "created_at": self.created_at,
301
+ "updated_at": self.updated_at,
302
+ "is_archived": self.is_archived,
303
+ "messages": []
304
+ }
305
+
306
+ for message in self.messages:
307
+ message_copy = message.copy()
308
+ message_copy["timestamp"] = safe_datetime(message_copy.get("timestamp"))
309
+
310
+ if "versions" in message_copy:
311
+ for version in message_copy["versions"]:
312
+ version["timestamp"] = safe_datetime(version.get("timestamp"))
313
+
314
+ save_dict["messages"].append(message_copy)
315
+
316
+ if not self.conversation_id:
317
+ insert_result = conversations_collection.insert_one(save_dict)
318
+ self.conversation_id = insert_result.inserted_id
319
+ logger.info(f"Saved new conversation with ID: {self.conversation_id}")
320
+ return self.conversation_id
321
+ else:
322
+ conversations_collection.update_one(
323
+ {"_id": self.conversation_id},
324
+ {"$set": save_dict}
325
+ )
326
+ logger.info(f"Updated conversation: {self.conversation_id}")
327
+ return self.conversation_id
328
+ except Exception as e:
329
+ logger.error(f"Error saving conversation: {e}")
330
+ raise
331
+
332
+ def add_message(self, role, content, sources=None, metadata=None, parent_message_id=None):
333
+ """Thêm tin nhắn mới vào cuộc hội thoại"""
334
+ timestamp = datetime.datetime.now()
335
+
336
+ message = {
337
+ "_id": ObjectId(),
338
+ "role": role,
339
+ "content": content,
340
+ "timestamp": timestamp,
341
+ "versions": [{
342
+ "content": content,
343
+ "timestamp": timestamp,
344
+ "version": 1,
345
+ "following_messages": []
346
+ }],
347
+ "current_version": 1,
348
+ "parent_message_id": parent_message_id,
349
+ "is_edited": False
350
+ }
351
+
352
+ if sources:
353
+ message["sources"] = sources
354
+ message["versions"][0]["sources"] = sources
355
+
356
+ if metadata:
357
+ message["metadata"] = metadata
358
+ message["versions"][0]["metadata"] = metadata
359
+
360
+ self.messages.append(message)
361
+ self.updated_at = timestamp
362
+ self.save()
363
+
364
+ logger.info(f"Added message to conversation {self.conversation_id}")
365
+ return message["_id"]
366
+
367
+ def edit_message(self, message_id, new_content):
368
+ """Chỉnh sửa tin nhắn và lưu following messages vào version hiện tại"""
369
+ try:
370
+ message_index = None
371
+ for i, message in enumerate(self.messages):
372
+ if str(message["_id"]) == str(message_id):
373
+ message_index = i
374
+ break
375
+
376
+ if message_index is None:
377
+ return False, "Không tìm thấy tin nhắn"
378
+
379
+ message = self.messages[message_index]
380
+
381
+ if message["role"] != "user":
382
+ return False, "Chỉ có thể chỉnh sửa tin nhắn của người dùng"
383
+
384
+ timestamp = datetime.datetime.now()
385
+
386
+ # Capture following messages cho version hiện tại
387
+ following_messages = self.capture_following_messages(message_index)
388
+
389
+ # Khởi tạo versions nếu chưa có
390
+ if "versions" not in message:
391
+ message["versions"] = [{
392
+ "content": message["content"],
393
+ "timestamp": safe_datetime(message.get("timestamp", timestamp)),
394
+ "version": 1,
395
+ "following_messages": following_messages
396
+ }]
397
+ if "sources" in message:
398
+ message["versions"][0]["sources"] = message["sources"]
399
+ if "metadata" in message:
400
+ message["versions"][0]["metadata"] = message["metadata"]
401
+ else:
402
+ # Cập nhật following_messages cho version hiện tại
403
+ current_version_index = message.get("current_version", 1) - 1
404
+ if current_version_index < len(message["versions"]):
405
+ message["versions"][current_version_index]["following_messages"] = following_messages
406
+
407
+ # Tạo version mới
408
+ new_version = len(message["versions"]) + 1
409
+ new_version_data = {
410
+ "content": new_content,
411
+ "timestamp": timestamp,
412
+ "version": new_version,
413
+ "following_messages": []
414
+ }
415
+
416
+ message["versions"].append(new_version_data)
417
+ message["current_version"] = new_version
418
+ message["content"] = new_content
419
+ message["is_edited"] = True
420
+
421
+ # Xóa tất cả messages sau message được edit
422
+ self.messages = self.messages[:message_index + 1]
423
+
424
+ self.updated_at = timestamp
425
+ self.save()
426
+
427
+ logger.info(f"Edited message {message_id}, created version {new_version} with {len(following_messages)} following messages")
428
+ return True, "Đã chỉnh sửa tin nhắn thành công"
429
+
430
+ except Exception as e:
431
+ logger.error(f"Error editing message: {e}")
432
+ return False, f"Lỗi: {str(e)}"
433
+
434
+ def regenerate_bot_response_after_edit(self, user_message_id, new_response, sources=None):
435
+ """Thêm phản hồi bot mới sau khi edit tin nhắn user"""
436
+ try:
437
+ user_message_index = None
438
+ for i, message in enumerate(self.messages):
439
+ if str(message["_id"]) == str(user_message_id):
440
+ user_message_index = i
441
+ break
442
+
443
+ if user_message_index is None:
444
+ return False, "Không tìm thấy tin nhắn user"
445
+
446
+ timestamp = datetime.datetime.now()
447
+ bot_message = {
448
+ "_id": ObjectId(),
449
+ "role": "bot",
450
+ "content": new_response,
451
+ "timestamp": timestamp,
452
+ "versions": [{
453
+ "content": new_response,
454
+ "timestamp": timestamp,
455
+ "version": 1,
456
+ "following_messages": []
457
+ }],
458
+ "current_version": 1,
459
+ "parent_message_id": self.messages[user_message_index]["_id"],
460
+ "is_edited": False
461
+ }
462
+
463
+ if sources:
464
+ bot_message["sources"] = sources
465
+ bot_message["versions"][0]["sources"] = sources
466
+
467
+ self.messages.append(bot_message)
468
+
469
+ # Cập nhật following_messages cho version hiện tại của user message
470
+ user_message = self.messages[user_message_index]
471
+ if "versions" in user_message:
472
+ current_version_index = user_message.get("current_version", 1) - 1
473
+ if current_version_index < len(user_message["versions"]):
474
+ # Capture lại following messages bao gồm bot response mới
475
+ following_messages = self.capture_following_messages(user_message_index)
476
+ user_message["versions"][current_version_index]["following_messages"] = following_messages
477
+
478
+ self.updated_at = timestamp
479
+ self.save()
480
+
481
+ logger.info(f"Added bot response after edit for user message {user_message_id}")
482
+ return True, "Đã tạo phản hồi mới"
483
+
484
+ except Exception as e:
485
+ logger.error(f"Error adding bot response: {e}")
486
+ return False, f"Lỗi: {str(e)}"
487
+
488
+ def regenerate_response(self, message_id, new_response, sources=None):
489
+ """Tạo version mới cho phản hồi bot"""
490
+ try:
491
+ message_index = None
492
+ for i, message in enumerate(self.messages):
493
+ if str(message["_id"]) == str(message_id):
494
+ message_index = i
495
+ break
496
+
497
+ if message_index is None:
498
+ return False, "Không tìm thấy tin nhắn"
499
+
500
+ message = self.messages[message_index]
501
+
502
+ if message["role"] != "bot":
503
+ return False, "Chỉ có thể regenerate phản hồi của bot"
504
+
505
+ timestamp = datetime.datetime.now()
506
+
507
+ # Capture following messages cho version hiện tại
508
+ following_messages = self.capture_following_messages(message_index)
509
+
510
+ # Khởi tạo versions nếu chưa có
511
+ if "versions" not in message:
512
+ message["versions"] = [{
513
+ "content": message["content"],
514
+ "timestamp": safe_datetime(message.get("timestamp", timestamp)),
515
+ "version": 1,
516
+ "sources": message.get("sources", []),
517
+ "following_messages": following_messages
518
+ }]
519
+ if "metadata" in message:
520
+ message["versions"][0]["metadata"] = message["metadata"]
521
+ else:
522
+ # Cập nhật following_messages cho version hiện tại
523
+ current_version_index = message.get("current_version", 1) - 1
524
+ if current_version_index < len(message["versions"]):
525
+ message["versions"][current_version_index]["following_messages"] = following_messages
526
+
527
+ # Tạo version mới
528
+ new_version = len(message["versions"]) + 1
529
+ new_version_data = {
530
+ "content": new_response,
531
+ "timestamp": timestamp,
532
+ "version": new_version,
533
+ "following_messages": []
534
+ }
535
+
536
+ if sources:
537
+ new_version_data["sources"] = sources
538
+
539
+ message["versions"].append(new_version_data)
540
+ message["current_version"] = new_version
541
+ message["content"] = new_response
542
+ message["is_edited"] = True
543
+
544
+ if sources:
545
+ message["sources"] = sources
546
+
547
+ # Xóa tất cả messages sau regenerate message
548
+ self.messages = self.messages[:message_index + 1]
549
+
550
+ self.updated_at = datetime.datetime.now()
551
+ self.save()
552
+
553
+ logger.info(f"Regenerated response for message {message_id}, version {new_version} with {len(following_messages)} following messages")
554
+ return True, "Đã tạo phản hồi mới thành công"
555
+
556
+ except Exception as e:
557
+ logger.error(f"Error regenerating response: {e}")
558
+ return False, f"Lỗi: {str(e)}"
559
+
560
+ def switch_message_version(self, message_id, version_number):
561
+ """Chuyển đổi version của tin nhắn và restore following messages"""
562
+ try:
563
+ message_index = None
564
+ for i, message in enumerate(self.messages):
565
+ if str(message["_id"]) == str(message_id):
566
+ message_index = i
567
+ break
568
+
569
+ if message_index is None:
570
+ logger.error(f"Message not found: {message_id}")
571
+ return False
572
+
573
+ message = self.messages[message_index]
574
+
575
+ if not message.get("versions") or version_number > len(message["versions"]) or version_number < 1:
576
+ logger.error(f"Version {version_number} not found for message {message_id}")
577
+ return False
578
+
579
+ current_version = message.get("current_version", 1)
580
+ logger.info(f"Switching message {message_id} from version {current_version} to version {version_number}")
581
+
582
+ selected_version = message["versions"][version_number - 1]
583
+
584
+ # Update message content to selected version
585
+ message["current_version"] = version_number
586
+ message["content"] = selected_version["content"]
587
+
588
+ # Update sources and metadata
589
+ if "sources" in selected_version:
590
+ message["sources"] = selected_version["sources"]
591
+ elif "sources" in message:
592
+ del message["sources"]
593
+
594
+ if "metadata" in selected_version:
595
+ message["metadata"] = selected_version["metadata"]
596
+ elif "metadata" in message:
597
+ del message["metadata"]
598
+
599
+ # Restore following messages từ version được chọn
600
+ following_messages = selected_version.get("following_messages", [])
601
+ if following_messages:
602
+ restored_count = self.restore_following_messages(message_index, following_messages)
603
+ logger.info(f"Restored {restored_count} following messages from version {version_number}")
604
+ else:
605
+ # Nếu không có following messages, chỉ cắt conversation tại message này
606
+ self.messages = self.messages[:message_index + 1]
607
+ logger.info(f"No following messages in version {version_number}, truncated conversation at message index {message_index}")
608
+
609
+ self.updated_at = datetime.datetime.now()
610
+ self.save()
611
+
612
+ logger.info(f"Successfully switched to version {version_number} for message {message_id}")
613
+ return True
614
+
615
+ except Exception as e:
616
+ logger.error(f"Error switching message version: {e}")
617
+ return False
618
+
619
+ def delete_message_and_following(self, message_id):
620
+ """Xóa tin nhắn và tất cả tin nhắn sau nó"""
621
+ try:
622
+ message_index = None
623
+ for i, message in enumerate(self.messages):
624
+ if str(message["_id"]) == str(message_id):
625
+ message_index = i
626
+ break
627
+
628
+ if message_index is None:
629
+ return False, "Không tìm thấy tin nhắn"
630
+
631
+ self.messages = self.messages[:message_index]
632
+ self.updated_at = datetime.datetime.now()
633
+ self.save()
634
+
635
+ return True, "Đã xóa tin nhắn và các tin nhắn sau nó"
636
+
637
+ except Exception as e:
638
+ logger.error(f"Error deleting message: {e}")
639
+ return False, f"Lỗi: {str(e)}"
640
+
641
+ def delete(self):
642
+ """Xóa cuộc hội thoại từ database"""
643
+ try:
644
+ if self.conversation_id:
645
+ db = get_db()
646
+ conversations_collection = db.conversations
647
+ conversations_collection.delete_one({"_id": self.conversation_id})
648
+ logger.info(f"Đã xóa cuộc hội thoại: {self.conversation_id}")
649
+ return True
650
+ return False
651
+ except Exception as e:
652
+ logger.error(f"Lỗi khi xóa cuộc hội thoại: {e}")
653
+ return False
654
+
655
+ @classmethod
656
+ def find_by_id(cls, conversation_id):
657
+ """Tìm cuộc hội thoại theo ID"""
658
+ try:
659
+ db = get_db()
660
+ conversations_collection = db.conversations
661
+
662
+ if isinstance(conversation_id, str):
663
+ conversation_id = ObjectId(conversation_id)
664
+
665
+ conversation_dict = conversations_collection.find_one({"_id": conversation_id})
666
+
667
+ if conversation_dict:
668
+ return cls.from_dict(conversation_dict)
669
+ return None
670
+ except Exception as e:
671
+ logger.error(f"Lỗi khi tìm cuộc hội thoại: {e}")
672
+ return None
673
+
674
+ @classmethod
675
+ def find_by_user(cls, user_id, limit=50, skip=0, include_archived=False):
676
+ """Tìm cuộc hội thoại theo user_id"""
677
+ try:
678
+ db = get_db()
679
+ conversations_collection = db.conversations
680
+
681
+ if isinstance(user_id, str):
682
+ user_id = ObjectId(user_id)
683
+
684
+ query_filter = {"user_id": user_id}
685
+ if not include_archived:
686
+ query_filter["is_archived"] = {"$ne": True}
687
+
688
+ logger.info(f"Querying conversations with filter: {query_filter}, limit: {limit}, skip: {skip}")
689
+
690
+ conversations_cursor = conversations_collection.find(query_filter)\
691
+ .sort("updated_at", DESCENDING)\
692
+ .skip(skip)\
693
+ .limit(limit)
694
+
695
+ conversations_list = list(conversations_cursor)
696
+ logger.info(f"Found {len(conversations_list)} conversations for user {user_id}")
697
+
698
+ result = []
699
+ for conv_dict in conversations_list:
700
+ conv_obj = cls.from_dict(conv_dict)
701
+ if conv_obj:
702
+ result.append(conv_obj)
703
+
704
+ logger.info(f"Returning {len(result)} conversation objects")
705
+ return result
706
+
707
+ except Exception as e:
708
+ logger.error(f"Error finding conversations by user: {e}")
709
+ return []
710
+
711
+ @classmethod
712
+ def count_by_user(cls, user_id, include_archived=False):
713
+ """Đếm số cuộc hội thoại của user"""
714
+ try:
715
+ db = get_db()
716
+ conversations_collection = db.conversations
717
+
718
+ if isinstance(user_id, str):
719
+ user_id = ObjectId(user_id)
720
+
721
+ query_filter = {"user_id": user_id}
722
+ if not include_archived:
723
+ query_filter["is_archived"] = {"$ne": True}
724
+
725
+ count = conversations_collection.count_documents(query_filter)
726
+ logger.info(f"User {user_id} has {count} conversations (include_archived: {include_archived})")
727
+
728
+ return count
729
+ except Exception as e:
730
+ logger.error(f"Error counting conversations: {e}")
731
+ return 0
models/feedback_model.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import datetime
3
+ from pymongo import MongoClient
4
+ from bson.objectid import ObjectId
5
+ import logging
6
+ from dotenv import load_dotenv
7
+
8
+ # Cấu hình logging
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Tải biến môi trường
12
+ load_dotenv()
13
+
14
+ # Kết nối MongoDB
15
+ MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/")
16
+ DATABASE_NAME = os.getenv("MONGO_DB_NAME", "nutribot_db")
17
+
18
+ # Singleton pattern cho kết nối MongoDB
19
+ _mongo_client = None
20
+ _db = None
21
+
22
+ def get_db():
23
+ """Trả về instance của MongoDB database (singleton pattern)"""
24
+ global _mongo_client, _db
25
+ if _mongo_client is None:
26
+ try:
27
+ _mongo_client = MongoClient(MONGO_URI)
28
+ _db = _mongo_client[DATABASE_NAME]
29
+ logger.info(f"Đã kết nối đến database: {DATABASE_NAME}")
30
+ except Exception as e:
31
+ logger.error(f"Lỗi kết nối MongoDB: {e}")
32
+ raise
33
+ return _db
34
+
35
+ def ensure_indexes():
36
+ """Tạo các index cần thiết cho feedback collection"""
37
+ try:
38
+ db = get_db()
39
+ feedback_collection = db.feedback
40
+
41
+ # Index cho user_id
42
+ feedback_collection.create_index("user_id")
43
+
44
+ # Index cho status
45
+ feedback_collection.create_index("status")
46
+
47
+ # Index cho category
48
+ feedback_collection.create_index("category")
49
+
50
+ # Index cho created_at (để sắp xếp)
51
+ feedback_collection.create_index([("created_at", -1)])
52
+
53
+ logger.info("Đã tạo indexes cho feedback collection")
54
+
55
+ except Exception as e:
56
+ logger.error(f"Lỗi tạo indexes: {e}")
57
+
58
+ class Feedback:
59
+ def __init__(self, user_id=None, rating=5, category='', title='', content='',
60
+ status='pending', admin_response='', created_at=None, updated_at=None,
61
+ feedback_id=None):
62
+ self.feedback_id = feedback_id
63
+ self.user_id = user_id
64
+ self.rating = rating
65
+ self.category = category
66
+ self.title = title
67
+ self.content = content
68
+ self.status = status # pending, reviewed, resolved
69
+ self.admin_response = admin_response
70
+ self.created_at = created_at or datetime.datetime.now()
71
+ self.updated_at = updated_at or datetime.datetime.now()
72
+
73
+ def to_dict(self):
74
+ """Chuyển đổi thông tin feedback thành dictionary"""
75
+ feedback_dict = {
76
+ "user_id": self.user_id,
77
+ "rating": self.rating,
78
+ "category": self.category,
79
+ "title": self.title,
80
+ "content": self.content,
81
+ "status": self.status,
82
+ "admin_response": self.admin_response,
83
+ "created_at": self.created_at,
84
+ "updated_at": self.updated_at
85
+ }
86
+ if self.feedback_id:
87
+ feedback_dict["_id"] = self.feedback_id
88
+ return feedback_dict
89
+
90
+ @classmethod
91
+ def from_dict(cls, feedback_dict):
92
+ """Tạo đối tượng Feedback từ dictionary"""
93
+ if not feedback_dict:
94
+ return None
95
+
96
+ return cls(
97
+ feedback_id=feedback_dict.get("_id"),
98
+ user_id=feedback_dict.get("user_id"),
99
+ rating=feedback_dict.get("rating", 5),
100
+ category=feedback_dict.get("category", ""),
101
+ title=feedback_dict.get("title", ""),
102
+ content=feedback_dict.get("content", ""),
103
+ status=feedback_dict.get("status", "pending"),
104
+ admin_response=feedback_dict.get("admin_response", ""),
105
+ created_at=feedback_dict.get("created_at"),
106
+ updated_at=feedback_dict.get("updated_at")
107
+ )
108
+
109
+ def save(self):
110
+ """Lưu thông tin feedback vào database"""
111
+ try:
112
+ db = get_db()
113
+ feedback_collection = db.feedback
114
+
115
+ self.updated_at = datetime.datetime.now()
116
+
117
+ if not self.feedback_id:
118
+ insert_result = feedback_collection.insert_one(self.to_dict())
119
+ self.feedback_id = insert_result.inserted_id
120
+ logger.info(f"Đã tạo feedback mới với ID: {self.feedback_id}")
121
+ return self.feedback_id
122
+ else:
123
+ feedback_collection.update_one(
124
+ {"_id": self.feedback_id},
125
+ {"$set": self.to_dict()}
126
+ )
127
+ logger.info(f"Đã cập nhật feedback: {self.feedback_id}")
128
+ return self.feedback_id
129
+ except Exception as e:
130
+ logger.error(f"Lỗi khi lưu feedback: {e}")
131
+ raise
132
+
133
+ @classmethod
134
+ def find_by_id(cls, feedback_id):
135
+ """Tìm feedback theo ID"""
136
+ try:
137
+ db = get_db()
138
+ feedback_collection = db.feedback
139
+
140
+ if isinstance(feedback_id, str):
141
+ feedback_id = ObjectId(feedback_id)
142
+
143
+ feedback_dict = feedback_collection.find_one({"_id": feedback_id})
144
+
145
+ if feedback_dict:
146
+ return cls.from_dict(feedback_dict)
147
+ return None
148
+ except Exception as e:
149
+ logger.error(f"Lỗi khi tìm feedback: {e}")
150
+ return None
151
+
152
+ @classmethod
153
+ def find_by_user(cls, user_id, limit=10, skip=0):
154
+ """Tìm feedback theo user_id"""
155
+ try:
156
+ db = get_db()
157
+ feedback_collection = db.feedback
158
+
159
+ if isinstance(user_id, str):
160
+ user_id = ObjectId(user_id)
161
+
162
+ feedbacks_cursor = feedback_collection.find({"user_id": user_id})\
163
+ .sort("created_at", -1)\
164
+ .skip(skip)\
165
+ .limit(limit)
166
+
167
+ result = []
168
+ for feedback_dict in feedbacks_cursor:
169
+ feedback_obj = cls.from_dict(feedback_dict)
170
+ if feedback_obj:
171
+ result.append(feedback_obj)
172
+
173
+ return result
174
+
175
+ except Exception as e:
176
+ logger.error(f"Lỗi tìm feedback theo user: {e}")
177
+ return []
178
+
179
+ @classmethod
180
+ def get_all_for_admin(cls, limit=20, skip=0, status_filter=None):
181
+ """Lấy tất cả feedback cho admin với join user info"""
182
+ try:
183
+ db = get_db()
184
+ feedback_collection = db.feedback
185
+
186
+ # Tạo query filter
187
+ query_filter = {}
188
+ if status_filter:
189
+ query_filter["status"] = status_filter
190
+
191
+ # Aggregate để join với user collection
192
+ pipeline = [
193
+ {"$match": query_filter},
194
+ {
195
+ "$lookup": {
196
+ "from": "users",
197
+ "localField": "user_id",
198
+ "foreignField": "_id",
199
+ "as": "user_info"
200
+ }
201
+ },
202
+ {"$sort": {"created_at": -1}},
203
+ {"$skip": skip},
204
+ {"$limit": limit}
205
+ ]
206
+
207
+ result = []
208
+ for feedback_doc in feedback_collection.aggregate(pipeline):
209
+ feedback = cls.from_dict(feedback_doc)
210
+
211
+ # Thêm thông tin user nếu có
212
+ if feedback and feedback_doc.get("user_info"):
213
+ user_info = feedback_doc["user_info"][0]
214
+ feedback.user_name = user_info.get("name", "Ẩn danh")
215
+ feedback.user_email = user_info.get("email", "")
216
+ else:
217
+ feedback.user_name = "Ẩn danh"
218
+ feedback.user_email = ""
219
+
220
+ result.append(feedback)
221
+
222
+ return result
223
+
224
+ except Exception as e:
225
+ logger.error(f"Lỗi lấy feedback cho admin: {e}")
226
+ return []
227
+
228
+ def update_admin_response(self, response, status):
229
+ """Cập nhật phản hồi từ admin"""
230
+ try:
231
+ self.admin_response = response
232
+ self.status = status
233
+ self.save()
234
+ return True
235
+ except Exception as e:
236
+ logger.error(f"Lỗi cập nhật admin response: {e}")
237
+ return False
238
+
239
+ @staticmethod
240
+ def get_stats():
241
+ """Lấy thống kê về feedback"""
242
+ try:
243
+ db = get_db()
244
+ feedback_collection = db.feedback
245
+
246
+ # Tổng số feedback
247
+ total_feedback = feedback_collection.count_documents({})
248
+
249
+ # Feedback pending
250
+ pending_feedback = feedback_collection.count_documents({"status": "pending"})
251
+
252
+ # Feedback trong tháng này
253
+ now = datetime.datetime.now()
254
+ month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
255
+ this_month_feedback = feedback_collection.count_documents({
256
+ "created_at": {"$gte": month_start}
257
+ })
258
+
259
+ # Đánh giá trung bình
260
+ pipeline = [
261
+ {"$group": {"_id": None, "avg_rating": {"$avg": "$rating"}}}
262
+ ]
263
+ rating_result = list(feedback_collection.aggregate(pipeline))
264
+ average_rating = rating_result[0]["avg_rating"] if rating_result else 0
265
+
266
+ return {
267
+ "total_feedback": total_feedback,
268
+ "pending_feedback": pending_feedback,
269
+ "this_month_feedback": this_month_feedback,
270
+ "average_rating": average_rating
271
+ }
272
+
273
+ except Exception as e:
274
+ logger.error(f"Lỗi lấy thống kê feedback: {e}")
275
+ return {
276
+ "total_feedback": 0,
277
+ "pending_feedback": 0,
278
+ "this_month_feedback": 0,
279
+ "average_rating": 0
280
+ }
281
+
282
+ @staticmethod
283
+ def create_feedback(user_id, rating, category, title, content):
284
+ """Tạo feedback mới"""
285
+ try:
286
+ feedback = Feedback(
287
+ user_id=ObjectId(user_id) if isinstance(user_id, str) else user_id,
288
+ rating=rating,
289
+ category=category,
290
+ title=title,
291
+ content=content
292
+ )
293
+
294
+ feedback_id = feedback.save()
295
+ return True, {"feedback_id": str(feedback_id)}
296
+
297
+ except Exception as e:
298
+ logger.error(f"Lỗi tạo feedback: {e}")
299
+ return False, str(e)
models/user_model.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import datetime
3
+ from pymongo import MongoClient
4
+ from bson.objectid import ObjectId
5
+ import bcrypt
6
+ import logging
7
+ from dotenv import load_dotenv
8
+
9
+ # Cấu hình logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Tải biến môi trường
13
+ load_dotenv()
14
+
15
+ # Kết nối MongoDB
16
+ MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/")
17
+ DATABASE_NAME = os.getenv("MONGO_DB_NAME", "nutribot_db")
18
+
19
+ # Singleton pattern cho kết nối MongoDB
20
+ _mongo_client = None
21
+ _db = None
22
+
23
+ def get_db():
24
+ """Trả về instance của MongoDB database (singleton pattern)"""
25
+ global _mongo_client, _db
26
+ if _mongo_client is None:
27
+ try:
28
+ _mongo_client = MongoClient(MONGO_URI)
29
+ _db = _mongo_client[DATABASE_NAME]
30
+ logger.info(f"Đã kết nối đến database: {DATABASE_NAME}")
31
+ except Exception as e:
32
+ logger.error(f"Lỗi kết nối MongoDB: {e}")
33
+ raise
34
+ return _db
35
+
36
+ class User:
37
+ def __init__(self, name, email, password, gender=None, role="user", permissions=None,
38
+ user_id=None, created_at=None, updated_at=None, last_login=None):
39
+ self.user_id = user_id
40
+ self.name = name
41
+ self.email = email
42
+ self.password = password # Mật khẩu đã mã hóa
43
+ self.gender = gender
44
+ self.role = role # "user" hoặc "admin"
45
+ self.permissions = permissions or self._get_default_permissions(role)
46
+ self.created_at = created_at or datetime.datetime.now()
47
+ self.updated_at = updated_at or datetime.datetime.now()
48
+ self.last_login = last_login
49
+
50
+ def _get_default_permissions(self, role):
51
+ """Lấy quyền mặc định theo role"""
52
+ if role == "admin":
53
+ return {
54
+ "users": {"read": True, "write": True, "delete": True},
55
+ "documents": {"read": True, "write": True, "delete": True},
56
+ "conversations": {"read": True, "write": True, "delete": True},
57
+ "system": {"read": True, "write": True, "delete": False},
58
+ "analytics": {"read": True, "write": False, "delete": False}
59
+ }
60
+ else: # role == "user"
61
+ return {
62
+ "conversations": {"read": True, "write": True, "delete": True},
63
+ "profile": {"read": True, "write": True, "delete": False}
64
+ }
65
+
66
+ @staticmethod
67
+ def hash_password(password):
68
+ """Mã hóa mật khẩu sử dụng bcrypt"""
69
+ salt = bcrypt.gensalt()
70
+ hashed_pw = bcrypt.hashpw(password.encode('utf-8'), salt)
71
+ return hashed_pw
72
+
73
+ @staticmethod
74
+ def check_password(hashed_password, password):
75
+ """Kiểm tra mật khẩu"""
76
+ return bcrypt.checkpw(password.encode('utf-8'), hashed_password)
77
+
78
+ def is_admin(self):
79
+ """Kiểm tra xem user có phải admin không"""
80
+ return self.role == "admin"
81
+
82
+ def has_permission(self, resource, action):
83
+ """Kiểm tra quyền truy cập"""
84
+ if not self.permissions:
85
+ return False
86
+
87
+ resource_perms = self.permissions.get(resource, {})
88
+ return resource_perms.get(action, False)
89
+
90
+ def to_dict(self):
91
+ """Chuyển đổi thông tin user thành dictionary"""
92
+ user_dict = {
93
+ "name": self.name,
94
+ "email": self.email,
95
+ "password": self.password,
96
+ "gender": self.gender,
97
+ "role": self.role,
98
+ "permissions": self.permissions,
99
+ "created_at": self.created_at,
100
+ "updated_at": self.updated_at,
101
+ "last_login": self.last_login
102
+ }
103
+ if self.user_id:
104
+ user_dict["_id"] = self.user_id
105
+ return user_dict
106
+
107
+ @classmethod
108
+ def from_dict(cls, user_dict):
109
+ """Tạo đối tượng User từ dictionary"""
110
+ if not user_dict:
111
+ return None
112
+
113
+ return cls(
114
+ user_id=user_dict.get("_id"),
115
+ name=user_dict.get("name"),
116
+ email=user_dict.get("email"),
117
+ password=user_dict.get("password"),
118
+ gender=user_dict.get("gender"),
119
+ role=user_dict.get("role", "user"), # Mặc định là user
120
+ permissions=user_dict.get("permissions"),
121
+ created_at=user_dict.get("created_at"),
122
+ updated_at=user_dict.get("updated_at"),
123
+ last_login=user_dict.get("last_login")
124
+ )
125
+
126
+ @classmethod
127
+ def find_by_email(cls, email):
128
+ """Tìm người dùng theo email"""
129
+ try:
130
+ db = get_db()
131
+ users_collection = db.users
132
+ user_data = users_collection.find_one({"email": email})
133
+ return cls.from_dict(user_data)
134
+ except Exception as e:
135
+ logger.error(f"Lỗi tìm người dùng theo email: {e}")
136
+ return None
137
+
138
+ @classmethod
139
+ def find_by_id(cls, user_id):
140
+ """Tìm người dùng theo ID"""
141
+ try:
142
+ db = get_db()
143
+ users_collection = db.users
144
+ user_data = users_collection.find_one({"_id": ObjectId(user_id)})
145
+ return cls.from_dict(user_data)
146
+ except Exception as e:
147
+ logger.error(f"Lỗi tìm người dùng theo ID: {e}")
148
+ return None
149
+
150
+ @classmethod
151
+ def get_all_admins(cls, page=1, per_page=10):
152
+ """Lấy danh sách tất cả admin với phân trang"""
153
+ try:
154
+ db = get_db()
155
+ users_collection = db.users
156
+
157
+ skip = (page - 1) * per_page
158
+ admins_data = users_collection.find(
159
+ {"role": "admin"}
160
+ ).sort("created_at", -1).skip(skip).limit(per_page)
161
+
162
+ total = users_collection.count_documents({"role": "admin"})
163
+
164
+ return [cls.from_dict(admin) for admin in admins_data], total
165
+ except Exception as e:
166
+ logger.error(f"Lỗi lấy danh sách admin: {e}")
167
+ return [], 0
168
+
169
+ def save(self):
170
+ """Lưu thông tin người dùng vào database"""
171
+ try:
172
+ db = get_db()
173
+ users_collection = db.users
174
+
175
+ if not self.user_id:
176
+ # Đây là người dùng mới
177
+ insert_result = users_collection.insert_one(self.to_dict())
178
+ self.user_id = insert_result.inserted_id
179
+ logger.info(f"Đã tạo người dùng mới với ID: {self.user_id}")
180
+ return self.user_id
181
+ else:
182
+ # Cập nhật người dùng đã tồn tại
183
+ self.updated_at = datetime.datetime.now()
184
+ users_collection.update_one(
185
+ {"_id": self.user_id},
186
+ {"$set": self.to_dict()}
187
+ )
188
+ logger.info(f"Đã cập nhật thông tin người dùng: {self.user_id}")
189
+ return self.user_id
190
+ except Exception as e:
191
+ logger.error(f"Lỗi khi lưu thông tin người dùng: {e}")
192
+ raise
193
+
194
+ def update_last_login(self):
195
+ """Cập nhật thời gian đăng nhập cuối"""
196
+ try:
197
+ self.last_login = datetime.datetime.now()
198
+ self.save()
199
+ except Exception as e:
200
+ logger.error(f"Lỗi cập nhật last_login: {e}")
201
+
202
+ def delete(self):
203
+ """Xóa người dùng từ database"""
204
+ try:
205
+ if self.user_id:
206
+ db = get_db()
207
+ users_collection = db.users
208
+ users_collection.delete_one({"_id": self.user_id})
209
+ logger.info(f"Đã xóa người dùng: {self.user_id}")
210
+ return True
211
+ return False
212
+ except Exception as e:
213
+ logger.error(f"Lỗi khi xóa người dùng: {e}")
214
+ return False
215
+
216
+ @staticmethod
217
+ def register(name, email, password, gender=None, role="user"):
218
+ """Đăng ký người dùng mới"""
219
+ try:
220
+ # Kiểm tra email đã tồn tại chưa
221
+ existing_user = User.find_by_email(email)
222
+ if existing_user:
223
+ return False, "Email đã được sử dụng"
224
+
225
+ # Mã hóa mật khẩu
226
+ hashed_password = User.hash_password(password)
227
+
228
+ # Tạo người dùng mới
229
+ new_user = User(
230
+ name=name,
231
+ email=email,
232
+ password=hashed_password,
233
+ gender=gender,
234
+ role=role
235
+ )
236
+
237
+ # Lưu vào database
238
+ user_id = new_user.save()
239
+
240
+ return True, {"user_id": str(user_id)}
241
+ except Exception as e:
242
+ logger.error(f"Lỗi đăng ký người dùng: {e}")
243
+ return False, f"Lỗi đăng ký: {str(e)}"
244
+
245
+ @staticmethod
246
+ def login(email, password):
247
+ """Đăng nhập người dùng"""
248
+ try:
249
+ if not email or not password:
250
+ return False, "Vui lòng nhập đầy đủ thông tin đăng nhập"
251
+
252
+ user = User.find_by_email(email)
253
+ if not user:
254
+ return False, "Email không tồn tại"
255
+
256
+ if not User.check_password(user.password, password):
257
+ return False, "Mật khẩu không chính xác"
258
+
259
+ # Cập nhật thời gian đăng nhập
260
+ user.update_last_login()
261
+
262
+ return True, user
263
+ except Exception as e:
264
+ logger.error(f"Lỗi đăng nhập: {e}")
265
+ return False, f"Lỗi đăng nhập: {str(e)}"
266
+
267
+ @staticmethod
268
+ def create_admin(name, email, password, gender=None):
269
+ """Tạo admin mới"""
270
+ return User.register(name, email, password, gender, role="admin")
271
+
272
+ @staticmethod
273
+ def create_default_admin():
274
+ """Tạo admin mặc định nếu chưa có"""
275
+ try:
276
+ db = get_db()
277
+ users_collection = db.users
278
+
279
+ # Kiểm tra xem đã có admin chưa
280
+ existing_admin = users_collection.find_one({"role": "admin"})
281
+
282
+ if not existing_admin:
283
+ # Tạo admin mặc định
284
+ default_email = "[email protected]"
285
+ default_password = "NutribotAdmin2024!"
286
+
287
+ success, result = User.create_admin(
288
+ name="Administrator",
289
+ email=default_email,
290
+ password=default_password
291
+ )
292
+
293
+ if success:
294
+ logger.info(f"Đã tạo admin mặc định: {default_email}")
295
+ logger.info(f"Mật khẩu mặc định: {default_password}")
296
+ return True, {
297
+ "email": default_email,
298
+ "password": default_password,
299
+ "user_id": result["user_id"]
300
+ }
301
+ else:
302
+ logger.error(f"Lỗi tạo admin mặc định: {result}")
303
+ return False, result
304
+ else:
305
+ logger.info("Admin đã tồn tại")
306
+ return True, {"message": "Admin đã tồn tại"}
307
+
308
+ except Exception as e:
309
+ logger.error(f"Lỗi tạo admin mặc định: {e}")
310
+ return False, str(e)
311
+
312
+ @staticmethod
313
+ def get_stats():
314
+ """Lấy thống kê về users"""
315
+ try:
316
+ db = get_db()
317
+ users_collection = db.users
318
+
319
+ # Tổng số users
320
+ total_users = users_collection.count_documents({})
321
+
322
+ # Số admin
323
+ total_admins = users_collection.count_documents({"role": "admin"})
324
+
325
+ # Users đăng ký hôm nay
326
+ today_start = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
327
+ new_users_today = users_collection.count_documents({
328
+ "created_at": {"$gte": today_start}
329
+ })
330
+
331
+ # Users có hoạt động (có last_login)
332
+ active_users = users_collection.count_documents({
333
+ "last_login": {"$exists": True}
334
+ })
335
+
336
+ return {
337
+ "total_users": total_users,
338
+ "total_admins": total_admins,
339
+ "active_users": active_users,
340
+ "new_users_today": new_users_today
341
+ }
342
+ except Exception as e:
343
+ logger.error(f"Lỗi lấy thống kê users: {e}")
344
+ return {
345
+ "total_users": 0,
346
+ "total_admins": 0,
347
+ "active_users": 0,
348
+ "new_users_today": 0
349
+ }
requirements.txt ADDED
Binary file (5.63 kB). View file
 
scripts/__init__.py ADDED
File without changes
scripts/embed_data.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import argparse
5
+ import logging
6
+
7
+ # Set UTF-8 encoding cho console
8
+ os.environ['PYTHONIOENCODING'] = 'utf-8'
9
+
10
+ # Thêm thư mục cha vào sys.path để import các module từ thư mục backend
11
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
+
13
+ from core.data_processor import DataProcessor
14
+ from core.embedding_model import get_embedding_model
15
+
16
+ # Cấu hình logging với UTF-8
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
20
+ handlers=[
21
+ logging.StreamHandler(),
22
+ logging.FileHandler("embed_data.log", encoding='utf-8')
23
+ ]
24
+ )
25
+ logger = logging.getLogger("embed_data")
26
+
27
+ def embed_all_data(data_dir, force=False):
28
+ """
29
+ Embedding tất cả dữ liệu từ thư mục data
30
+
31
+ Args:
32
+ data_dir: Đường dẫn đến thư mục chứa dữ liệu
33
+ force: Nếu True, sẽ xóa và tạo lại chỉ mục hiện có
34
+ """
35
+ logger.info(f"Bat dau qua trinh embedding du lieu tu {data_dir}")
36
+ start_time = time.time()
37
+
38
+ # Khởi tạo các components
39
+ data_processor = DataProcessor(data_dir=data_dir)
40
+ embedding_model = get_embedding_model()
41
+
42
+ # Kiểm tra xem có chỉ mục hiện có không
43
+ collection_size = embedding_model.count()
44
+ if collection_size > 0 and not force:
45
+ logger.info(f"Da ton tai chi muc voi {collection_size} items")
46
+ end_time = time.time()
47
+ logger.info(f"Hoan thanh kiem tra chi muc trong {end_time - start_time:.2f} giay")
48
+ logger.info("Su dung --force de tao lai chi muc")
49
+ return
50
+
51
+ # Nếu buộc tạo lại hoặc chưa có chỉ mục, tạo mới
52
+ if force:
53
+ logger.info("Xoa chi muc cu va tao lai...")
54
+ try:
55
+ embedding_model.chroma_client.delete_collection(name=embedding_model.collection.name)
56
+ # Tạo lại collection mới
57
+ embedding_model.collection = embedding_model.chroma_client.create_collection(name=embedding_model.collection.name)
58
+ logger.info("Da xoa va tao lai collection")
59
+ except Exception as e:
60
+ logger.error(f"Loi khi xoa collection: {e}")
61
+
62
+ # Chuẩn bị dữ liệu cho embedding
63
+ logger.info("Dang chuan bi du lieu cho qua trinh embedding...")
64
+ all_items = data_processor.prepare_for_embedding()
65
+ logger.info(f"Da chuan bi {len(all_items)} items de embedding")
66
+
67
+ # Thống kê các loại dữ liệu
68
+ text_chunks = len([item for item in all_items if item.get("metadata", {}).get("content_type") == "text"])
69
+ tables = len([item for item in all_items if item.get("metadata", {}).get("content_type") == "table"])
70
+ figures = len([item for item in all_items if item.get("metadata", {}).get("content_type") == "figure"])
71
+
72
+ logger.info(f"Bao gom: {text_chunks} van ban, {tables} bang bieu, {figures} hinh anh")
73
+
74
+ # Thực hiện embedding
75
+ logger.info("Bat dau qua trinh embedding...")
76
+ batch_size = 50 # Batch size phù hợp, tùy chỉnh nếu cần
77
+
78
+ # Chia thành các batch nhỏ để xử lý
79
+ for i in range(0, len(all_items), batch_size):
80
+ batch = all_items[i:i + batch_size]
81
+ logger.info(f"Dang xu ly batch {i//batch_size + 1}/{(len(all_items) + batch_size - 1)//batch_size}, kich thuoc: {len(batch)}")
82
+ success = embedding_model.index_chunks(batch)
83
+ if not success:
84
+ logger.error(f"Loi xu ly batch {i//batch_size + 1}")
85
+ return
86
+
87
+ end_time = time.time()
88
+ elapsed_time = end_time - start_time
89
+
90
+ # Kiểm tra kết quả cuối cùng
91
+ final_count = embedding_model.count()
92
+ logger.info(f"Hoan thanh qua trinh embedding {final_count} items trong {elapsed_time:.2f} giay")
93
+
94
+ if __name__ == "__main__":
95
+ parser = argparse.ArgumentParser(description="Embedding du lieu cho he thong Nutribot")
96
+ parser.add_argument("--data-dir", type=str, default="data",
97
+ help="Duong dan den thu muc chua du lieu (mac dinh: data)")
98
+ parser.add_argument("--force", action="store_true",
99
+ help="Xoa va tao lai chi muc neu da ton tai")
100
+
101
+ args = parser.parse_args()
102
+
103
+ # Chuẩn hóa đường dẫn
104
+ data_dir = os.path.abspath(args.data_dir)
105
+
106
+ # Kiểm tra thư mục data có tồn tại không
107
+ if not os.path.exists(data_dir):
108
+ logger.error(f"Thu muc {data_dir} khong ton tai!")
109
+ sys.exit(1)
110
+
111
+ # Thực hiện embedding
112
+ embed_all_data(data_dir, args.force)
scripts/init_admin.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script khởi tạo admin mặc định cho hệ thống Nutribot
4
+ Chạy script này để tạo tài khoản admin đầu tiên
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import logging
10
+
11
+ # Thêm thư mục cha vào sys.path
12
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+
14
+ from models.user_model import User
15
+
16
+ # Cấu hình logging
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format='%(asctime)s - %(levelname)s - %(message)s'
20
+ )
21
+ logger = logging.getLogger(__name__)
22
+
23
+ def create_default_admin():
24
+ """Tạo admin mặc định"""
25
+ try:
26
+ print("=" * 50)
27
+ print("KHỞI TẠO ADMIN NUTRIBOT")
28
+ print("=" * 50)
29
+
30
+ # Kiểm tra xem đã có admin chưa
31
+ from models.user_model import get_db
32
+ db = get_db()
33
+ existing_admin = db.users.find_one({"role": "admin"})
34
+
35
+ if existing_admin:
36
+ print("❌ Đã có admin trong hệ thống!")
37
+ print(f"Admin hiện tại: {existing_admin.get('name')} ({existing_admin.get('email')})")
38
+
39
+ choice = input("\nBạn có muốn tạo admin mới không? (y/N): ").lower().strip()
40
+ if choice != 'y':
41
+ print("Hủy bỏ tạo admin.")
42
+ return
43
+
44
+ # Nhập thông tin admin
45
+ print("\nNhập thông tin cho admin mới:")
46
+
47
+ name = input("Họ tên: ").strip()
48
+ if not name:
49
+ name = "Administrator"
50
+ print(f"Sử dụng tên mặc định: {name}")
51
+
52
+ email = input("Email: ").strip()
53
+ if not email:
54
+ email = "[email protected]"
55
+ print(f"Sử dụng email mặc định: {email}")
56
+
57
+ # Kiểm tra email đã tồn tại
58
+ if User.find_by_email(email):
59
+ print(f"❌ Email {email} đã được sử dụng!")
60
+ return
61
+
62
+ password = input("Mật khẩu (tối thiểu 6 ký tự): ").strip()
63
+ if not password or len(password) < 6:
64
+ password = "Admin123!"
65
+ print(f"Sử dụng mật khẩu mặc định: {password}")
66
+
67
+ gender = input("Giới tính (male/female/other, có thể bỏ trống): ").strip()
68
+ if gender and gender not in ['male', 'female', 'other']:
69
+ gender = None
70
+
71
+ # Tạo admin
72
+ print("\nĐang tạo admin...")
73
+ success, result = User.create_admin(name, email, password, gender)
74
+
75
+ if success:
76
+ print("✅ Tạo admin thành công!")
77
+ print("\n" + "=" * 50)
78
+ print("THÔNG TIN ĐĂNG NHẬP ADMIN")
79
+ print("=" * 50)
80
+ print(f"Email: {email}")
81
+ print(f"Mật khẩu: {password}")
82
+ print(f"Tên: {name}")
83
+ print(f"ID: {result['user_id']}")
84
+ print("=" * 50)
85
+ print("\n⚠️ LƯU Ý:")
86
+ print("- Hãy ghi nhớ thông tin đăng nhập này")
87
+ print("- Nên đổi mật khẩu sau lần đăng nhập đầu tiên")
88
+ print("- Truy cập admin panel tại: http://localhost:5173/admin")
89
+ print()
90
+ else:
91
+ print(f"❌ Lỗi tạo admin: {result}")
92
+
93
+ except Exception as e:
94
+ logger.error(f"Lỗi tạo admin: {e}")
95
+ print(f"❌ Có lỗi xảy ra: {e}")
96
+
97
+ def main():
98
+ """Hàm main"""
99
+ try:
100
+ create_default_admin()
101
+ except KeyboardInterrupt:
102
+ print("\n\nĐã hủy bỏ tạo admin.")
103
+ except Exception as e:
104
+ print(f"\nLỗi: {e}")
105
+
106
+ if __name__ == "__main__":
107
+ main()
testapi.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ import os
3
+ from dotenv import load_dotenv
4
+ load_dotenv()
5
+ OPENAI_API_KEY=os.getenv("OPENAI_API_KEY")
6
+ client = OpenAI(
7
+ api_key = OPENAI_API_KEY
8
+ )
9
+ completion = client.chat.completions.create(
10
+ model="gpt-4o-mini",
11
+ store=True,
12
+ messages=[
13
+ {"role": "user", "content": "What is the capital of Vietnam?"}
14
+ ]
15
+ )
16
+
17
+ print(completion.choices[0].message)