Spaces:
				
			
			
	
			
			
		Runtime error
		
	
	
	
			
			
	
	
	
	
		
		
		Runtime error
		
	Commit 
							
							·
						
						d3e9dc7
	
1
								Parent(s):
							
							253c2e1
								
update: check file uploaded
Browse files- Dockerfile +18 -24
- config.py +2 -2
- db/mongoDB.py +1 -1
- dependencies.py +4 -20
- main.py +3 -3
- routers/documents.py +1 -111
- services/document_service.py +2 -67
- utils/utils.py +1 -22
    	
        Dockerfile
    CHANGED
    
    | @@ -1,41 +1,33 @@ | |
| 1 | 
             
            # =================================================================
         | 
| 2 | 
             
            # STAGE 1: BUILDER - Stage để cài đặt các dependencies nặng
         | 
| 3 | 
             
            # =================================================================
         | 
| 4 | 
            -
            # Sử dụng image đầy đủ để có các công cụ build cần thiết
         | 
| 5 | 
             
            FROM python:3.10 as builder
         | 
| 6 |  | 
| 7 | 
             
            # Cập nhật và cài đặt các gói hệ thống cho việc build
         | 
| 8 | 
            -
            # Chỉ cài những gì thực sự cần để `pip install` hoạt động
         | 
| 9 | 
             
            RUN apt-get update && apt-get install -y --no-install-recommends \
         | 
| 10 | 
             
                build-essential \
         | 
| 11 | 
             
                && apt-get clean \
         | 
| 12 | 
             
                && rm -rf /var/lib/apt/lists/*
         | 
| 13 |  | 
| 14 | 
            -
            # Thiết lập thư mục làm việc
         | 
| 15 | 
             
            WORKDIR /app
         | 
| 16 |  | 
| 17 | 
            -
            # Tạo  | 
| 18 | 
            -
            # Đây là một thực hành tốt, giúp cô lập thư viện
         | 
| 19 | 
             
            RUN python -m venv /opt/venv
         | 
| 20 | 
            -
            # Kích hoạt venv cho các lệnh RUN tiếp theo
         | 
| 21 | 
             
            ENV PATH="/opt/venv/bin:$PATH"
         | 
| 22 |  | 
| 23 | 
             
            # Sao chép file requirements trước để tận dụng Docker layer caching
         | 
| 24 | 
             
            COPY requirements.txt .
         | 
| 25 |  | 
| 26 | 
            -
            # Cài đặt  | 
| 27 | 
            -
            # Điều này giúp tối ưu hóa số lượng layer của Docker
         | 
| 28 | 
             
            RUN pip install --no-cache-dir --upgrade pip && \
         | 
| 29 | 
             
                pip install --no-cache-dir -r requirements.txt
         | 
| 30 |  | 
| 31 | 
             
            # =================================================================
         | 
| 32 | 
             
            # STAGE 2: FINAL - Stage cuối cùng, nhỏ gọn để chạy ứng dụng
         | 
| 33 | 
             
            # =================================================================
         | 
| 34 | 
            -
            # Bắt đầu từ một image slim siêu nhẹ
         | 
| 35 | 
             
            FROM python:3.10-slim
         | 
| 36 |  | 
| 37 | 
             
            # Cài đặt chỉ các dependencies hệ thống cần thiết cho RUNTIME
         | 
| 38 | 
            -
            # Không cần `build-essential`, `git`, `curl` ở đây nữa
         | 
| 39 | 
             
            RUN apt-get update && apt-get install -y --no-install-recommends \
         | 
| 40 | 
             
                poppler-utils \
         | 
| 41 | 
             
                libgl1-mesa-glx \
         | 
| @@ -43,37 +35,39 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| 43 | 
             
                && apt-get clean \
         | 
| 44 | 
             
                && rm -rf /var/lib/apt/lists/*
         | 
| 45 |  | 
| 46 | 
            -
            # Thiết lập thư mục làm việc
         | 
| 47 | 
             
            WORKDIR /app
         | 
| 48 |  | 
| 49 | 
            -
            # Sao chép môi trường ảo  | 
| 50 | 
             
            COPY --from=builder /opt/venv /opt/venv
         | 
| 51 |  | 
| 52 | 
            -
            # Kích hoạt virtual environment | 
| 53 | 
             
            ENV PATH="/opt/venv/bin:$PATH"
         | 
| 54 |  | 
|  | |
| 55 | 
             
            # Thiết lập các biến môi trường quan trọng
         | 
| 56 | 
            -
            #  | 
| 57 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 58 | 
             
            ENV HF_HUB_DISABLE_SYMLINKS_WARNING=1
         | 
| 59 | 
            -
            # Đảm bảo log Python hiển thị ngay lập tức, rất quan trọng cho việc debug trên Render
         | 
| 60 | 
             
            ENV PYTHONUNBUFFERED=1
         | 
|  | |
| 61 |  | 
| 62 | 
             
            # Sao chép toàn bộ mã nguồn của ứng dụng
         | 
| 63 | 
             
            COPY . .
         | 
| 64 |  | 
| 65 | 
             
            # Tải trước (pre-download/bake) các model vào trong image
         | 
| 66 | 
            -
            #  | 
| 67 | 
            -
            # Các model sẽ được lưu vào thư mục cache đã định nghĩa bởi HF_HOME.
         | 
| 68 | 
            -
            # **QUAN TRỌNG**: Đảm bảo tên model ở đây khớp chính xác với tên trong file config.py của bạn.
         | 
| 69 | 
             
            RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('bkai-foundation-models/vietnamese-bi-encoder')"
         | 
| 70 | 
             
            RUN python -c "from langchain_community.cross_encoders import HuggingFaceCrossEncoder; HuggingFaceCrossEncoder(model_name='cross-encoder/ms-marco-MiniLM-L-6-v2')"
         | 
| 71 |  | 
| 72 | 
            -
            # Mở cổng | 
| 73 | 
            -
            # Port này phải khớp với port trong lệnh CMD
         | 
| 74 | 
             
            EXPOSE 7860
         | 
| 75 |  | 
| 76 | 
            -
            # Lệnh chạy ứng dụng | 
| 77 | 
            -
            # Gunicorn ổn định và hiệu quả hơn Uvicorn --reload
         | 
| 78 | 
            -
            # Nó sẽ tự động sử dụng biến $PORT do Render cung cấp
         | 
| 79 | 
             
            CMD ["gunicorn", "-w", "2", "-k", "uvicorn.workers.UvicornWorker", "main:app", "--bind", "0.0.0.0:7860", "--timeout", "120"]
         | 
|  | |
| 1 | 
             
            # =================================================================
         | 
| 2 | 
             
            # STAGE 1: BUILDER - Stage để cài đặt các dependencies nặng
         | 
| 3 | 
             
            # =================================================================
         | 
|  | |
| 4 | 
             
            FROM python:3.10 as builder
         | 
| 5 |  | 
| 6 | 
             
            # Cập nhật và cài đặt các gói hệ thống cho việc build
         | 
|  | |
| 7 | 
             
            RUN apt-get update && apt-get install -y --no-install-recommends \
         | 
| 8 | 
             
                build-essential \
         | 
| 9 | 
             
                && apt-get clean \
         | 
| 10 | 
             
                && rm -rf /var/lib/apt/lists/*
         | 
| 11 |  | 
|  | |
| 12 | 
             
            WORKDIR /app
         | 
| 13 |  | 
| 14 | 
            +
            # Tạo và kích hoạt venv
         | 
|  | |
| 15 | 
             
            RUN python -m venv /opt/venv
         | 
|  | |
| 16 | 
             
            ENV PATH="/opt/venv/bin:$PATH"
         | 
| 17 |  | 
| 18 | 
             
            # Sao chép file requirements trước để tận dụng Docker layer caching
         | 
| 19 | 
             
            COPY requirements.txt .
         | 
| 20 |  | 
| 21 | 
            +
            # Cài đặt thư viện Python
         | 
|  | |
| 22 | 
             
            RUN pip install --no-cache-dir --upgrade pip && \
         | 
| 23 | 
             
                pip install --no-cache-dir -r requirements.txt
         | 
| 24 |  | 
| 25 | 
             
            # =================================================================
         | 
| 26 | 
             
            # STAGE 2: FINAL - Stage cuối cùng, nhỏ gọn để chạy ứng dụng
         | 
| 27 | 
             
            # =================================================================
         | 
|  | |
| 28 | 
             
            FROM python:3.10-slim
         | 
| 29 |  | 
| 30 | 
             
            # Cài đặt chỉ các dependencies hệ thống cần thiết cho RUNTIME
         | 
|  | |
| 31 | 
             
            RUN apt-get update && apt-get install -y --no-install-recommends \
         | 
| 32 | 
             
                poppler-utils \
         | 
| 33 | 
             
                libgl1-mesa-glx \
         | 
|  | |
| 35 | 
             
                && apt-get clean \
         | 
| 36 | 
             
                && rm -rf /var/lib/apt/lists/*
         | 
| 37 |  | 
|  | |
| 38 | 
             
            WORKDIR /app
         | 
| 39 |  | 
| 40 | 
            +
            # Sao chép môi trường ảo từ stage builder
         | 
| 41 | 
             
            COPY --from=builder /opt/venv /opt/venv
         | 
| 42 |  | 
| 43 | 
            +
            # Kích hoạt virtual environment
         | 
| 44 | 
             
            ENV PATH="/opt/venv/bin:$PATH"
         | 
| 45 |  | 
| 46 | 
            +
            # --- PHẦN SỬA ĐỔI QUAN TRỌNG ---
         | 
| 47 | 
             
            # Thiết lập các biến môi trường quan trọng
         | 
| 48 | 
            +
            # 1. **SỬA LẠI ĐÂY**: Trỏ thư mục cache vào /tmp, nơi ứng dụng có quyền ghi
         | 
| 49 | 
            +
            #    Điều này sẽ sửa lỗi "Permission denied"
         | 
| 50 | 
            +
            ENV HF_HOME=/tmp/huggingface_cache
         | 
| 51 | 
            +
            ENV SENTENCE_TRANSFORMERS_HOME=/tmp/huggingface_cache
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            # 2. Tạo thư mục cache và cấp quyền (thực hành tốt)
         | 
| 54 | 
            +
            RUN mkdir -p /tmp/huggingface_cache && chmod 777 /tmp/huggingface_cache
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            # 3. Các biến môi trường khác giữ nguyên
         | 
| 57 | 
             
            ENV HF_HUB_DISABLE_SYMLINKS_WARNING=1
         | 
|  | |
| 58 | 
             
            ENV PYTHONUNBUFFERED=1
         | 
| 59 | 
            +
            # --- KẾT THÚC PHẦN SỬA ĐỔI ---
         | 
| 60 |  | 
| 61 | 
             
            # Sao chép toàn bộ mã nguồn của ứng dụng
         | 
| 62 | 
             
            COPY . .
         | 
| 63 |  | 
| 64 | 
             
            # Tải trước (pre-download/bake) các model vào trong image
         | 
| 65 | 
            +
            # Bây giờ các model sẽ được lưu vào /tmp/huggingface_cache bên trong image
         | 
|  | |
|  | |
| 66 | 
             
            RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('bkai-foundation-models/vietnamese-bi-encoder')"
         | 
| 67 | 
             
            RUN python -c "from langchain_community.cross_encoders import HuggingFaceCrossEncoder; HuggingFaceCrossEncoder(model_name='cross-encoder/ms-marco-MiniLM-L-6-v2')"
         | 
| 68 |  | 
| 69 | 
            +
            # Mở cổng
         | 
|  | |
| 70 | 
             
            EXPOSE 7860
         | 
| 71 |  | 
| 72 | 
            +
            # Lệnh chạy ứng dụng
         | 
|  | |
|  | |
| 73 | 
             
            CMD ["gunicorn", "-w", "2", "-k", "uvicorn.workers.UvicornWorker", "main:app", "--bind", "0.0.0.0:7860", "--timeout", "120"]
         | 
    	
        config.py
    CHANGED
    
    | @@ -18,7 +18,7 @@ CORE_DATA_FOLDER = os.path.join(BASE_DIR, "data", "core") | |
| 18 | 
             
            # PROCESSED_FILES_FOLDER = os.path.join(BASE_DIR, "data", "processed_files")
         | 
| 19 | 
             
            # FAILED_FILES_FOLDER = os.path.join(BASE_DIR, "data", "failed_files")
         | 
| 20 | 
             
            # PROCESSED_HASH_LOG = os.path.join(BASE_DIR, "data", "processed_hashes.log")
         | 
| 21 | 
            -
            PENDING_UPLOADS_FOLDER = '/tmp/pending_uploads'
         | 
| 22 | 
             
            LEGAL_DIC_FOLDER = os.path.join(BASE_DIR, "data", "dictionary")
         | 
| 23 |  | 
| 24 | 
             
            # Cấu hình cho DB
         | 
| @@ -53,7 +53,7 @@ FRONTEND_URL = os.environ.get("FRONTEND_URL") | |
| 53 |  | 
| 54 | 
             
            APP_ENVIRONMENT = os.environ.get("APP_ENVIRONMENT")
         | 
| 55 |  | 
| 56 | 
            -
            CHECKPOINT_FILE = "processed_files.log"
         | 
| 57 |  | 
| 58 | 
             
            MONGODB_CLOUD_URI= os.environ.get("MONGODB_CLOUD_URI")
         | 
| 59 | 
             
            DB_NAME= os.environ.get("DB_NAME")
         | 
|  | |
| 18 | 
             
            # PROCESSED_FILES_FOLDER = os.path.join(BASE_DIR, "data", "processed_files")
         | 
| 19 | 
             
            # FAILED_FILES_FOLDER = os.path.join(BASE_DIR, "data", "failed_files")
         | 
| 20 | 
             
            # PROCESSED_HASH_LOG = os.path.join(BASE_DIR, "data", "processed_hashes.log")
         | 
| 21 | 
            +
            # PENDING_UPLOADS_FOLDER = '/tmp/pending_uploads'
         | 
| 22 | 
             
            LEGAL_DIC_FOLDER = os.path.join(BASE_DIR, "data", "dictionary")
         | 
| 23 |  | 
| 24 | 
             
            # Cấu hình cho DB
         | 
|  | |
| 53 |  | 
| 54 | 
             
            APP_ENVIRONMENT = os.environ.get("APP_ENVIRONMENT")
         | 
| 55 |  | 
| 56 | 
            +
            # CHECKPOINT_FILE = "processed_files.log"
         | 
| 57 |  | 
| 58 | 
             
            MONGODB_CLOUD_URI= os.environ.get("MONGODB_CLOUD_URI")
         | 
| 59 | 
             
            DB_NAME= os.environ.get("DB_NAME")
         | 
    	
        db/mongoDB.py
    CHANGED
    
    | @@ -31,7 +31,7 @@ async def connect_to_mongo(): | |
| 31 | 
             
                Hàm này sẽ được gọi từ lifespan của FastAPI.
         | 
| 32 | 
             
                """
         | 
| 33 | 
             
                if mongo_db.client:
         | 
| 34 | 
            -
                    logger.info("MongoDB connection already established.")
         | 
| 35 | 
             
                    return
         | 
| 36 |  | 
| 37 | 
             
                logger.info(f"🔸 Connecting to MongoDB Atlas...")
         | 
|  | |
| 31 | 
             
                Hàm này sẽ được gọi từ lifespan của FastAPI.
         | 
| 32 | 
             
                """
         | 
| 33 | 
             
                if mongo_db.client:
         | 
| 34 | 
            +
                    logger.info("✅ MongoDB connection already established.")
         | 
| 35 | 
             
                    return
         | 
| 36 |  | 
| 37 | 
             
                logger.info(f"🔸 Connecting to MongoDB Atlas...")
         | 
    	
        dependencies.py
    CHANGED
    
    | @@ -38,22 +38,6 @@ def get_app_state(request: Request): | |
| 38 | 
             
                    raise RuntimeError("Application state ('app_state') not found. Initialization failed?")
         | 
| 39 | 
             
                return request.app.state.app_state
         | 
| 40 |  | 
| 41 | 
            -
            # def initialize_redis_client():
         | 
| 42 | 
            -
            #     redis_url = os.environ.get("REDIS_URL")
         | 
| 43 | 
            -
            #     if not redis_url:
         | 
| 44 | 
            -
            #         logger.error("🔸[Redis] REDIS_URL environment variable not set.")
         | 
| 45 | 
            -
            #         raise ValueError("REDIS_URL is not configured.")
         | 
| 46 | 
            -
            #     try:
         | 
| 47 | 
            -
            #         logger.info(f"🔸[Redis] Attempting to connect to Redis at {redis_url}...")
         | 
| 48 | 
            -
            #         client = redis.Redis.from_url(redis_url, socket_connect_timeout=5, socket_timeout=5)
         | 
| 49 | 
            -
            #         logger.info("🔸[Redis] Connected successfully and pinged.")
         | 
| 50 | 
            -
            #         return client
         | 
| 51 | 
            -
            #     except redis.exceptions.ConnectionError as e:
         | 
| 52 | 
            -
            #         logger.error(f"🔸[Redis] Connection failed for URL '{redis_url}': {e}")
         | 
| 53 | 
            -
            #         raise ConnectionError(f"Failed to connect to Redis: {e}")
         | 
| 54 | 
            -
            #     except Exception as e:
         | 
| 55 | 
            -
            #         logger.error(f"🔸[Redis] Error initializing Redis from URL '{redis_url}': {e}")
         | 
| 56 | 
            -
            #         raise RuntimeError(f"Error initializing Redis: {e}")
         | 
| 57 |  | 
| 58 | 
             
            async def initialize_api_components(app_state: AppState):
         | 
| 59 | 
             
                """Khởi tạo các thành phần cần thiết cho API """
         | 
| @@ -184,7 +168,7 @@ async def get_current_user( | |
| 184 | 
             
                    logger.error(f"GET_CURRENT_USER: *** KHÔNG TÌM THẤY TOKEN (Nguồn: {source_of_token}) - RAISING 401 ***")
         | 
| 185 | 
             
                    raise credentials_exception
         | 
| 186 |  | 
| 187 | 
            -
                logger.info(f"GET_CURRENT_USER: Token để verify (nguồn: {source_of_token}): {token_to_verify[:20]}...")
         | 
| 188 |  | 
| 189 | 
             
                # 1. Kiểm tra token trong blacklist
         | 
| 190 | 
             
                try:
         | 
| @@ -221,7 +205,7 @@ async def get_current_user( | |
| 221 | 
             
                    email = payload.get("sub")
         | 
| 222 | 
             
                    exp = payload.get("exp")
         | 
| 223 |  | 
| 224 | 
            -
                    logger.info(f"GET_CURRENT_USER: JWT decode thành công - email: {email}, exp: {exp}")
         | 
| 225 |  | 
| 226 | 
             
                    if not isinstance(email, str) or not email:
         | 
| 227 | 
             
                        logger.error("GET_CURRENT_USER: *** EMAIL KHÔNG HỢP LỆ TRONG TOKEN ***")
         | 
| @@ -265,7 +249,7 @@ async def get_current_user( | |
| 265 | 
             
                # 3. Lấy thông tin người dùng từ database
         | 
| 266 | 
             
                user_data: Optional[dict] = None # Khởi tạo để tránh UnboundLocalError
         | 
| 267 | 
             
                try:
         | 
| 268 | 
            -
                    logger.info(f"GET_CURRENT_USER: Đang tìm user trong DB: {email.lower()}") # email đã được validate là str
         | 
| 269 | 
             
                    user_data = await mongo_db.users.find_one({"email": email.lower()}, {"password": 0, "_id": 0})
         | 
| 270 | 
             
                    # print(user_data) # Bỏ print trong production
         | 
| 271 |  | 
| @@ -273,7 +257,7 @@ async def get_current_user( | |
| 273 | 
             
                        logger.error(f"GET_CURRENT_USER: *** KHÔNG TÌM THẤY USER TRONG DB ({email.lower()}) - RAISING 401 ***")
         | 
| 274 | 
             
                        raise credentials_exception
         | 
| 275 |  | 
| 276 | 
            -
                    logger.info(f"GET_CURRENT_USER: Tìm thấy user - data: {user_data}")
         | 
| 277 |  | 
| 278 | 
             
                except HTTPException:
         | 
| 279 | 
             
                    raise
         | 
|  | |
| 38 | 
             
                    raise RuntimeError("Application state ('app_state') not found. Initialization failed?")
         | 
| 39 | 
             
                return request.app.state.app_state
         | 
| 40 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 41 |  | 
| 42 | 
             
            async def initialize_api_components(app_state: AppState):
         | 
| 43 | 
             
                """Khởi tạo các thành phần cần thiết cho API """
         | 
|  | |
| 168 | 
             
                    logger.error(f"GET_CURRENT_USER: *** KHÔNG TÌM THẤY TOKEN (Nguồn: {source_of_token}) - RAISING 401 ***")
         | 
| 169 | 
             
                    raise credentials_exception
         | 
| 170 |  | 
| 171 | 
            +
                # logger.info(f"GET_CURRENT_USER: Token để verify (nguồn: {source_of_token}): {token_to_verify[:20]}...")
         | 
| 172 |  | 
| 173 | 
             
                # 1. Kiểm tra token trong blacklist
         | 
| 174 | 
             
                try:
         | 
|  | |
| 205 | 
             
                    email = payload.get("sub")
         | 
| 206 | 
             
                    exp = payload.get("exp")
         | 
| 207 |  | 
| 208 | 
            +
                    # logger.info(f"GET_CURRENT_USER: JWT decode thành công - email: {email}, exp: {exp}")
         | 
| 209 |  | 
| 210 | 
             
                    if not isinstance(email, str) or not email:
         | 
| 211 | 
             
                        logger.error("GET_CURRENT_USER: *** EMAIL KHÔNG HỢP LỆ TRONG TOKEN ***")
         | 
|  | |
| 249 | 
             
                # 3. Lấy thông tin người dùng từ database
         | 
| 250 | 
             
                user_data: Optional[dict] = None # Khởi tạo để tránh UnboundLocalError
         | 
| 251 | 
             
                try:
         | 
| 252 | 
            +
                    # logger.info(f"GET_CURRENT_USER: Đang tìm user trong DB: {email.lower()}") # email đã được validate là str
         | 
| 253 | 
             
                    user_data = await mongo_db.users.find_one({"email": email.lower()}, {"password": 0, "_id": 0})
         | 
| 254 | 
             
                    # print(user_data) # Bỏ print trong production
         | 
| 255 |  | 
|  | |
| 257 | 
             
                        logger.error(f"GET_CURRENT_USER: *** KHÔNG TÌM THẤY USER TRONG DB ({email.lower()}) - RAISING 401 ***")
         | 
| 258 | 
             
                        raise credentials_exception
         | 
| 259 |  | 
| 260 | 
            +
                    # logger.info(f"GET_CURRENT_USER: Tìm thấy user - data: {user_data}")
         | 
| 261 |  | 
| 262 | 
             
                except HTTPException:
         | 
| 263 | 
             
                    raise
         | 
    	
        main.py
    CHANGED
    
    | @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) | |
| 22 |  | 
| 23 | 
             
            @asynccontextmanager
         | 
| 24 | 
             
            async def lifespan(app: FastAPI):
         | 
| 25 | 
            -
                logger.info(" | 
| 26 |  | 
| 27 | 
             
                current_app_state_instance = AppState()
         | 
| 28 | 
             
                initialization_successful = False
         | 
| @@ -71,7 +71,7 @@ app = FastAPI( | |
| 71 |  | 
| 72 | 
             
            app.add_middleware(
         | 
| 73 | 
             
                SessionMiddleware,
         | 
| 74 | 
            -
                secret_key=os.environ.get("SESSION_SECRET_KEY" | 
| 75 | 
             
            )
         | 
| 76 |  | 
| 77 |  | 
| @@ -93,7 +93,7 @@ app.include_router(health_router, prefix="/api", tags=["Status"]) # Hoặc chỉ | |
| 93 |  | 
| 94 | 
             
            # Run with Uvicorn
         | 
| 95 | 
             
            if __name__ == "__main__":
         | 
| 96 | 
            -
                logger.info(" | 
| 97 | 
             
                is_dev_mode = config.APP_ENVIRONMENT.lower() == "development"
         | 
| 98 | 
             
                uvicorn.run(
         | 
| 99 | 
             
                    "main:app", # Đảm bảo "main" là tên file python của bạn
         | 
|  | |
| 22 |  | 
| 23 | 
             
            @asynccontextmanager
         | 
| 24 | 
             
            async def lifespan(app: FastAPI):
         | 
| 25 | 
            +
                logger.info("🚀 [Lifespan] STARTING UP...")
         | 
| 26 |  | 
| 27 | 
             
                current_app_state_instance = AppState()
         | 
| 28 | 
             
                initialization_successful = False
         | 
|  | |
| 71 |  | 
| 72 | 
             
            app.add_middleware(
         | 
| 73 | 
             
                SessionMiddleware,
         | 
| 74 | 
            +
                secret_key=os.environ.get("SESSION_SECRET_KEY")
         | 
| 75 | 
             
            )
         | 
| 76 |  | 
| 77 |  | 
|  | |
| 93 |  | 
| 94 | 
             
            # Run with Uvicorn
         | 
| 95 | 
             
            if __name__ == "__main__":
         | 
| 96 | 
            +
                logger.info("🚀 Chạy FastAPI server với Uvicorn...")
         | 
| 97 | 
             
                is_dev_mode = config.APP_ENVIRONMENT.lower() == "development"
         | 
| 98 | 
             
                uvicorn.run(
         | 
| 99 | 
             
                    "main:app", # Đảm bảo "main" là tên file python của bạn
         | 
    	
        routers/documents.py
    CHANGED
    
    | @@ -1,113 +1,3 @@ | |
| 1 | 
            -
            # from fastapi import APIRouter, UploadFile, File, BackgroundTasks, HTTPException, Depends, Request
         | 
| 2 | 
            -
            # import os
         | 
| 3 | 
            -
            # import time
         | 
| 4 | 
            -
            # import shutil
         | 
| 5 | 
            -
            # from schemas.user import UserOut
         | 
| 6 | 
            -
            # from dependencies import get_current_user
         | 
| 7 | 
            -
            # import logging
         | 
| 8 | 
            -
            # from typing import List
         | 
| 9 | 
            -
            # import config
         | 
| 10 | 
            -
            # from utils.utils import calculate_file_hash, check_if_hash_exists
         | 
| 11 | 
            -
            # from services.document_service import full_process_and_ingest_pipeline
         | 
| 12 | 
            -
            # from dependencies import get_app_state
         | 
| 13 | 
            -
            # logger = logging.getLogger(__name__)
         | 
| 14 | 
            -
             | 
| 15 | 
            -
            # router = APIRouter()
         | 
| 16 | 
            -
             | 
| 17 | 
            -
            # ALLOWED_EXTENSIONS = {".pdf", ".docx", ".doc"}
         | 
| 18 | 
            -
             | 
| 19 | 
            -
            # @router.post("/upload", status_code=202)
         | 
| 20 | 
            -
            # async def upload_and_ingest_documents(
         | 
| 21 | 
            -
            #     fastapi_request: Request,
         | 
| 22 | 
            -
            #     background_tasks: BackgroundTasks,
         | 
| 23 | 
            -
            #     current_user: UserOut = Depends(get_current_user),
         | 
| 24 | 
            -
            #     files: List[UploadFile] = File(..., description="Một hoặc nhiều file tài liệu cần upload.")
         | 
| 25 | 
            -
            # ):
         | 
| 26 | 
            -
            #     """
         | 
| 27 | 
            -
            #     Endpoint duy nhất để upload một hoặc nhiều tài liệu.
         | 
| 28 | 
            -
             | 
| 29 | 
            -
            #     - **files**: Danh sách các file tài liệu cần upload.
         | 
| 30 | 
            -
            #     - API sẽ xử lý từng file trong nền và trả về ngay một báo cáo tổng hợp.
         | 
| 31 | 
            -
            #     - File trùng lặp (dựa trên nội dung) hoặc có định dạng không hỗ trợ sẽ bị bỏ qua.
         | 
| 32 | 
            -
            #     """
         | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
            #     # Dòng này giờ sẽ chạy thành công vì bạn có quyền ghi vào /tmp
         | 
| 36 | 
            -
            #     os.makedirs(config.PENDING_UPLOADS_FOLDER , exist_ok=True)
         | 
| 37 | 
            -
             | 
| 38 | 
            -
            #     app_state = get_app_state(request=fastapi_request)
         | 
| 39 | 
            -
            #     embedding_model = app_state.embeddings
         | 
| 40 | 
            -
            #     if not files:
         | 
| 41 | 
            -
            #         raise HTTPException(status_code=400, detail="No files were uploaded.")
         | 
| 42 | 
            -
             | 
| 43 | 
            -
            #     accepted_files = []
         | 
| 44 | 
            -
            #     skipped_files = []
         | 
| 45 | 
            -
             | 
| 46 | 
            -
            #     for file in files:
         | 
| 47 | 
            -
            #         temp_file_path = None
         | 
| 48 | 
            -
            #         try:
         | 
| 49 | 
            -
            #             # Kiểm tra định dạng file
         | 
| 50 | 
            -
            #             file_extension = os.path.splitext(file.filename)[1].lower()
         | 
| 51 | 
            -
            #             if file_extension not in ALLOWED_EXTENSIONS:
         | 
| 52 | 
            -
            #                 skipped_files.append({"filename": file.filename, "reason": "Unsupported file type"})
         | 
| 53 | 
            -
            #                 continue
         | 
| 54 | 
            -
             | 
| 55 | 
            -
            #             # 1. Lưu file tạm để tính hash
         | 
| 56 | 
            -
            #             # Thêm timestamp để tránh xung đột tên file nếu upload nhiều file cùng tên trong 1 request
         | 
| 57 | 
            -
            #             temp_filename = f"temp_{int(time.time()*1000)}_{file.filename}"
         | 
| 58 | 
            -
            #             temp_file_path = os.path.join(config.PENDING_UPLOADS_FOLDER, temp_filename)
         | 
| 59 | 
            -
            #             with open(temp_file_path, "wb") as buffer:
         | 
| 60 | 
            -
            #                 shutil.copyfileobj(file.file, buffer)
         | 
| 61 | 
            -
             | 
| 62 | 
            -
            #             # 2. Tính toán hash
         | 
| 63 | 
            -
            #             file_hash = calculate_file_hash(temp_file_path)
         | 
| 64 | 
            -
             | 
| 65 | 
            -
            #             # 3. Kiểm tra trùng lặp
         | 
| 66 | 
            -
            #             if await check_if_hash_exists(file_hash):
         | 
| 67 | 
            -
            #                 skipped_files.append({"filename": file.filename, "reason": "Duplicate file content"})
         | 
| 68 | 
            -
            #                 os.remove(temp_file_path)
         | 
| 69 | 
            -
            #                 continue
         | 
| 70 | 
            -
             | 
| 71 | 
            -
            #             # 4. File hợp lệ, chuẩn bị để xử lý
         | 
| 72 | 
            -
            #             final_filename = file.filename
         | 
| 73 | 
            -
            #             final_file_path = os.path.join(config.PENDING_UPLOADS_FOLDER, final_filename)
         | 
| 74 | 
            -
            #             # Xử lý nếu tên file đã tồn tại để tránh ghi đè
         | 
| 75 | 
            -
            #             if os.path.exists(final_file_path):
         | 
| 76 | 
            -
            #                  base, ext = os.path.splitext(final_filename)
         | 
| 77 | 
            -
            #                  final_filename = f"{base}_{file_hash[:8]}{ext}"
         | 
| 78 | 
            -
            #                  final_file_path = os.path.join(config.PENDING_UPLOADS_FOLDER, final_filename)
         | 
| 79 | 
            -
             | 
| 80 | 
            -
            #             os.rename(temp_file_path, final_file_path)
         | 
| 81 | 
            -
            #             temp_file_path = None # Đánh dấu là đã di chuyển
         | 
| 82 | 
            -
             | 
| 83 | 
            -
            #             # 5. Thêm tác vụ nền cho file này
         | 
| 84 | 
            -
            #             background_tasks.add_task(full_process_and_ingest_pipeline, final_file_path, file_hash,embedding_model)
         | 
| 85 | 
            -
             | 
| 86 | 
            -
            #             accepted_files.append({"filename": final_filename, "hash": file_hash})
         | 
| 87 | 
            -
             | 
| 88 | 
            -
            #         except Exception as e:
         | 
| 89 | 
            -
            #             logger.error(f"Error processing {file.filename} in upload batch: {e}", exc_info=True)
         | 
| 90 | 
            -
            #             skipped_files.append({"filename": file.filename, "reason": f"Server error: {str(e)}"})
         | 
| 91 | 
            -
            #             if temp_file_path and os.path.exists(temp_file_path):
         | 
| 92 | 
            -
            #                 os.remove(temp_file_path)
         | 
| 93 | 
            -
             | 
| 94 | 
            -
            #     # Nếu không có file nào được chấp nhận sau khi lọc
         | 
| 95 | 
            -
            #     if not accepted_files:
         | 
| 96 | 
            -
            #         raise HTTPException(
         | 
| 97 | 
            -
            #             status_code=400,
         | 
| 98 | 
            -
            #             detail={"message": "No valid new files were accepted for processing.", "skipped_files": skipped_files}
         | 
| 99 | 
            -
            #         )
         | 
| 100 | 
            -
             | 
| 101 | 
            -
            #     # Trả về kết quả tổng hợp
         | 
| 102 | 
            -
            #     return {
         | 
| 103 | 
            -
            #         "message": f"Request completed. Accepted {len(accepted_files)} files for background processing.",
         | 
| 104 | 
            -
            #         "accepted_files": accepted_files,
         | 
| 105 | 
            -
            #         "skipped_files": skipped_files
         | 
| 106 | 
            -
            #     }
         | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
            # routers/documents.py
         | 
| 110 | 
            -
             | 
| 111 | 
             
            import os
         | 
| 112 | 
             
            import hashlib
         | 
| 113 | 
             
            from typing import List
         | 
| @@ -116,7 +6,7 @@ from io import BytesIO | |
| 116 | 
             
            from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, BackgroundTasks
         | 
| 117 | 
             
            from fastapi.concurrency import run_in_threadpool
         | 
| 118 |  | 
| 119 | 
            -
            from  | 
| 120 | 
             
            from services.document_service import full_process_and_ingest_pipeline,convert_to_text_content
         | 
| 121 |  | 
| 122 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 1 | 
             
            import os
         | 
| 2 | 
             
            import hashlib
         | 
| 3 | 
             
            from typing import List
         | 
|  | |
| 6 | 
             
            from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, BackgroundTasks
         | 
| 7 | 
             
            from fastapi.concurrency import run_in_threadpool
         | 
| 8 |  | 
| 9 | 
            +
            from services.document_service import  check_if_hash_exists
         | 
| 10 | 
             
            from services.document_service import full_process_and_ingest_pipeline,convert_to_text_content
         | 
| 11 |  | 
| 12 |  | 
    	
        services/document_service.py
    CHANGED
    
    | @@ -18,71 +18,6 @@ logger = logging.getLogger(__name__) | |
| 18 | 
             
            from rag_components import create_weaviate_schema_if_not_exists, ingest_chunks_with_native_batching
         | 
| 19 | 
             
            from utils.process_data import hierarchical_split_law_document,extract_document_metadata,clean_document_text,infer_field, infer_entity_type, filter_and_serialize_complex_metadata
         | 
| 20 |  | 
| 21 | 
            -
            # def convert_to_text_content(source_path: str) -> str:
         | 
| 22 | 
            -
            #     source_file = Path(source_path)
         | 
| 23 | 
            -
            #     file_extension = source_file.suffix.lower()
         | 
| 24 | 
            -
            #     logger.info(f"Extracting content from: {source_file.name}")
         | 
| 25 | 
            -
            #     content = ""
         | 
| 26 | 
            -
            #     if file_extension == ".pdf":
         | 
| 27 | 
            -
            #         parser = LlamaParse( api_key=config.LLAMA_CLOUD_API_KEY,
         | 
| 28 | 
            -
            #                     result_type="text",
         | 
| 29 | 
            -
            #                     verbose=True, # Giữ verbose để theo dõi
         | 
| 30 | 
            -
            #                     language="vi")
         | 
| 31 | 
            -
            #         documents = parser.load_data([str(source_file)])
         | 
| 32 | 
            -
            #         if documents: content = documents[0].text
         | 
| 33 | 
            -
            #     elif file_extension == ".docx":
         | 
| 34 | 
            -
            #         doc = docx.Document(source_path)
         | 
| 35 | 
            -
            #         content = '\n'.join([para.text for para in doc.paragraphs])
         | 
| 36 | 
            -
            #     elif file_extension == ".doc":
         | 
| 37 | 
            -
            #         content = pypandoc.convert_file(source_path, 'plain', format='doc')
         | 
| 38 | 
            -
            #     else:
         | 
| 39 | 
            -
            #         raise ValueError(f"Unsupported file format: {file_extension}")
         | 
| 40 | 
            -
            #     if not content.strip():
         | 
| 41 | 
            -
            #         raise ValueError("Extracted content is empty.")
         | 
| 42 | 
            -
            #     logger.info(f"✅ Successfully extracted content from {source_file.name}.")
         | 
| 43 | 
            -
            #     return content
         | 
| 44 | 
            -
             | 
| 45 | 
            -
            # async def full_process_and_ingest_pipeline(filepath: str, file_hash: str, embedding_model):
         | 
| 46 | 
            -
            #     filename = os.path.basename(filepath)
         | 
| 47 | 
            -
            #     logger.info(f"BACKGROUND TASK: Starting full pipeline for: {filename} (Hash: {file_hash[:10]}...)")
         | 
| 48 | 
            -
            #     weaviate_client = None
         | 
| 49 | 
            -
            #     try:
         | 
| 50 | 
            -
            #         raw_content = convert_to_text_content(filepath)
         | 
| 51 | 
            -
             | 
| 52 | 
            -
            #         doc_metadata = extract_document_metadata(raw_content, filename)
         | 
| 53 | 
            -
            #         doc_metadata["source"] = filename
         | 
| 54 | 
            -
            #         cleaned_content = clean_document_text(raw_content)
         | 
| 55 | 
            -
            #         doc_metadata["field"] = infer_field(cleaned_content, doc_metadata.get("ten_van_ban"))
         | 
| 56 | 
            -
            #         doc_metadata["entity_type"] = infer_entity_type(cleaned_content, doc_metadata.get("field", ""))
         | 
| 57 | 
            -
             | 
| 58 | 
            -
            #         doc_to_split = Document(page_content=cleaned_content, metadata=doc_metadata)
         | 
| 59 | 
            -
            #         chunks_from_file = hierarchical_split_law_document(doc_to_split)
         | 
| 60 | 
            -
             | 
| 61 | 
            -
            #         if not chunks_from_file:
         | 
| 62 | 
            -
            #             raise ValueError("File did not yield any chunks after processing.")
         | 
| 63 | 
            -
             | 
| 64 | 
            -
            #         processed_chunks = filter_and_serialize_complex_metadata(chunks_from_file)
         | 
| 65 | 
            -
             | 
| 66 | 
            -
            #         weaviate_client = connect_to_weaviate()
         | 
| 67 | 
            -
            #         embeddings_model = embedding_model
         | 
| 68 | 
            -
            #         collection_name = config.WEAVIATE_COLLECTION_NAME
         | 
| 69 | 
            -
            #         create_weaviate_schema_if_not_exists(weaviate_client, collection_name)
         | 
| 70 | 
            -
             | 
| 71 | 
            -
            #         ingest_chunks_with_native_batching(weaviate_client, collection_name, processed_chunks, embeddings_model)
         | 
| 72 | 
            -
             | 
| 73 | 
            -
            #         await utils.log_processed_hash(file_hash, filename)
         | 
| 74 | 
            -
            #         logger.info(f"✅ Successfully ingested '{filename}'.")
         | 
| 75 | 
            -
            #         # shutil.move(filepath, os.path.join(config.PROCESSED_FILES_FOLDER, filename))
         | 
| 76 | 
            -
            #         logger.info(f"Moved '{filename}' to processed folder.")
         | 
| 77 | 
            -
            #     except Exception as e:
         | 
| 78 | 
            -
            #         logger.error(f"❌ FAILED pipeline for '{filename}': {e}", exc_info=True)
         | 
| 79 | 
            -
            #         # shutil.move(filepath, os.path.join(config.FAILED_FILES_FOLDER, filename))
         | 
| 80 | 
            -
            #         logger.info(f"Moved '{filename}' to failed folder.")
         | 
| 81 | 
            -
            #     finally:
         | 
| 82 | 
            -
            #         if weaviate_client and weaviate_client.is_connected():
         | 
| 83 | 
            -
            #             weaviate_client.close()
         | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
             
            # --- SỬA LẠI HÀM NÀY ĐỂ NHẬN STREAM ---
         | 
| 87 | 
             
            def convert_to_text_content(source_stream: BytesIO, original_filename: str) -> str:
         | 
| 88 | 
             
                """Trích xuất nội dung text từ một stream trong bộ nhớ."""
         | 
| @@ -147,7 +82,7 @@ async def full_process_and_ingest_pipeline(raw_content: str, filename: str, file | |
| 147 | 
             
                    processed_chunks = filter_and_serialize_complex_metadata(chunks_from_file)
         | 
| 148 |  | 
| 149 | 
             
                    # Giai đoạn 2: Ingest vào Weaviate (I/O-bound và CPU-bound)
         | 
| 150 | 
            -
                    weaviate_client = connect_to_weaviate()
         | 
| 151 |  | 
| 152 |  | 
| 153 | 
             
                    await run_in_threadpool(create_weaviate_schema_if_not_exists, weaviate_client, config.WEAVIATE_COLLECTION_NAME)
         | 
| @@ -191,4 +126,4 @@ async def log_failed_process(file_hash: str, filename: str, error_message: str): | |
| 191 | 
             
            # Hàm kiểm tra trùng lặp
         | 
| 192 | 
             
            async def check_if_hash_exists(file_hash: str) -> bool:
         | 
| 193 | 
             
                count = await mongo_db.processed_documents.count_documents({"file_hash": file_hash, "status": "SUCCESS"})
         | 
| 194 | 
            -
                return count > 0
         | 
|  | |
| 18 | 
             
            from rag_components import create_weaviate_schema_if_not_exists, ingest_chunks_with_native_batching
         | 
| 19 | 
             
            from utils.process_data import hierarchical_split_law_document,extract_document_metadata,clean_document_text,infer_field, infer_entity_type, filter_and_serialize_complex_metadata
         | 
| 20 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 21 | 
             
            # --- SỬA LẠI HÀM NÀY ĐỂ NHẬN STREAM ---
         | 
| 22 | 
             
            def convert_to_text_content(source_stream: BytesIO, original_filename: str) -> str:
         | 
| 23 | 
             
                """Trích xuất nội dung text từ một stream trong bộ nhớ."""
         | 
|  | |
| 82 | 
             
                    processed_chunks = filter_and_serialize_complex_metadata(chunks_from_file)
         | 
| 83 |  | 
| 84 | 
             
                    # Giai đoạn 2: Ingest vào Weaviate (I/O-bound và CPU-bound)
         | 
| 85 | 
            +
                    weaviate_client = connect_to_weaviate(run_diagnostics=False)
         | 
| 86 |  | 
| 87 |  | 
| 88 | 
             
                    await run_in_threadpool(create_weaviate_schema_if_not_exists, weaviate_client, config.WEAVIATE_COLLECTION_NAME)
         | 
|  | |
| 126 | 
             
            # Hàm kiểm tra trùng lặp
         | 
| 127 | 
             
            async def check_if_hash_exists(file_hash: str) -> bool:
         | 
| 128 | 
             
                count = await mongo_db.processed_documents.count_documents({"file_hash": file_hash, "status": "SUCCESS"})
         | 
| 129 | 
            +
                return count > 0
         | 
    	
        utils/utils.py
    CHANGED
    
    | @@ -7,7 +7,7 @@ from typing import List, Optional | |
| 7 | 
             
            from schemas.chat import  Message
         | 
| 8 | 
             
            from redis.asyncio import Redis
         | 
| 9 | 
             
            import bcrypt
         | 
| 10 | 
            -
            from datetime import datetime, timedelta | 
| 11 | 
             
            from jose import jwt
         | 
| 12 | 
             
            from config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
         | 
| 13 | 
             
            from typing import List, Dict,  Optional
         | 
| @@ -444,7 +444,6 @@ async def get_langchain_chat_history(app_state, chat_id: str) -> RedisChatMessag | |
| 444 | 
             
            # api/utils.py
         | 
| 445 |  | 
| 446 | 
             
            import hashlib
         | 
| 447 | 
            -
            import config
         | 
| 448 |  | 
| 449 | 
             
            logger = logging.getLogger(__name__)
         | 
| 450 |  | 
| @@ -465,23 +464,3 @@ def calculate_file_hash(filepath: str) -> str: | |
| 465 | 
             
            #     except IOError as e:
         | 
| 466 | 
             
            #         logger.error(f"Could not read hash log file: {e}")
         | 
| 467 | 
             
            #         return False
         | 
| 468 | 
            -
             | 
| 469 | 
            -
            async def check_if_hash_exists(file_hash: str) -> bool:
         | 
| 470 | 
            -
                # Đếm số document có hash tương ứng
         | 
| 471 | 
            -
                count = await mongo_db.processed_documents.count_documents({"file_hash": file_hash})
         | 
| 472 | 
            -
                return count > 0
         | 
| 473 | 
            -
             | 
| 474 | 
            -
            async def log_processed_hash(file_hash: str, filename: str):
         | 
| 475 | 
            -
                try:
         | 
| 476 | 
            -
                    document_record = {
         | 
| 477 | 
            -
                        "file_hash": file_hash,          # Hash của file
         | 
| 478 | 
            -
                        "original_filename": filename,   # Tên file gốc
         | 
| 479 | 
            -
                        "processed_at": datetime.now(timezone.utc), # Thời gian xử lý
         | 
| 480 | 
            -
                        "status": "SUCCESS",
         | 
| 481 | 
            -
                        # Thêm các thông tin khác nếu cần, ví dụ:
         | 
| 482 | 
            -
                        # "source_url": "https://url_cua_file_tren_s3_hoac_cloudinary",
         | 
| 483 | 
            -
                        # "user_uploader": user_email
         | 
| 484 | 
            -
                    }
         | 
| 485 | 
            -
                    await mongo_db.processed_documents.insert_one(document_record)
         | 
| 486 | 
            -
                except IOError as e:
         | 
| 487 | 
            -
                    logger.error(f"Could not write to hash log file: {e}")
         | 
|  | |
| 7 | 
             
            from schemas.chat import  Message
         | 
| 8 | 
             
            from redis.asyncio import Redis
         | 
| 9 | 
             
            import bcrypt
         | 
| 10 | 
            +
            from datetime import datetime, timedelta
         | 
| 11 | 
             
            from jose import jwt
         | 
| 12 | 
             
            from config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
         | 
| 13 | 
             
            from typing import List, Dict,  Optional
         | 
|  | |
| 444 | 
             
            # api/utils.py
         | 
| 445 |  | 
| 446 | 
             
            import hashlib
         | 
|  | |
| 447 |  | 
| 448 | 
             
            logger = logging.getLogger(__name__)
         | 
| 449 |  | 
|  | |
| 464 | 
             
            #     except IOError as e:
         | 
| 465 | 
             
            #         logger.error(f"Could not read hash log file: {e}")
         | 
| 466 | 
             
            #         return False
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  |