Spaces:
Sleeping
Sleeping
| from flask import Blueprint, request, jsonify | |
| import logging | |
| import datetime | |
| import os | |
| from bson.objectid import ObjectId | |
| from models.user_model import User | |
| from models.conversation_model import get_db, Conversation | |
| from flask_jwt_extended import jwt_required, get_jwt_identity | |
| from functools import wraps | |
| import json | |
| from core.embedding_model import get_embedding_model | |
| from werkzeug.utils import secure_filename | |
| import google.generativeai as genai | |
| from config import GEMINI_API_KEY | |
| import time | |
| import sys | |
| from models.feedback_model import Feedback | |
| # Thiết lập logging | |
| logger = logging.getLogger(__name__) | |
| # Tạo blueprint | |
| admin_routes = Blueprint('admin', __name__) | |
| # Configure Gemini | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| def setup_debug_logging(): | |
| """Thiết lập logging debug cho Gemini response""" | |
| log_dir = "logs" | |
| if not os.path.exists(log_dir): | |
| os.makedirs(log_dir) | |
| return log_dir | |
| def save_gemini_response_to_file(response_text, parsed_data, doc_id): | |
| """Lưu response của Gemini vào file để debug""" | |
| try: | |
| log_dir = setup_debug_logging() | |
| timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") | |
| # Tạo tên file log | |
| log_filename = f"gemini_response_{doc_id}_{timestamp}.log" | |
| log_path = os.path.join(log_dir, log_filename) | |
| with open(log_path, 'w', encoding='utf-8') as f: | |
| f.write("=" * 80 + "\n") | |
| f.write(f"GEMINI RESPONSE DEBUG LOG\n") | |
| f.write(f"Document ID: {doc_id}\n") | |
| f.write(f"Timestamp: {datetime.datetime.now().isoformat()}\n") | |
| f.write("=" * 80 + "\n\n") | |
| f.write("RAW RESPONSE FROM GEMINI:\n") | |
| f.write("-" * 40 + "\n") | |
| f.write(response_text) | |
| f.write("\n\n") | |
| f.write("PARSED JSON DATA:\n") | |
| f.write("-" * 40 + "\n") | |
| f.write(json.dumps(parsed_data, indent=2, ensure_ascii=False)) | |
| f.write("\n\n") | |
| # Debug chunks content vs summary | |
| f.write("CHUNKS CONTENT vs SUMMARY ANALYSIS:\n") | |
| f.write("-" * 40 + "\n") | |
| for i, chunk in enumerate(parsed_data.get('chunks', [])[:3]): | |
| f.write(f"CHUNK {i+1} ({chunk.get('id', 'no-id')}):\n") | |
| f.write(f"Title: {chunk.get('title', 'no-title')}\n") | |
| f.write(f"Summary Length: {len(chunk.get('summary', ''))}\n") | |
| f.write(f"Content Length: {len(chunk.get('content', ''))}\n") | |
| f.write(f"Summary: {chunk.get('summary', 'no-summary')[:200]}...\n") | |
| f.write(f"Content: {chunk.get('content', 'no-content')[:200]}...\n") | |
| f.write("\n") | |
| # Debug tables | |
| if parsed_data.get('tables'): | |
| f.write("TABLES ANALYSIS:\n") | |
| f.write("-" * 40 + "\n") | |
| for i, table in enumerate(parsed_data.get('tables', [])[:2]): | |
| f.write(f"TABLE {i+1} ({table.get('id', 'no-id')}):\n") | |
| f.write(f"Title: {table.get('title', 'no-title')}\n") | |
| f.write(f"Summary: {table.get('summary', 'no-summary')[:100]}...\n") | |
| f.write(f"Content: {table.get('content', 'no-content')[:100]}...\n") | |
| f.write("\n") | |
| logger.info(f"Saved Gemini response debug log to: {log_path}") | |
| return log_path | |
| except Exception as e: | |
| logger.error(f"Error saving debug log: {e}") | |
| return None | |
| def require_admin(f): | |
| """Decorator để kiểm tra quyền admin""" | |
| def decorated_function(*args, **kwargs): | |
| try: | |
| user_id = get_jwt_identity() | |
| user = User.find_by_id(user_id) | |
| if not user or not user.is_admin(): | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không có quyền truy cập admin" | |
| }), 403 | |
| request.current_user = user | |
| return f(*args, **kwargs) | |
| except Exception as e: | |
| logger.error(f"Lỗi xác thực admin: {e}") | |
| return jsonify({ | |
| "success": False, | |
| "error": "Lỗi xác thực" | |
| }), 500 | |
| return decorated_function | |
| # ===== DASHBOARD ROUTES ===== | |
| def get_overview_stats(): | |
| """Lấy thống kê tổng quan cho dashboard""" | |
| try: | |
| db = get_db() | |
| # Sử dụng timezone Việt Nam (UTC+7) | |
| import pytz | |
| vietnam_tz = pytz.timezone('Asia/Ho_Chi_Minh') | |
| now_vietnam = datetime.datetime.now(vietnam_tz) | |
| # Thống kê users thực tế | |
| total_users = db.users.count_documents({}) if hasattr(db, 'users') else 0 | |
| today_start = now_vietnam.replace(hour=0, minute=0, second=0, microsecond=0) | |
| new_users_today = db.users.count_documents({ | |
| "created_at": {"$gte": today_start.astimezone(pytz.UTC).replace(tzinfo=None)} | |
| }) if hasattr(db, 'users') else 0 | |
| # Thống kê conversations thực tế | |
| total_conversations = db.conversations.count_documents({}) | |
| day_ago = now_vietnam - datetime.timedelta(days=1) | |
| recent_conversations = db.conversations.count_documents({ | |
| "updated_at": {"$gte": day_ago.astimezone(pytz.UTC).replace(tzinfo=None)} | |
| }) | |
| # Thống kê tin nhắn thực tế | |
| pipeline = [ | |
| {"$project": {"message_count": {"$size": "$messages"}}}, | |
| {"$group": {"_id": None, "total_messages": {"$sum": "$message_count"}}} | |
| ] | |
| message_result = list(db.conversations.aggregate(pipeline)) | |
| total_messages = message_result[0]["total_messages"] if message_result else 0 | |
| # Thống kê admin thực tế | |
| total_admins = db.users.count_documents({"role": "admin"}) if hasattr(db, 'users') else 0 | |
| # Thống kê theo tuần (7 ngày gần nhất) - SỬA LẠI | |
| daily_stats = [] | |
| for i in range(7): | |
| # Tính ngày theo timezone Việt Nam | |
| target_date = now_vietnam.date() - datetime.timedelta(days=6-i) | |
| day_start = vietnam_tz.localize(datetime.datetime.combine(target_date, datetime.time.min)) | |
| day_end = vietnam_tz.localize(datetime.datetime.combine(target_date, datetime.time.max)) | |
| # Convert về UTC để query MongoDB | |
| day_start_utc = day_start.astimezone(pytz.UTC).replace(tzinfo=None) | |
| day_end_utc = day_end.astimezone(pytz.UTC).replace(tzinfo=None) | |
| # Đếm conversations được tạo HOẶC cập nhật trong ngày | |
| daily_conversations_created = db.conversations.count_documents({ | |
| "created_at": {"$gte": day_start_utc, "$lte": day_end_utc} | |
| }) | |
| daily_conversations_updated = db.conversations.count_documents({ | |
| "updated_at": {"$gte": day_start_utc, "$lte": day_end_utc}, | |
| "created_at": {"$lt": day_start_utc} # Không đếm trùng với conversations mới tạo | |
| }) | |
| total_daily_activity = daily_conversations_created + daily_conversations_updated | |
| daily_users = 0 | |
| if hasattr(db, 'users'): | |
| daily_users = db.users.count_documents({ | |
| "created_at": {"$gte": day_start_utc, "$lte": day_end_utc} | |
| }) | |
| daily_stats.append({ | |
| "date": target_date.strftime("%Y-%m-%d"), | |
| "label": target_date.strftime("%d/%m"), | |
| "conversations": total_daily_activity, | |
| "users": daily_users, | |
| "created": daily_conversations_created, | |
| "updated": daily_conversations_updated | |
| }) | |
| # Thống kê theo độ tuổi thực tế | |
| age_pipeline = [ | |
| {"$group": {"_id": "$age_context", "count": {"$sum": 1}}}, | |
| {"$sort": {"_id": 1}} | |
| ] | |
| age_stats = list(db.conversations.aggregate(age_pipeline)) | |
| return jsonify({ | |
| "success": True, | |
| "stats": { | |
| "users": { | |
| "total": total_users, | |
| "new_today": new_users_today, | |
| "active": total_users | |
| }, | |
| "conversations": { | |
| "total": total_conversations, | |
| "recent": recent_conversations | |
| }, | |
| "messages": { | |
| "total": total_messages, | |
| "avg_per_conversation": round(total_messages / total_conversations, 1) if total_conversations > 0 else 0 | |
| }, | |
| "admins": { | |
| "total": total_admins | |
| }, | |
| "daily_stats": daily_stats, | |
| "age_distribution": [ | |
| { | |
| "age_group": f"{stat['_id']} tuổi" if stat['_id'] else "Không rõ", | |
| "count": stat["count"] | |
| } | |
| for stat in age_stats | |
| ], | |
| "timezone": "Asia/Ho_Chi_Minh", | |
| "current_time": now_vietnam.isoformat() | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy thống kê tổng quan: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_system_info(): | |
| """Lấy thông tin hệ thống""" | |
| try: | |
| from core.embedding_model import get_embedding_model | |
| # Thông tin vector database | |
| try: | |
| embedding_model = get_embedding_model() | |
| vector_count = embedding_model.count() | |
| except: | |
| vector_count = 0 | |
| # Thông tin database | |
| db = get_db() | |
| collections = db.list_collection_names() | |
| system_info = { | |
| "database": { | |
| "type": "MongoDB", | |
| "collections": len(collections), | |
| "status": "active" | |
| }, | |
| "vector_db": { | |
| "type": "ChromaDB", | |
| "embeddings": vector_count, | |
| "model": "multilingual-e5-base" | |
| }, | |
| "ai": { | |
| "generation_model": "Gemini 2.0 Flash", | |
| "embedding_model": "multilingual-e5-base", | |
| "status": "active" | |
| } | |
| } | |
| return jsonify({ | |
| "success": True, | |
| "system_info": system_info | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy thông tin hệ thống: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_system_alerts(): | |
| """Lấy cảnh báo hệ thống""" | |
| try: | |
| alerts = [] | |
| # Kiểm tra số lượng conversations | |
| db = get_db() | |
| total_conversations = db.conversations.count_documents({}) | |
| if total_conversations > 100: | |
| alerts.append({ | |
| "type": "info", | |
| "title": "Lượng dữ liệu cao", | |
| "message": f"Hệ thống có {total_conversations} cuộc hội thoại", | |
| "severity": "low" | |
| }) | |
| # Mặc định: hệ thống hoạt động bình thường | |
| if not alerts: | |
| alerts.append({ | |
| "type": "info", | |
| "title": "Hệ thống hoạt động bình thường", | |
| "message": "Tất cả dịch vụ đang chạy ổn định", | |
| "severity": "low" | |
| }) | |
| return jsonify({ | |
| "success": True, | |
| "alerts": alerts | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy cảnh báo hệ thống: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| # ===== USER MANAGEMENT ROUTES ===== | |
| def get_all_users(): | |
| """Lấy danh sách người dùng với thông tin thực tế""" | |
| try: | |
| db = get_db() | |
| users_collection = db.users | |
| page = int(request.args.get('page', 1)) | |
| per_page = int(request.args.get('per_page', 20)) | |
| search = request.args.get('search', '') | |
| gender_filter = request.args.get('gender', '') | |
| role_filter = request.args.get('role', '') | |
| sort_by = request.args.get('sort_by', 'created_at') | |
| sort_order = request.args.get('sort_order', 'desc') | |
| # Tạo query filter | |
| query_filter = {} | |
| if search: | |
| query_filter["$or"] = [ | |
| {"name": {"$regex": search, "$options": "i"}}, | |
| {"email": {"$regex": search, "$options": "i"}} | |
| ] | |
| if gender_filter: | |
| query_filter["gender"] = gender_filter | |
| if role_filter: | |
| query_filter["role"] = role_filter | |
| # Pagination | |
| skip = (page - 1) * per_page | |
| sort_direction = -1 if sort_order == 'desc' else 1 | |
| users_cursor = users_collection.find(query_filter).sort(sort_by, sort_direction).skip(skip).limit(per_page) | |
| total_users = users_collection.count_documents(query_filter) | |
| users_list = [] | |
| for user_data in users_cursor: | |
| # Đếm conversations thực tế | |
| conversation_count = db.conversations.count_documents({"user_id": user_data["_id"]}) | |
| # Lấy conversation mới nhất để biết last_activity | |
| latest_conversation = db.conversations.find_one( | |
| {"user_id": user_data["_id"]}, | |
| sort=[("updated_at", -1)] | |
| ) | |
| # Đếm tin nhắn | |
| user_conversations = list(db.conversations.find({"user_id": user_data["_id"]})) | |
| total_messages = sum(len(conv.get("messages", [])) for conv in user_conversations) | |
| users_list.append({ | |
| "id": str(user_data["_id"]), | |
| "name": user_data.get("name", ""), | |
| "email": user_data.get("email", ""), | |
| "gender": user_data.get("gender", ""), | |
| "role": user_data.get("role", "user"), | |
| "created_at": user_data.get("created_at").isoformat() if user_data.get("created_at") else None, | |
| "updated_at": user_data.get("updated_at").isoformat() if user_data.get("updated_at") else None, | |
| "last_login": user_data.get("last_login").isoformat() if user_data.get("last_login") else None, | |
| "conversation_count": conversation_count, | |
| "message_count": total_messages, | |
| "last_activity": latest_conversation.get("updated_at").isoformat() if latest_conversation and latest_conversation.get("updated_at") else None, | |
| "avg_messages_per_conversation": round(total_messages / conversation_count, 1) if conversation_count > 0 else 0 | |
| }) | |
| # Thống kê tổng hợp | |
| stats = { | |
| "total_users": total_users, | |
| "total_admins": users_collection.count_documents({"role": "admin"}), | |
| "total_regular_users": users_collection.count_documents({"role": "user"}), | |
| "active_users": users_collection.count_documents({"last_login": {"$exists": True}}), | |
| "gender_stats": { | |
| "male": users_collection.count_documents({"gender": "male"}), | |
| "female": users_collection.count_documents({"gender": "female"}), | |
| "other": users_collection.count_documents({"gender": "other"}), | |
| "unknown": users_collection.count_documents({"gender": {"$in": [None, ""]}}) | |
| } | |
| } | |
| return jsonify({ | |
| "success": True, | |
| "users": users_list, | |
| "stats": stats, | |
| "pagination": { | |
| "page": page, | |
| "per_page": per_page, | |
| "total": total_users, | |
| "pages": (total_users + per_page - 1) // per_page | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy danh sách users: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_user_detail(user_id): | |
| """Lấy chi tiết người dùng với thông tin đầy đủ""" | |
| try: | |
| user = User.find_by_id(user_id) | |
| if not user: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không tìm thấy người dùng" | |
| }), 404 | |
| db = get_db() | |
| # Lấy conversations của user | |
| user_conversations = list(db.conversations.find( | |
| {"user_id": ObjectId(user_id)}, | |
| {"title": 1, "created_at": 1, "updated_at": 1, "age_context": 1, "messages": 1} | |
| ).sort("updated_at", -1)) | |
| # Tính thống kê chi tiết | |
| total_messages = sum(len(conv.get("messages", [])) for conv in user_conversations) | |
| conversation_stats = { | |
| "total_conversations": len(user_conversations), | |
| "total_messages": total_messages, | |
| "avg_messages_per_conversation": round(total_messages / len(user_conversations), 1) if user_conversations else 0, | |
| "most_recent_conversation": user_conversations[0].get("updated_at").isoformat() if user_conversations and user_conversations[0].get("updated_at") else None, | |
| "oldest_conversation": user_conversations[-1].get("created_at").isoformat() if user_conversations and user_conversations[-1].get("created_at") else None | |
| } | |
| # Thống kê theo độ tuổi | |
| age_stats = {} | |
| for conv in user_conversations: | |
| age = conv.get("age_context") | |
| if age: | |
| age_stats[age] = age_stats.get(age, 0) + 1 | |
| user_detail = { | |
| "id": str(user.user_id), | |
| "name": user.name, | |
| "email": user.email, | |
| "gender": user.gender, | |
| "role": user.role, | |
| "created_at": user.created_at.isoformat() if user.created_at else None, | |
| "updated_at": user.updated_at.isoformat() if user.updated_at else None, | |
| "last_login": user.last_login.isoformat() if user.last_login else None, | |
| "stats": conversation_stats, | |
| "age_usage": age_stats, | |
| "recent_conversations": [ | |
| { | |
| "id": str(conv["_id"]), | |
| "title": conv.get("title", ""), | |
| "created_at": conv.get("created_at").isoformat() if conv.get("created_at") else None, | |
| "updated_at": conv.get("updated_at").isoformat() if conv.get("updated_at") else None, | |
| "message_count": len(conv.get("messages", [])), | |
| "age_context": conv.get("age_context") | |
| } | |
| for conv in user_conversations[:10] | |
| ] | |
| } | |
| return jsonify({ | |
| "success": True, | |
| "user": user_detail | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy chi tiết user: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def delete_user(user_id): | |
| """Xóa người dùng""" | |
| try: | |
| user = User.find_by_id(user_id) | |
| if not user: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không tìm thấy người dùng" | |
| }), 404 | |
| # Xóa tất cả conversations của user | |
| db = get_db() | |
| conversations_deleted = db.conversations.delete_many({"user_id": ObjectId(user_id)}) | |
| # Xóa user | |
| user_deleted = user.delete() | |
| if user_deleted: | |
| return jsonify({ | |
| "success": True, | |
| "message": f"Đã xóa người dùng và {conversations_deleted.deleted_count} cuộc hội thoại" | |
| }) | |
| else: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không thể xóa người dùng" | |
| }), 500 | |
| except Exception as e: | |
| logger.error(f"Lỗi xóa user: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def bulk_delete_users(): | |
| """Xóa nhiều người dùng""" | |
| try: | |
| data = request.json | |
| user_ids = data.get('user_ids', []) | |
| if not user_ids: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không có user nào được chọn" | |
| }), 400 | |
| deleted_count = 0 | |
| failed_ids = [] | |
| db = get_db() | |
| for user_id in user_ids: | |
| try: | |
| # Xóa conversations của user | |
| db.conversations.delete_many({"user_id": ObjectId(user_id)}) | |
| # Xóa user | |
| user = User.find_by_id(user_id) | |
| if user and user.delete(): | |
| deleted_count += 1 | |
| else: | |
| failed_ids.append(user_id) | |
| except Exception as e: | |
| logger.error(f"Lỗi xóa user {user_id}: {e}") | |
| failed_ids.append(user_id) | |
| return jsonify({ | |
| "success": True, | |
| "message": f"Đã xóa {deleted_count}/{len(user_ids)} người dùng", | |
| "deleted_count": deleted_count, | |
| "failed_ids": failed_ids | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi xóa bulk users: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| # ===== CONVERSATION MANAGEMENT ROUTES ===== | |
| def get_all_conversations(): | |
| """Lấy danh sách cuộc hội thoại - Fixed ObjectId serialization""" | |
| try: | |
| db = get_db() | |
| page = int(request.args.get('page', 1)) | |
| per_page = int(request.args.get('per_page', 20)) | |
| search = request.args.get('search', '') | |
| age_filter = request.args.get('age', '') | |
| archived_filter = request.args.get('archived', 'all') | |
| # Tạo query filter | |
| query_filter = {} | |
| if search: | |
| query_filter["title"] = {"$regex": search, "$options": "i"} | |
| if age_filter: | |
| query_filter["age_context"] = int(age_filter) | |
| if archived_filter != 'all': | |
| query_filter["is_archived"] = archived_filter == 'true' | |
| skip = (page - 1) * per_page | |
| conversations = list(db.conversations.find(query_filter).skip(skip).limit(per_page).sort("updated_at", -1)) | |
| total_conversations = db.conversations.count_documents(query_filter) | |
| conversations_list = [] | |
| for conv in conversations: | |
| try: | |
| # ✅ FIX: Safely convert ObjectId to string và handle user lookup | |
| user_id = conv.get("user_id") | |
| user_name = "Unknown" | |
| if user_id: | |
| try: | |
| # Ensure user_id is ObjectId | |
| if isinstance(user_id, str): | |
| user_id = ObjectId(user_id) | |
| user = User.find_by_id(str(user_id)) # Convert to string for the method | |
| if user: | |
| user_name = user.name | |
| except Exception as user_error: | |
| logger.warning(f"Could not fetch user {user_id}: {user_error}") | |
| user_name = "Unknown" | |
| # ✅ FIX: Safely handle datetime objects | |
| def safe_isoformat(dt): | |
| if dt is None: | |
| return None | |
| if hasattr(dt, 'isoformat'): | |
| return dt.isoformat() | |
| return str(dt) | |
| # ✅ FIX: Safely handle messages array | |
| messages = conv.get("messages", []) | |
| processed_messages = [] | |
| for msg in messages: | |
| if isinstance(msg, dict): | |
| processed_msg = { | |
| "role": msg.get("role", ""), | |
| "content": msg.get("content", ""), | |
| "timestamp": safe_isoformat(msg.get("timestamp")) | |
| } | |
| processed_messages.append(processed_msg) | |
| conversation_data = { | |
| "id": str(conv["_id"]), # ✅ Always convert ObjectId to string | |
| "title": conv.get("title", ""), | |
| "user_name": user_name, | |
| "age_context": conv.get("age_context"), | |
| "message_count": len(messages), | |
| "is_archived": conv.get("is_archived", False), | |
| "created_at": safe_isoformat(conv.get("created_at")), | |
| "updated_at": safe_isoformat(conv.get("updated_at")), | |
| "messages": processed_messages | |
| } | |
| conversations_list.append(conversation_data) | |
| except Exception as conv_error: | |
| logger.error(f"Error processing conversation {conv.get('_id')}: {conv_error}") | |
| # Skip this conversation but continue with others | |
| continue | |
| return jsonify({ | |
| "success": True, | |
| "conversations": conversations_list, | |
| "pagination": { | |
| "page": page, | |
| "per_page": per_page, | |
| "total": total_conversations, | |
| "pages": (total_conversations + per_page - 1) // per_page | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy danh sách conversations: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def delete_conversation(conversation_id): | |
| """Xóa cuộc hội thoại - Fixed ObjectId handling""" | |
| try: | |
| # ✅ FIX: Use Conversation model instead of direct DB access | |
| conversation = Conversation.find_by_id(conversation_id) | |
| if not conversation: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không tìm thấy cuộc hội thoại" | |
| }), 404 | |
| success = conversation.delete() | |
| if success: | |
| return jsonify({ | |
| "success": True, | |
| "message": "Đã xóa cuộc hội thoại" | |
| }) | |
| else: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không thể xóa cuộc hội thoại" | |
| }), 500 | |
| except Exception as e: | |
| logger.error(f"Lỗi xóa conversation: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def bulk_delete_conversations(): | |
| """Xóa nhiều cuộc hội thoại - Fixed ObjectId handling""" | |
| try: | |
| data = request.json | |
| conversation_ids = data.get('conversation_ids', []) | |
| if not conversation_ids: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không có conversation nào được chọn" | |
| }), 400 | |
| db = get_db() | |
| deleted_count = 0 | |
| failed_ids = [] | |
| for conv_id in conversation_ids: | |
| try: | |
| # ✅ FIX: Ensure proper ObjectId conversion | |
| if isinstance(conv_id, str): | |
| conv_id = ObjectId(conv_id) | |
| result = db.conversations.delete_one({"_id": conv_id}) | |
| if result.deleted_count > 0: | |
| deleted_count += 1 | |
| else: | |
| failed_ids.append(str(conv_id)) | |
| except Exception as e: | |
| logger.error(f"Error deleting conversation {conv_id}: {e}") | |
| failed_ids.append(str(conv_id)) | |
| continue | |
| return jsonify({ | |
| "success": True, | |
| "message": f"Đã xóa {deleted_count}/{len(conversation_ids)} cuộc hội thoại", | |
| "deleted_count": deleted_count, | |
| "failed_ids": failed_ids | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi xóa bulk conversations: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| # ===== DOCUMENT MANAGEMENT ROUTES ===== | |
| def get_all_documents(): | |
| """Lấy danh sách tài liệu từ ChromaDB theo đúng metadata""" | |
| try: | |
| embedding_model = get_embedding_model() | |
| # Lấy tất cả documents từ ChromaDB | |
| results = embedding_model.collection.get( | |
| include=['metadatas', 'documents'] | |
| ) | |
| if not results or not results['metadatas']: | |
| return jsonify({ | |
| "success": True, | |
| "documents": [], | |
| "stats": { | |
| "total": 0, | |
| "by_chapter": {}, | |
| "by_type": {} | |
| } | |
| }) | |
| # Phân tích metadata để nhóm theo chapter | |
| documents_by_chapter = {} | |
| stats = { | |
| "total": 0, | |
| "by_chapter": {}, | |
| "by_type": {} | |
| } | |
| for i, metadata in enumerate(results['metadatas']): | |
| # Lấy chapter từ metadata | |
| chapter = metadata.get('chapter', 'unknown') | |
| content_type = metadata.get('content_type', 'text') | |
| # Cập nhật stats | |
| stats["by_chapter"][chapter] = stats["by_chapter"].get(chapter, 0) + 1 | |
| stats["by_type"][content_type] = stats["by_type"].get(content_type, 0) + 1 | |
| # Nhóm theo chapter | |
| if chapter not in documents_by_chapter: | |
| chapter_title = get_chapter_title(chapter) | |
| chapter_type = get_chapter_type(chapter) | |
| documents_by_chapter[chapter] = { | |
| "id": chapter, | |
| "title": chapter_title, | |
| "description": f"Tài liệu {chapter_title.lower()}", | |
| "type": chapter_type, | |
| "status": "processed", | |
| "created_at": metadata.get('created_at', datetime.datetime.now().isoformat()), | |
| "content_stats": { | |
| "chunks": 0, | |
| "tables": 0, | |
| "figures": 0 | |
| } | |
| } | |
| # Cập nhật content stats | |
| if content_type == "table": | |
| documents_by_chapter[chapter]["content_stats"]["tables"] += 1 | |
| elif content_type == "figure": | |
| documents_by_chapter[chapter]["content_stats"]["figures"] += 1 | |
| else: | |
| documents_by_chapter[chapter]["content_stats"]["chunks"] += 1 | |
| documents_list = list(documents_by_chapter.values()) | |
| stats["total"] = len(documents_list) | |
| logger.info(f"Found chapters: {list(documents_by_chapter.keys())}") | |
| logger.info(f"Stats: {stats}") | |
| return jsonify({ | |
| "success": True, | |
| "documents": documents_list, | |
| "stats": stats | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy danh sách documents: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_chapter_title(chapter): | |
| try: | |
| # Nếu là document upload, lấy title từ metadata | |
| if chapter.startswith('bosung') or chapter.startswith('upload_'): | |
| embedding_model = get_embedding_model() | |
| results = embedding_model.collection.get( | |
| where={"chapter": chapter}, | |
| limit=1 | |
| ) | |
| if results and results.get('metadatas') and results['metadatas'][0]: | |
| metadata = results['metadatas'][0] | |
| document_title = metadata.get('document_title') or metadata.get('document_source') | |
| if document_title and document_title != 'Tài liệu upload': | |
| return document_title | |
| # Fallback cho các chapter chuẩn | |
| chapter_titles = { | |
| 'bai1': 'Bài 1: Dinh dưỡng theo lứa tuổi học sinh', | |
| 'bai2': 'Bài 2: An toàn thực phẩm', | |
| 'bai3': 'Bài 3: Vệ sinh dinh dưỡng', | |
| 'bai4': 'Bài 4: Giáo dục dinh dưỡng', | |
| 'phuluc': 'Phụ lục' | |
| } | |
| # Kiểm tra pattern bosung | |
| if chapter.startswith('bosung'): | |
| return f'Tài liệu bổ sung {chapter.replace("bosung", "")}' | |
| return chapter_titles.get(chapter, f'Tài liệu {chapter}') | |
| except Exception as e: | |
| logger.error(f"Error getting chapter title: {e}") | |
| return f'Tài liệu {chapter}' | |
| def get_chapter_type(chapter): | |
| """Lấy loại chapter""" | |
| if chapter.startswith('bai'): | |
| return 'lesson' | |
| elif chapter == 'phuluc': | |
| return 'appendix' | |
| else: | |
| return 'uploaded' | |
| def get_document_detail(doc_id): | |
| """Lấy chi tiết document theo chapter""" | |
| try: | |
| logger.info(f"Getting document detail for: {doc_id}") | |
| # Sử dụng try-catch để tránh lỗi tensor | |
| try: | |
| embedding_model = get_embedding_model() | |
| except Exception as model_error: | |
| logger.warning(f"Cannot load embedding model: {model_error}") | |
| # Fallback: truy cập trực tiếp ChromaDB | |
| import chromadb | |
| from config import CHROMA_PERSIST_DIRECTORY, COLLECTION_NAME | |
| chroma_client = chromadb.PersistentClient(path=CHROMA_PERSIST_DIRECTORY) | |
| collection = chroma_client.get_collection(name=COLLECTION_NAME) | |
| else: | |
| collection = embedding_model.collection | |
| # Lấy tất cả documents và filter manually để tránh lỗi query | |
| try: | |
| all_results = collection.get( | |
| include=['metadatas', 'documents'] # Bỏ 'ids' để tránh lỗi | |
| ) | |
| if not all_results or not all_results.get('metadatas'): | |
| logger.warning("No documents found in collection") | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không có dữ liệu trong collection" | |
| }), 404 | |
| # Filter manually | |
| filtered_documents = [] | |
| filtered_metadatas = [] | |
| for i, metadata in enumerate(all_results['metadatas']): | |
| if not metadata: | |
| continue | |
| chunk_id = metadata.get('chunk_id', '') | |
| chapter = metadata.get('chapter', '') | |
| # Kiểm tra match với doc_id | |
| if (chunk_id.startswith(f"{doc_id}_") or | |
| chapter == doc_id or | |
| (doc_id in ['bai1', 'bai2', 'bai3', 'bai4'] and chunk_id.startswith(f"{doc_id}_")) or | |
| (doc_id == 'phuluc' and 'phuluc' in chunk_id.lower())): | |
| filtered_documents.append(all_results['documents'][i]) | |
| filtered_metadatas.append(metadata) | |
| logger.info(f"Found {len(filtered_documents)} matching documents for {doc_id}") | |
| if not filtered_documents: | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Không tìm thấy tài liệu cho {doc_id}" | |
| }), 404 | |
| except Exception as query_error: | |
| logger.error(f"ChromaDB query error: {query_error}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Lỗi truy vấn cơ sở dữ liệu: {str(query_error)}" | |
| }), 500 | |
| # Xử lý kết quả và nhóm theo content_type | |
| chunks_by_type = { | |
| 'text': [], | |
| 'table': [], | |
| 'figure': [] | |
| } | |
| for i, metadata in enumerate(filtered_metadatas): | |
| content_type = metadata.get('content_type', 'text') | |
| # Parse age_range | |
| age_range_str = metadata.get('age_range', '1-19') | |
| try: | |
| if '-' in age_range_str: | |
| age_min, age_max = map(int, age_range_str.split('-')) | |
| else: | |
| age_min = age_max = int(age_range_str) | |
| except: | |
| age_min, age_max = 1, 19 | |
| chunk = { | |
| "id": metadata.get('chunk_id', f'chunk_{i}'), | |
| "title": metadata.get('title', 'Không có tiêu đề'), | |
| "content": filtered_documents[i], | |
| "content_type": content_type, | |
| "age_range": age_range_str, | |
| "age_min": age_min, | |
| "age_max": age_max, | |
| "summary": metadata.get('summary', 'Không có tóm tắt'), | |
| "pages": metadata.get('pages', ''), | |
| "word_count": metadata.get('word_count', 0), | |
| "token_count": metadata.get('token_count', 0), | |
| "related_chunks": metadata.get('related_chunks', '').split(',') if metadata.get('related_chunks') else [], | |
| "created_at": metadata.get('created_at', ''), | |
| "document_source": metadata.get('document_source', ''), | |
| # Metadata đặc biệt | |
| "contains_table": metadata.get('contains_table', False), | |
| "contains_figure": metadata.get('contains_figure', False), | |
| "table_columns": metadata.get('table_columns', '').split(',') if metadata.get('table_columns') else [] | |
| } | |
| # Phân loại vào đúng nhóm | |
| if content_type in chunks_by_type: | |
| chunks_by_type[content_type].append(chunk) | |
| else: | |
| chunks_by_type['text'].append(chunk) | |
| # Tính thống kê | |
| total_chunks = sum(len(chunks) for chunks in chunks_by_type.values()) | |
| logger.info(f"Successfully processed {total_chunks} chunks for {doc_id}") | |
| return jsonify({ | |
| "success": True, | |
| "document": { | |
| "id": doc_id, | |
| "chunks": chunks_by_type, | |
| "stats": { | |
| "total_chunks": total_chunks, | |
| "text_chunks": len(chunks_by_type['text']), | |
| "table_chunks": len(chunks_by_type['table']), | |
| "figure_chunks": len(chunks_by_type['figure']) | |
| } | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Error getting document detail: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Lỗi máy chủ: {str(e)}" | |
| }), 500 | |
| def debug_metadata(): | |
| """Debug metadata trong ChromaDB""" | |
| try: | |
| # Sử dụng try-catch để tránh lỗi tensor | |
| try: | |
| embedding_model = get_embedding_model() | |
| total_docs = embedding_model.count() | |
| except Exception as model_error: | |
| logger.warning(f"Không thể load embedding model: {model_error}") | |
| # Fallback: truy cập trực tiếp ChromaDB | |
| import chromadb | |
| from config import CHROMA_PERSIST_DIRECTORY, COLLECTION_NAME | |
| chroma_client = chromadb.PersistentClient(path=CHROMA_PERSIST_DIRECTORY) | |
| collection = chroma_client.get_collection(name=COLLECTION_NAME) | |
| total_docs = collection.count() | |
| # Lấy sample metadata | |
| results = collection.get( | |
| limit=10, | |
| include=['metadatas'] | |
| ) | |
| else: | |
| # Lấy 10 documents đầu tiên để debug | |
| results = embedding_model.collection.get( | |
| limit=10, | |
| include=['metadatas'] | |
| ) | |
| debug_info = { | |
| "total_documents": total_docs, | |
| "sample_metadata": results['metadatas'][:5] if results and results.get('metadatas') else [], | |
| "all_metadata_keys": [] | |
| } | |
| if results and results.get('metadatas'): | |
| # Lấy tất cả keys từ metadata | |
| all_keys = set() | |
| for metadata in results['metadatas']: | |
| if metadata: # Kiểm tra metadata không None | |
| all_keys.update(metadata.keys()) | |
| debug_info["all_metadata_keys"] = list(all_keys) | |
| return jsonify({ | |
| "success": True, | |
| "debug_info": debug_info | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi debug metadata: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def upload_document(): | |
| """Upload PDF document""" | |
| try: | |
| if 'file' not in request.files: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không có file được upload" | |
| }), 400 | |
| file = request.files['file'] | |
| if file.filename == '': | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không có file được chọn" | |
| }), 400 | |
| if not file.filename.lower().endswith('.pdf'): | |
| return jsonify({ | |
| "success": False, | |
| "error": "Chỉ chấp nhận file PDF" | |
| }), 400 | |
| # Tạo thư mục temp nếu chưa có | |
| temp_dir = '/tmp' | |
| if not os.path.exists(temp_dir): | |
| os.makedirs(temp_dir) | |
| # Lưu file tạm thời | |
| filename = secure_filename(file.filename) | |
| temp_path = os.path.join(temp_dir, f"{int(time.time())}_{filename}") | |
| file.save(temp_path) | |
| # Metadata từ form | |
| title = request.form.get('title', filename.replace('.pdf', '')) | |
| description = request.form.get('description', '') | |
| author = request.form.get('author', '') | |
| # Tạo document_id duy nhất dựa trên số lượng tài liệu bổ sung hiện có | |
| document_id = generate_unique_document_id(title) | |
| logger.info(f"Uploaded file: {filename} -> {temp_path}") | |
| return jsonify({ | |
| "success": True, | |
| "message": "File đã được upload thành công", | |
| "document_id": document_id, | |
| "filename": filename, | |
| "temp_path": temp_path, | |
| "metadata": { | |
| "title": title, | |
| "description": description, | |
| "author": author | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi upload document: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Lỗi upload: {str(e)}" | |
| }), 500 | |
| def generate_unique_document_id(title): | |
| """Tạo document ID duy nhất dựa trên title và số thứ tự""" | |
| try: | |
| embedding_model = get_embedding_model() | |
| # Đếm số tài liệu bổ sung hiện có | |
| results = embedding_model.collection.get( | |
| where={"chapter": {"$like": "bosung%"}} | |
| ) | |
| if results and results.get('metadatas'): | |
| # Lấy tất cả chapter IDs bắt đầu bằng "bosung" | |
| existing_chapters = set() | |
| for metadata in results['metadatas']: | |
| if metadata and metadata.get('chapter'): | |
| chapter = metadata['chapter'] | |
| if chapter.startswith('bosung'): | |
| existing_chapters.add(chapter) | |
| # Tìm số thứ tự cao nhất | |
| max_num = 0 | |
| for chapter in existing_chapters: | |
| try: | |
| num = int(chapter.replace('bosung', '')) | |
| max_num = max(max_num, num) | |
| except: | |
| continue | |
| next_num = max_num + 1 | |
| else: | |
| next_num = 1 | |
| # Tạo ID mới | |
| document_id = f"bosung{next_num}" | |
| logger.info(f"Generated unique document ID: {document_id}") | |
| return document_id | |
| except Exception as e: | |
| logger.error(f"Error generating document ID: {e}") | |
| # Fallback: sử dụng timestamp | |
| return f"bosung_{int(time.time())}" | |
| def process_document(doc_id): | |
| """Process document với Gemini - THÊM DEBUG LOGGING""" | |
| try: | |
| data = request.json | |
| temp_path = data.get('temp_path') | |
| if not temp_path or not os.path.exists(temp_path): | |
| return jsonify({ | |
| "success": False, | |
| "error": "File không tồn tại" | |
| }), 400 | |
| logger.info(f"Processing document {doc_id} from {temp_path}") | |
| # Kiểm tra và import PyPDF2 | |
| try: | |
| import PyPDF2 | |
| except ImportError: | |
| logger.error("PyPDF2 không được cài đặt") | |
| return jsonify({ | |
| "success": False, | |
| "error": "Thiếu thư viện PyPDF2. Vui lòng cài đặt: pip install PyPDF2" | |
| }), 500 | |
| # Đọc PDF content | |
| try: | |
| with open(temp_path, 'rb') as file: | |
| pdf_reader = PyPDF2.PdfReader(file) | |
| pdf_text = "" | |
| for page_num in range(len(pdf_reader.pages)): | |
| page = pdf_reader.pages[page_num] | |
| page_text = page.extract_text() | |
| if page_text: | |
| pdf_text += page_text + "\n" | |
| logger.info(f"Extracted {len(pdf_text)} characters from PDF") | |
| # DEBUG: Log phần đầu của PDF text | |
| logger.info(f"PDF text preview (first 500 chars): {pdf_text[:500]}...") | |
| except Exception as pdf_error: | |
| logger.error(f"Lỗi đọc PDF: {pdf_error}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Không thể đọc file PDF: {str(pdf_error)}" | |
| }), 400 | |
| if not pdf_text.strip(): | |
| return jsonify({ | |
| "success": False, | |
| "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)" | |
| }), 400 | |
| # Tạo prompt cho Gemini | |
| try: | |
| prompt = create_document_processing_prompt(pdf_text) | |
| logger.info("Created prompt for Gemini") | |
| # DEBUG: Log độ dài prompt | |
| logger.info(f"Prompt length: {len(prompt)} characters") | |
| except Exception as prompt_error: | |
| logger.error(f"Lỗi tạo prompt: {prompt_error}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Lỗi tạo prompt: {str(prompt_error)}" | |
| }), 500 | |
| # Gọi Gemini API | |
| try: | |
| model = genai.GenerativeModel('gemini-2.5-flash') | |
| logger.info("Calling Gemini API...") | |
| response = model.generate_content( | |
| prompt, | |
| generation_config=genai.types.GenerationConfig( | |
| temperature=0.1, | |
| max_output_tokens=100000 # Sử dụng giá trị bạn đã tăng | |
| ) | |
| ) | |
| if not response or not response.text: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Gemini không trả về response" | |
| }), 500 | |
| result_text = response.text.strip() | |
| logger.info(f"Got response from Gemini: {len(result_text)} characters") | |
| # DEBUG: Log phần đầu của response | |
| logger.info(f"Gemini response preview (first 1000 chars): {result_text[:1000]}...") | |
| except Exception as gemini_error: | |
| logger.error(f"Lỗi gọi Gemini API: {gemini_error}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Lỗi gọi AI API: {str(gemini_error)}" | |
| }), 500 | |
| # Parse JSON response | |
| try: | |
| # Làm sạch JSON response | |
| original_result_text = result_text # Lưu bản gốc để log | |
| if result_text.startswith('```json'): | |
| result_text = result_text.replace('```json', '').replace('```', '').strip() | |
| elif result_text.startswith('```'): | |
| result_text = result_text[3:].rstrip('```').strip() | |
| logger.info(f"Cleaned response length: {len(result_text)} characters") | |
| processed_data = json.loads(result_text) | |
| logger.info("Successfully parsed JSON response") | |
| # DEBUG: Log sample chunks để kiểm tra content vs summary | |
| chunks = processed_data.get('chunks', []) | |
| logger.info(f"Found {len(chunks)} chunks in response") | |
| if chunks: | |
| for i, chunk in enumerate(chunks[:2]): # Log 2 chunks đầu | |
| chunk_id = chunk.get('id', f'chunk_{i}') | |
| summary_len = len(chunk.get('summary', '')) | |
| content_len = len(chunk.get('content', '')) | |
| logger.info(f"Chunk {chunk_id}: summary_len={summary_len}, content_len={content_len}") | |
| # Log content preview | |
| content_preview = chunk.get('content', '')[:300] | |
| summary_preview = chunk.get('summary', '')[:300] | |
| logger.info(f"Chunk {chunk_id} content preview: {content_preview}...") | |
| logger.info(f"Chunk {chunk_id} summary preview: {summary_preview}...") | |
| # Lưu debug log vào file | |
| debug_log_path = save_gemini_response_to_file(original_result_text, processed_data, doc_id) | |
| logger.info(f"Debug log saved to: {debug_log_path}") | |
| except json.JSONDecodeError as json_error: | |
| logger.error(f"JSON decode error: {json_error}") | |
| logger.error(f"Raw response: {result_text}") | |
| # Lưu response lỗi vào file debug | |
| try: | |
| log_dir = setup_debug_logging() | |
| timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") | |
| error_log_path = os.path.join(log_dir, f"gemini_error_{doc_id}_{timestamp}.log") | |
| with open(error_log_path, 'w', encoding='utf-8') as f: | |
| f.write("GEMINI JSON PARSE ERROR\n") | |
| f.write("=" * 40 + "\n") | |
| f.write(f"Error: {json_error}\n\n") | |
| f.write("Raw Response:\n") | |
| f.write(result_text) | |
| logger.info(f"Error log saved to: {error_log_path}") | |
| except: | |
| pass | |
| return jsonify({ | |
| "success": False, | |
| "error": f"AI trả về format không hợp lệ: {str(json_error)}" | |
| }), 500 | |
| # Validate format | |
| try: | |
| if not validate_processed_format(processed_data): | |
| return jsonify({ | |
| "success": False, | |
| "error": "AI trả về format không đúng cấu trúc yêu cầu" | |
| }), 500 | |
| except Exception as validate_error: | |
| logger.error(f"Validation error: {validate_error}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Lỗi validate format: {str(validate_error)}" | |
| }), 500 | |
| # Lưu vào ChromaDB | |
| try: | |
| save_processed_document(doc_id, processed_data) | |
| logger.info(f"Successfully saved document {doc_id} to ChromaDB") | |
| except Exception as save_error: | |
| logger.error(f"Lỗi lưu document: {save_error}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Lỗi lưu document: {str(save_error)}" | |
| }), 500 | |
| # Xóa file tạm | |
| try: | |
| if os.path.exists(temp_path): | |
| os.remove(temp_path) | |
| logger.info(f"Deleted temp file: {temp_path}") | |
| except Exception as delete_error: | |
| logger.warning(f"Cannot delete temp file: {delete_error}") | |
| return jsonify({ | |
| "success": True, | |
| "message": "Xử lý tài liệu thành công", | |
| "processed_chunks": len(processed_data.get('chunks', [])), | |
| "processed_tables": len(processed_data.get('tables', [])), | |
| "processed_figures": len(processed_data.get('figures', [])), | |
| "document_id": doc_id, | |
| "debug_log": debug_log_path # Trả về đường dẫn log file | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi xử lý document: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": f"Lỗi xử lý: {str(e)}" | |
| }), 500 | |
| def create_document_processing_prompt(pdf_text): | |
| """Tạo prompt chi tiết cho Gemini - CẢI THIỆN THÊM""" | |
| # Cắt ngắn text để tránh vượt quá limit | |
| max_text_length = 80000 # Tăng lên do bạn đã tăng token limit | |
| if len(pdf_text) > max_text_length: | |
| pdf_text = pdf_text[:max_text_length] + "..." | |
| prompt = f""" | |
| 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. | |
| TÀI LIỆU CẦN XỬ LÝ: | |
| {pdf_text} | |
| YÊU CẦU PHÂN TÍCH: | |
| 1. PHÂN CHIA NỘI DUNG: | |
| - Chia tài liệu thành các chunk có nghĩa (mỗi chunk 200-1000 từ) | |
| - 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 | |
| - Field "summary" chỉ là tóm tắt ngắn gọn (1-2 câu) | |
| - Field "content" phải chứa toàn bộ văn bản gốc của phần đó | |
| - KHÔNG viết lại, KHÔNG diễn giải, KHÔNG tóm tắt trong field "content" | |
| 2. CẤU TRÚC JSON: | |
| Mỗi chunk PHẢI có đủ các field sau: | |
| - "id": ID duy nhất | |
| - "title": Tiêu đề của chunk | |
| - "content": NỘI DUNG NGUYÊN VĂN từ PDF (bắt buộc phải có) | |
| - "summary": Tóm tắt ngắn gọn (khác với content) | |
| - "content_type": "text", "table", hoặc "figure" | |
| - "age_range": [min_age, max_age] từ 1-19 | |
| - "pages": trang nếu biết | |
| - "related_chunks": [] | |
| - "word_count": số từ trong content | |
| - "token_count": ước tính token | |
| 3. VÍ DỤ ĐÚNG: | |
| {{ | |
| "id": "bosung1_muc1_1", | |
| "title": "Giới thiệu về dinh dưỡng", | |
| "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...", | |
| "summary": "Giới thiệu khái niệm cơ bản về dinh dưỡng", | |
| "content_type": "text", | |
| "age_range": [1, 19] | |
| }} | |
| 4. ĐỊNH DẠNG OUTPUT JSON: | |
| {{ | |
| "bai_info": {{ | |
| "id": "bosung1", | |
| "title": "Tiêu đề tài liệu", | |
| "overview": "Tổng quan" | |
| }}, | |
| "chunks": [ | |
| {{ | |
| "id": "bosung1_muc1_1", | |
| "title": "Tiêu đề chunk", | |
| "content": "NỘI DUNG NGUYÊN VĂN TỪ PDF - BẮTED BUỘC PHẢI CÓ", | |
| "summary": "Tóm tắt ngắn gọn", | |
| "content_type": "text", | |
| "age_range": [1, 19], | |
| "pages": "", | |
| "related_chunks": [], | |
| "word_count": 100, | |
| "token_count": 150, | |
| "contains_table": false, | |
| "contains_figure": false | |
| }} | |
| ], | |
| "tables": [], | |
| "figures": [], | |
| "total_items": {{"chunks": 1, "tables": 0, "figures": 0}} | |
| }} | |
| LƯU Ý QUAN TRỌNG: | |
| - Field "content" PHẢI chứa văn bản nguyên gốc từ PDF | |
| - Field "summary" mới là phần tóm tắt | |
| - KHÔNG được để field "content" trống hoặc giống "summary" | |
| - Trả về JSON hợp lệ, không có text thừa | |
| Hãy phân tích và trả về JSON, đảm bảo field "content" chứa nội dung đầy đủ từ PDF. | |
| """ | |
| return prompt | |
| def validate_processed_format(data): | |
| """Validate format của processed data""" | |
| try: | |
| # Kiểm tra required fields | |
| required_fields = ['bai_info', 'chunks', 'total_items'] | |
| for field in required_fields: | |
| if field not in data: | |
| logger.error(f"Missing required field: {field}") | |
| return False | |
| # Validate bai_info | |
| bai_info = data['bai_info'] | |
| bai_info_fields = ['id', 'title', 'overview'] | |
| for field in bai_info_fields: | |
| if field not in bai_info: | |
| logger.error(f"Missing bai_info field: {field}") | |
| return False | |
| # Validate chunks | |
| if not isinstance(data['chunks'], list): | |
| logger.error("chunks must be a list") | |
| return False | |
| for i, chunk in enumerate(data['chunks']): | |
| chunk_fields = ['id', 'title', 'content_type', 'age_range', 'summary'] | |
| for field in chunk_fields: | |
| if field not in chunk: | |
| logger.error(f"Missing chunk field '{field}' in chunk {i}") | |
| return False | |
| # Validate age_range | |
| age_range = chunk['age_range'] | |
| if not isinstance(age_range, list) or len(age_range) != 2: | |
| logger.error(f"Invalid age_range in chunk {i}: must be list of 2 integers") | |
| return False | |
| if not all(isinstance(x, int) and 1 <= x <= 19 for x in age_range): | |
| logger.error(f"Invalid age_range values in chunk {i}: must be integers 1-19") | |
| return False | |
| # Validate optional fields | |
| for table in data.get('tables', []): | |
| if 'age_range' in table: | |
| age_range = table['age_range'] | |
| if not isinstance(age_range, list) or len(age_range) != 2: | |
| logger.error("Invalid age_range in table") | |
| return False | |
| for figure in data.get('figures', []): | |
| if 'age_range' in figure: | |
| age_range = figure['age_range'] | |
| if not isinstance(age_range, list) or len(age_range) != 2: | |
| logger.error("Invalid age_range in figure") | |
| return False | |
| logger.info("Document format validation passed") | |
| return True | |
| except Exception as e: | |
| logger.error(f"Validation error: {e}") | |
| return False | |
| def save_processed_document(doc_id, processed_data): | |
| try: | |
| embedding_model = get_embedding_model() | |
| document_title = processed_data.get('bai_info', {}).get('title', f'Tài liệu {doc_id}') | |
| # Chuẩn bị tất cả items để index | |
| all_items = [] | |
| # Xử lý chunks | |
| for chunk in processed_data.get('chunks', []): | |
| chunk_content = chunk.get('content', chunk.get('summary', '')) | |
| if not chunk_content: | |
| # Fallback nếu không có content | |
| chunk_content = f"Tiêu đề: {chunk['title']}\nNội dung: {chunk.get('summary', '')}" | |
| # Chuẩn bị metadata theo format cần thiết | |
| metadata = { | |
| "chunk_id": chunk['id'], | |
| "chapter": doc_id, | |
| "title": chunk['title'], | |
| "content_type": chunk['content_type'], | |
| "age_range": f"{chunk['age_range'][0]}-{chunk['age_range'][1]}", | |
| "age_min": chunk['age_range'][0], | |
| "age_max": chunk['age_range'][1], | |
| "summary": chunk['summary'], | |
| "pages": chunk.get('pages', ''), | |
| "related_chunks": ','.join(chunk.get('related_chunks', [])), | |
| "word_count": chunk.get('word_count', 0), | |
| "token_count": chunk.get('token_count', 0), | |
| "contains_table": chunk.get('contains_table', False), | |
| "contains_figure": chunk.get('contains_figure', False), | |
| "created_at": datetime.datetime.now().isoformat(), | |
| "document_source": document_title, | |
| "document_title": document_title | |
| } | |
| # Thêm vào danh sách | |
| all_items.append({ | |
| "content": chunk_content, | |
| "metadata": metadata, | |
| "id": chunk['id'] | |
| }) | |
| # Xử lý tables | |
| for table in processed_data.get('tables', []): | |
| table_content = table.get('content', f"Bảng: {table['title']}\nMô tả: {table.get('summary', '')}") | |
| metadata = { | |
| "chunk_id": table['id'], | |
| "chapter": doc_id, | |
| "title": table['title'], | |
| "content_type": "table", | |
| "age_range": f"{table['age_range'][0]}-{table['age_range'][1]}", | |
| "age_min": table['age_range'][0], | |
| "age_max": table['age_range'][1], | |
| "summary": table['summary'], | |
| "pages": table.get('pages', ''), | |
| "related_chunks": ','.join(table.get('related_chunks', [])), | |
| "table_columns": ','.join(table.get('table_columns', [])), | |
| "word_count": table.get('word_count', 0), | |
| "token_count": table.get('token_count', 0), | |
| "created_at": datetime.datetime.now().isoformat(), | |
| "document_source": document_title, | |
| "document_title": document_title | |
| } | |
| all_items.append({ | |
| "content": table_content, | |
| "metadata": metadata, | |
| "id": table['id'] | |
| }) | |
| # Xử lý figures | |
| for figure in processed_data.get('figures', []): | |
| figure_content = figure.get('content', f"Hình: {figure['title']}\nMô tả: {figure.get('summary', '')}") | |
| metadata = { | |
| "chunk_id": figure['id'], | |
| "chapter": doc_id, | |
| "title": figure['title'], | |
| "content_type": "figure", | |
| "age_range": f"{figure['age_range'][0]}-{figure['age_range'][1]}", | |
| "age_min": figure['age_range'][0], | |
| "age_max": figure['age_range'][1], | |
| "summary": figure['summary'], | |
| "pages": figure.get('pages', ''), | |
| "related_chunks": ','.join(figure.get('related_chunks', [])), | |
| "created_at": datetime.datetime.now().isoformat(), | |
| "document_source": document_title, | |
| "document_title": document_title | |
| } | |
| all_items.append({ | |
| "content": figure_content, | |
| "metadata": metadata, | |
| "id": figure['id'] | |
| }) | |
| # Sử dụng index_chunks để lưu tất cả items | |
| if all_items: | |
| success = embedding_model.index_chunks(all_items) | |
| if success: | |
| logger.info(f"Successfully indexed {len(all_items)} items for document {doc_id} (title: {document_title})") | |
| else: | |
| raise Exception("Failed to index chunks") | |
| else: | |
| logger.warning(f"No items to index for document {doc_id}") | |
| except Exception as e: | |
| logger.error(f"Error in save_processed_document: {str(e)}") | |
| raise | |
| def delete_document(doc_id): | |
| """Xóa document theo chapter""" | |
| try: | |
| embedding_model = get_embedding_model() | |
| # Lấy tất cả chunks của document - BỎ include=['ids'] | |
| results = embedding_model.collection.get( | |
| where={"chapter": doc_id} | |
| # Không cần include=['ids'] vì mặc định đã trả về ids | |
| ) | |
| if results and results.get('ids'): | |
| # Xóa từng chunk | |
| for chunk_id in results['ids']: | |
| embedding_model.collection.delete(ids=[chunk_id]) | |
| return jsonify({ | |
| "success": True, | |
| "message": f"Đã xóa {len(results['ids'])} chunks của document {doc_id}" | |
| }) | |
| else: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không tìm thấy document" | |
| }), 404 | |
| except Exception as e: | |
| logger.error(f"Lỗi xóa document: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def bulk_delete_documents(): | |
| """Xóa nhiều documents""" | |
| try: | |
| data = request.json | |
| doc_ids = data.get('doc_ids', []) | |
| if not doc_ids: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không có document nào được chọn" | |
| }), 400 | |
| embedding_model = get_embedding_model() | |
| deleted_count = 0 | |
| for doc_id in doc_ids: | |
| try: | |
| # SỬA: Bỏ include=['ids'] | |
| results = embedding_model.collection.get( | |
| where={"chapter": doc_id} | |
| ) | |
| if results and results.get('ids'): | |
| for chunk_id in results['ids']: | |
| embedding_model.collection.delete(ids=[chunk_id]) | |
| deleted_count += 1 | |
| except Exception as e: | |
| logger.error(f"Lỗi xóa document {doc_id}: {e}") | |
| continue | |
| return jsonify({ | |
| "success": True, | |
| "message": f"Đã xóa {deleted_count}/{len(doc_ids)} documents", | |
| "deleted_count": deleted_count | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi xóa bulk documents: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| # ===== ANALYTICS ROUTES ===== | |
| def get_analytics_overview(): | |
| """Lấy thống kê phân tích""" | |
| try: | |
| db = get_db() | |
| # Thống kê cơ bản | |
| total_conversations = db.conversations.count_documents({}) | |
| total_users = db.users.count_documents({}) if hasattr(db, 'users') else 1 | |
| # Thống kê theo tuần (7 ngày gần nhất) | |
| daily_stats = [] | |
| for i in range(7): | |
| day_start = datetime.datetime.now() - datetime.timedelta(days=6-i) | |
| day_end = day_start + datetime.timedelta(days=1) | |
| daily_conversations = db.conversations.count_documents({ | |
| "created_at": {"$gte": day_start, "$lt": day_end} | |
| }) | |
| daily_stats.append({ | |
| "date": day_start.strftime("%Y-%m-%d"), | |
| "label": day_start.strftime("%d/%m"), | |
| "conversations": daily_conversations, | |
| "users": 0 # Mock data | |
| }) | |
| # Thống kê theo độ tuổi | |
| age_stats = list(db.conversations.aggregate([ | |
| {"$group": {"_id": "$age_context", "count": {"$sum": 1}}}, | |
| {"$sort": {"_id": 1}} | |
| ])) | |
| return jsonify({ | |
| "success": True, | |
| "overview": { | |
| "totalUsers": total_users, | |
| "activeUsers": total_users, | |
| "totalConversations": total_conversations, | |
| "avgMessagesPerConversation": 6.8, | |
| "userGrowth": "+8.5%", | |
| "conversationGrowth": "+12.3%" | |
| }, | |
| "dailyStats": daily_stats, | |
| "ageDistribution": [ | |
| { | |
| "age_group": f"{stat['_id']} tuổi" if stat['_id'] else "Không rõ", | |
| "count": stat["count"] | |
| } | |
| for stat in age_stats | |
| ] | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy analytics: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def update_user(user_id): | |
| """Cập nhật thông tin người dùng""" | |
| try: | |
| data = request.json | |
| user = User.find_by_id(user_id) | |
| if not user: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không tìm thấy người dùng" | |
| }), 404 | |
| # Cập nhật thông tin | |
| if 'name' in data: | |
| user.name = data['name'] | |
| if 'gender' in data: | |
| user.gender = data['gender'] | |
| if 'role' in data: | |
| user.role = data['role'] | |
| # Lưu thay đổi | |
| user.save() | |
| return jsonify({ | |
| "success": True, | |
| "message": "Cập nhật người dùng thành công", | |
| "user": { | |
| "id": str(user.user_id), | |
| "name": user.name, | |
| "email": user.email, | |
| "gender": user.gender, | |
| "role": user.role | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi cập nhật user: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_all_feedback(): | |
| """API endpoint để admin xem tất cả feedback""" | |
| try: | |
| page = int(request.args.get('page', 1)) | |
| per_page = int(request.args.get('per_page', 20)) | |
| status_filter = request.args.get('status') | |
| skip = (page - 1) * per_page | |
| feedbacks = Feedback.get_all_for_admin(limit=per_page, skip=skip, status_filter=status_filter) | |
| result = [] | |
| for feedback in feedbacks: | |
| result.append({ | |
| "id": str(feedback.feedback_id), | |
| "user_name": getattr(feedback, 'user_name', 'Ẩn danh'), | |
| "user_email": getattr(feedback, 'user_email', ''), | |
| "rating": feedback.rating, | |
| "category": feedback.category, | |
| "title": feedback.title, | |
| "content": feedback.content, | |
| "status": feedback.status, | |
| "admin_response": feedback.admin_response, | |
| "created_at": feedback.created_at.isoformat(), | |
| "updated_at": feedback.updated_at.isoformat() | |
| }) | |
| return jsonify({ | |
| "success": True, | |
| "feedbacks": result | |
| }) | |
| except Exception as e: | |
| logger.error(f"Error getting feedback for admin: {e}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_feedback_stats(): | |
| """API endpoint để lấy thống kê feedback""" | |
| try: | |
| stats = Feedback.get_stats() | |
| return jsonify({ | |
| "success": True, | |
| "stats": stats | |
| }) | |
| except Exception as e: | |
| logger.error(f"Error getting feedback stats: {e}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def respond_to_feedback(feedback_id): | |
| """API endpoint để admin phản hồi feedback""" | |
| try: | |
| data = request.json | |
| response_text = data.get('response', '') | |
| new_status = data.get('status', 'reviewed') | |
| if not response_text.strip(): | |
| return jsonify({ | |
| "success": False, | |
| "error": "Vui lòng nhập phản hồi" | |
| }), 400 | |
| feedback = Feedback.find_by_id(feedback_id) | |
| if not feedback: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không tìm thấy feedback" | |
| }), 404 | |
| success = feedback.update_admin_response(response_text.strip(), new_status) | |
| if success: | |
| return jsonify({ | |
| "success": True, | |
| "message": "Đã phản hồi feedback thành công" | |
| }) | |
| else: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Không thể cập nhật phản hồi" | |
| }), 500 | |
| except Exception as e: | |
| logger.error(f"Error responding to feedback: {e}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| # ===== SYSTEM SETTINGS ROUTES ===== | |
| def get_system_config(): | |
| """Lấy cấu hình hệ thống thật""" | |
| try: | |
| import os | |
| from config import EMBEDDING_MODEL, GEMINI_API_KEY, CHROMA_PERSIST_DIRECTORY, COLLECTION_NAME | |
| from core.embedding_model import get_embedding_model | |
| # Thông tin database | |
| db = get_db() | |
| # Thông tin Collections trong MongoDB | |
| try: | |
| mongo_collections = db.list_collection_names() | |
| mongo_stats = {} | |
| for collection_name in mongo_collections: | |
| collection = db[collection_name] | |
| mongo_stats[collection_name] = { | |
| "document_count": collection.count_documents({}), | |
| "estimated_size": collection.estimated_document_count() | |
| } | |
| except Exception as e: | |
| mongo_collections = [] | |
| mongo_stats = {"error": str(e)} | |
| # Thông tin ChromaDB/Vector Database | |
| try: | |
| embedding_model = get_embedding_model() | |
| vector_stats = embedding_model.get_stats() | |
| chroma_status = "Connected" | |
| vector_count = embedding_model.count() | |
| except Exception as e: | |
| vector_stats = {"error": str(e)} | |
| chroma_status = "Error" | |
| vector_count = 0 | |
| # Thông tin Gemini API | |
| gemini_status = "Connected" if GEMINI_API_KEY else "Not configured" | |
| # Thông tin hệ thống | |
| import platform | |
| import psutil | |
| system_info = { | |
| "python_version": platform.python_version(), | |
| "platform": platform.platform(), | |
| "cpu_count": psutil.cpu_count(), | |
| "memory_total": round(psutil.virtual_memory().total / (1024**3), 2), # GB | |
| "memory_available": round(psutil.virtual_memory().available / (1024**3), 2), # GB | |
| "disk_usage": round(psutil.disk_usage('/').percent, 2) | |
| } | |
| # Cấu hình application | |
| app_config = { | |
| "debug_mode": os.getenv("FLASK_ENV") == "development", | |
| "secret_key_configured": bool(os.getenv("JWT_SECRET_KEY")), | |
| "mongodb_uri": os.getenv("MONGO_URI", "mongodb://localhost:27017/"), | |
| "database_name": os.getenv("MONGO_DB_NAME", "nutribot_db"), | |
| "embedding_model": EMBEDDING_MODEL, | |
| "chroma_directory": CHROMA_PERSIST_DIRECTORY, | |
| "collection_name": COLLECTION_NAME, | |
| "gemini_configured": bool(GEMINI_API_KEY) | |
| } | |
| return jsonify({ | |
| "success": True, | |
| "system_config": { | |
| "application": app_config, | |
| "system": system_info, | |
| "database": { | |
| "mongodb": { | |
| "status": "Connected", | |
| "collections": mongo_collections, | |
| "statistics": mongo_stats | |
| }, | |
| "vector_db": { | |
| "status": chroma_status, | |
| "document_count": vector_count, | |
| "statistics": vector_stats | |
| } | |
| }, | |
| "ai_services": { | |
| "gemini": { | |
| "status": gemini_status, | |
| "model": "gemini-2.0-flash" | |
| } | |
| } | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy system config: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_performance_metrics(): | |
| """Lấy metrics hiệu năng hệ thống""" | |
| try: | |
| import psutil | |
| import time | |
| from datetime import datetime, timedelta | |
| # CPU và Memory hiện tại | |
| cpu_percent = psutil.cpu_percent(interval=1) | |
| memory = psutil.virtual_memory() | |
| disk = psutil.disk_usage('/') | |
| # Thống kê database | |
| db = get_db() | |
| # Thống kê MongoDB performance | |
| try: | |
| # Thời gian trung bình cho queries (mock data - MongoDB professional monitoring cần tools khác) | |
| conversations_count = db.conversations.count_documents({}) | |
| users_count = db.users.count_documents({}) if hasattr(db, 'users') else 0 | |
| # Tốc độ xử lý tin nhắn trong 24h qua | |
| yesterday = datetime.now() - timedelta(days=1) | |
| recent_conversations = db.conversations.count_documents({ | |
| "updated_at": {"$gte": yesterday} | |
| }) | |
| db_performance = { | |
| "total_documents": conversations_count + users_count, | |
| "query_speed": "~50ms", # Mock data | |
| "recent_activity": recent_conversations, | |
| "index_efficiency": "95%" # Mock data | |
| } | |
| except Exception as e: | |
| db_performance = {"error": str(e)} | |
| # Vector DB performance | |
| try: | |
| from core.embedding_model import get_embedding_model | |
| embedding_model = get_embedding_model() | |
| vector_count = embedding_model.count() | |
| # Test search speed | |
| start_time = time.time() | |
| test_results = embedding_model.search("test query", top_k=5) | |
| search_time = (time.time() - start_time) * 1000 # milliseconds | |
| vector_performance = { | |
| "total_vectors": vector_count, | |
| "search_speed_ms": round(search_time, 2), | |
| "embedding_dimension": 768, # multilingual-e5-base dimension | |
| "retrieval_accuracy": "85%" # Mock data - would need evaluation dataset | |
| } | |
| except Exception as e: | |
| vector_performance = {"error": str(e)} | |
| # AI API performance | |
| ai_performance = { | |
| "average_response_time": "2.5s", # Mock data | |
| "success_rate": "98.5%", # Mock data | |
| "daily_requests": recent_conversations * 2, # Estimate | |
| "token_usage": "~150k tokens/day" # Mock data | |
| } | |
| return jsonify({ | |
| "success": True, | |
| "performance": { | |
| "system": { | |
| "cpu_usage": cpu_percent, | |
| "memory_usage": memory.percent, | |
| "memory_total_gb": round(memory.total / (1024**3), 2), | |
| "memory_used_gb": round(memory.used / (1024**3), 2), | |
| "disk_usage": disk.percent, | |
| "disk_total_gb": round(disk.total / (1024**3), 2), | |
| "uptime": "24h 15m" # Mock data | |
| }, | |
| "database": db_performance, | |
| "vector_search": vector_performance, | |
| "ai_generation": ai_performance | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy performance metrics: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_system_logs(): | |
| """Lấy logs hệ thống""" | |
| try: | |
| import os | |
| from datetime import datetime | |
| log_entries = [] | |
| # Đọc logs từ file nếu có | |
| log_files = [ | |
| "logs/app.log", | |
| "logs/error.log", | |
| "embed_data.log" | |
| ] | |
| for log_file in log_files: | |
| if os.path.exists(log_file): | |
| try: | |
| with open(log_file, 'r', encoding='utf-8') as f: | |
| lines = f.readlines() | |
| # Lấy 50 dòng cuối | |
| recent_lines = lines[-50:] if len(lines) > 50 else lines | |
| for line in recent_lines: | |
| if line.strip(): | |
| log_entries.append({ | |
| "timestamp": datetime.now().isoformat(), | |
| "level": "INFO", # Parse từ log format thực tế | |
| "source": log_file, | |
| "message": line.strip() | |
| }) | |
| except Exception as e: | |
| log_entries.append({ | |
| "timestamp": datetime.now().isoformat(), | |
| "level": "ERROR", | |
| "source": "system", | |
| "message": f"Cannot read {log_file}: {str(e)}" | |
| }) | |
| # Nếu không có log files, tạo mock logs | |
| if not log_entries: | |
| log_entries = [ | |
| { | |
| "timestamp": datetime.now().isoformat(), | |
| "level": "INFO", | |
| "source": "system", | |
| "message": "Application started successfully" | |
| }, | |
| { | |
| "timestamp": datetime.now().isoformat(), | |
| "level": "INFO", | |
| "source": "database", | |
| "message": "MongoDB connection established" | |
| }, | |
| { | |
| "timestamp": datetime.now().isoformat(), | |
| "level": "INFO", | |
| "source": "vector_db", | |
| "message": "ChromaDB initialized successfully" | |
| } | |
| ] | |
| # Sort by timestamp descending | |
| log_entries.sort(key=lambda x: x["timestamp"], reverse=True) | |
| return jsonify({ | |
| "success": True, | |
| "logs": log_entries[:100] # Limit to 100 entries | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy system logs: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def create_backup(): | |
| """Tạo backup dữ liệu""" | |
| try: | |
| from datetime import datetime | |
| import json | |
| import os | |
| # Tạo thư mục backup nếu chưa có | |
| backup_dir = "backups" | |
| if not os.path.exists(backup_dir): | |
| os.makedirs(backup_dir) | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| # Backup MongoDB data | |
| db = get_db() | |
| backup_data = { | |
| "timestamp": datetime.now().isoformat(), | |
| "collections": {} | |
| } | |
| # Export conversations | |
| conversations = list(db.conversations.find({})) | |
| for conv in conversations: | |
| conv["_id"] = str(conv["_id"]) # Convert ObjectId to string | |
| if "user_id" in conv: | |
| conv["user_id"] = str(conv["user_id"]) | |
| backup_data["collections"]["conversations"] = conversations | |
| # Export users if exists | |
| if hasattr(db, 'users'): | |
| users = list(db.users.find({})) | |
| for user in users: | |
| user["_id"] = str(user["_id"]) | |
| # Remove password for security | |
| if "password" in user: | |
| del user["password"] | |
| backup_data["collections"]["users"] = users | |
| # Save backup file | |
| backup_filename = f"backup_{timestamp}.json" | |
| backup_path = os.path.join(backup_dir, backup_filename) | |
| with open(backup_path, 'w', encoding='utf-8') as f: | |
| json.dump(backup_data, f, indent=2, default=str, ensure_ascii=False) | |
| # Get file size | |
| file_size = os.path.getsize(backup_path) | |
| file_size_mb = round(file_size / (1024 * 1024), 2) | |
| return jsonify({ | |
| "success": True, | |
| "message": "Backup created successfully", | |
| "backup": { | |
| "filename": backup_filename, | |
| "path": backup_path, | |
| "size_mb": file_size_mb, | |
| "timestamp": datetime.now().isoformat(), | |
| "collections_count": len(backup_data["collections"]), | |
| "total_documents": sum(len(coll) for coll in backup_data["collections"].values()) | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi tạo backup: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 | |
| def get_security_settings(): | |
| """Lấy cài đặt bảo mật""" | |
| try: | |
| import os | |
| from datetime import datetime, timedelta | |
| # Kiểm tra cấu hình bảo mật | |
| security_config = { | |
| "jwt_configured": bool(os.getenv("JWT_SECRET_KEY")), | |
| "admin_accounts": 1, # Mock data | |
| "password_policy": { | |
| "min_length": 6, | |
| "require_special_chars": False, | |
| "require_numbers": False | |
| }, | |
| "session_timeout": "24 hours", | |
| "ssl_enabled": False, # Mock data | |
| "rate_limiting": False # Mock data | |
| } | |
| # Recent login attempts (mock data) | |
| recent_logins = [ | |
| { | |
| "timestamp": (datetime.now() - timedelta(hours=1)).isoformat(), | |
| "user": "admin@nutribot.com", | |
| "ip": "127.0.0.1", | |
| "status": "success" | |
| }, | |
| { | |
| "timestamp": (datetime.now() - timedelta(hours=3)).isoformat(), | |
| "user": "admin@nutribot.com", | |
| "ip": "127.0.0.1", | |
| "status": "success" | |
| } | |
| ] | |
| # Security recommendations | |
| recommendations = [ | |
| { | |
| "priority": "high", | |
| "title": "Enable HTTPS", | |
| "description": "Deploy with SSL certificate for production" | |
| }, | |
| { | |
| "priority": "medium", | |
| "title": "Implement Rate Limiting", | |
| "description": "Add rate limiting to prevent abuse" | |
| }, | |
| { | |
| "priority": "low", | |
| "title": "Strengthen Password Policy", | |
| "description": "Require stronger passwords for admin accounts" | |
| } | |
| ] | |
| return jsonify({ | |
| "success": True, | |
| "security": { | |
| "configuration": security_config, | |
| "recent_logins": recent_logins, | |
| "recommendations": recommendations | |
| } | |
| }) | |
| except Exception as e: | |
| logger.error(f"Lỗi lấy security settings: {str(e)}") | |
| return jsonify({ | |
| "success": False, | |
| "error": str(e) | |
| }), 500 |