import os import time import uuid import logging import threading import requests # Dùng để stream dung lượng file chính xác from contextlib import asynccontextmanager from typing import List, Optional from fastapi import FastAPI, HTTPException, Depends from fastapi.responses import HTMLResponse, JSONResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import uvicorn # ─── Cấu hình hệ thống ──────────────────────────────────────────────────────── logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) BEARER_TOKEN = os.environ.get("BEARER_TOKEN", "quan11082012") MODEL_DIR = "/app/models" MODEL_PATH = os.path.join(MODEL_DIR, "qwen2.5-coder-7b-instruct-q4_k_m.gguf") MODEL_NAME = "qwen2.5-coder-7b-instruct" N_CTX = int(os.environ.get("N_CTX", "2048")) # Cấu hình an toàn tránh tràn RAM CPU N_THREADS = int(os.environ.get("N_THREADS", "4")) # ─── Biến trạng thái toàn cục phục vụ Web UI ──────────────────────────────────── llm = None STATUS = "Chưa khởi động" DOWNLOAD_PERCENT = 0 DOWNLOADED_GB = "0.00" TOTAL_GB = "0.00" ERROR_MSG = "" def download_and_load_model(): """Hàm chạy ngầm: Tải model có hiển thị tiến trình % và nạp vào RAM""" global llm, STATUS, DOWNLOAD_PERCENT, DOWNLOADED_GB, TOTAL_GB, ERROR_MSG url = "https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF/resolve/main/qwen2.5-coder-7b-instruct-q4_k_m.gguf" try: os.makedirs(MODEL_DIR, exist_ok=True) # Kiểm tra file tồn tại và dung lượng hợp lệ (~4.4 GB trở lên) if not os.path.exists(MODEL_PATH) or os.path.getsize(MODEL_PATH) < 4000000000: STATUS = "Đang tiến hành tải xuống model từ Hugging Face Hub..." logger.info(STATUS) # Thực hiện request stream để lấy từng chunk dữ liệu response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: raise Exception(f"Không thể kết nối đến link tải model. HTTP Code: {response.status_code}") total_bytes = int(response.headers.get('content-length', 4683132032)) TOTAL_GB = f"{total_bytes / (1024**3):.2f}" downloaded_bytes = 0 # Đọc từng block 4MB để tăng tốc độ ghi file with open(MODEL_PATH, "wb") as f: for chunk in response.iter_content(chunk_size=4*1024*1024): if chunk: f.write(chunk) downloaded_bytes += len(chunk) # Tính toán phần trăm để đẩy lên giao diện web DOWNLOAD_PERCENT = min(int((downloaded_bytes / total_bytes) * 100), 100) DOWNLOADED_GB = f"{downloaded_bytes / (1024**3):.2f}" logger.info("✅ Tải xuống file GGUF hoàn tất.") else: logger.info("ℹ️ Tìm thấy model có sẵn tại local. Bỏ qua bước tải xuống.") DOWNLOAD_PERCENT = 100 size_bytes = os.path.getsize(MODEL_PATH) DOWNLOADED_GB = f"{size_bytes / (1024**3):.2f}" TOTAL_GB = DOWNLOADED_GB # Chuyển sang công đoạn nạp vào RAM STATUS = "Đang nạp cấu hình model vào bộ nhớ RAM (Mất khoảng 1 phút)..." logger.info(STATUS) from llama_cpp import Llama llm = Llama( model_path=MODEL_PATH, n_ctx=N_CTX, n_threads=N_THREADS, n_gpu_layers=0, verbose=False, chat_format="chatml" ) STATUS = "Ready" logger.info("✅ [HỆ THỐNG] Toàn bộ quy trình hoàn tất. Model đã SẴN SÀNG!") except Exception as e: STATUS = "Lỗi" ERROR_MSG = str(e) logger.error(f"❌ Quy trình khởi động thất bại: {e}") # ─── Lifespan (Quản lý chu kỳ chạy ứng dụng) ────────────────────────────────── @asynccontextmanager async def lifespan(app: FastAPI): # Kích hoạt Thread chạy biệt lập ngay khi vừa bật Docker để giải phóng cổng 7860 threading.Thread(target=download_and_load_model, daemon=True).start() yield logger.info("🛑 Đang đóng ứng dụng.") # ─── Khởi tạo Ứng dụng FastAPI ──────────────────────────────────────────────── app = FastAPI(title="Minecraft Bot LLM Dashboard", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) security = HTTPBearer() def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): if credentials.credentials != BEARER_TOKEN: raise HTTPException(status_code=401, detail="Sai Bearer Token.") return credentials.credentials # ─── API Schemas ────────────────────────────────────────────────────────────── class ChatMessage(BaseModel): role: str content: str class ChatCompletionRequest(BaseModel): model: Optional[str] = None messages: List[ChatMessage] max_tokens: Optional[int] = None temperature: Optional[float] = 0.5 # ─── Endpoints Xử lý Biến trạng thái Giao diện ───────────────────────────────── @app.get("/api/status") async def get_status(): """Endpoint trả về tiến trình thời gian thực cho giao diện Front-end""" return JSONResponse({ "status": STATUS, "percent": DOWNLOAD_PERCENT, "downloaded": DOWNLOADED_GB, "total": TOTAL_GB, "error": ERROR_MSG }) @app.post("/v1/chat/completions", dependencies=[Depends(verify_token)]) async def chat_completions(request: ChatCompletionRequest): if STATUS != "Ready" or llm is None: raise HTTPException(status_code=503, detail=f"Hệ thống chưa sẵn sàng. Trạng thái hiện tại: {STATUS}") try: messages = [{"role": m.role, "content": m.content} for m in request.messages] result = llm.create_chat_completion( messages=messages, max_tokens=request.max_tokens or 512, temperature=request.temperature ) choice = result["choices"][0] return { "id": f"chatcmpl-{uuid.uuid4().hex}", "object": "chat.completion", "created": int(time.time()), "model": MODEL_NAME, "choices": [{ "index": 0, "message": {"role": "assistant", "content": choice["message"]["content"]}, "finish_reason": "stop" }] } except Exception as e: raise HTTPException(status_code=500, detail=f"Lỗi xử lý ngôn ngữ: {str(e)}") # ─── Giao diện Web HTML tích hợp (index.html) ───────────────────────────────── @app.get("/", response_class=HTMLResponse) async def serve_index(): html_content = """
Theo dõi trạng thái tải hệ thống và thử nghiệm kết nối trực quan