diff --git a/.env b/.env index c8359121..bb113aa9 100644 --- a/.env +++ b/.env @@ -65,3 +65,5 @@ DOCUMENT_CHUNK_OVERLAP=200 # Embedding configurations EMBEDDING_MAX_TEXT_LENGTH=3072 + +DOCKER_BACKEND_DOCKERFILE=Dockerfile.backend.cuda diff --git a/.gitignore b/.gitignore index 137e9de1..9e6f32bf 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,8 @@ run/* .backend.pid .frontend.pid logs/train/ +llama_cpp_backup/llama.cpp.zip +scripts/check_cuda_status.ps1 +scripts/test_cuda_detection.bat +.env +.gpu_selected diff --git a/Dockerfile.backend b/Dockerfile.backend index 2129e28f..4e340bc3 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -19,6 +19,11 @@ RUN mkdir -p /app/dependencies /app/data/sqlite /app/data/chroma_db /app/logs /a COPY dependencies/graphrag-1.2.1.dev27.tar.gz /app/dependencies/ COPY dependencies/llama.cpp.zip /app/dependencies/ +# Copy GPU checker script (only used for status reporting, not rebuilding) +COPY docker/app/check_gpu_support.sh /app/ +COPY docker/app/check_torch_cuda.py /app/ +RUN chmod +x /app/check_gpu_support.sh + # Build llama.cpp RUN LLAMA_LOCAL_ZIP="dependencies/llama.cpp.zip" \ && echo "Using local llama.cpp archive..." \ @@ -33,7 +38,11 @@ RUN LLAMA_LOCAL_ZIP="dependencies/llama.cpp.zip" \ echo "Successfully built llama-server"; \ fi -# +# Mark as CPU-only build for runtime reference +RUN mkdir -p /app/data && \ + echo "{ \"gpu_optimized\": false, \"optimized_on\": \"$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\" }" > /app/data/gpu_optimized.json && \ + echo "Created CPU-only marker file" + # Copy project configuration - Files that occasionally change COPY pyproject.toml README.md /app/ diff --git a/Dockerfile.backend.cuda b/Dockerfile.backend.cuda new file mode 100644 index 00000000..02aee8fe --- /dev/null +++ b/Dockerfile.backend.cuda @@ -0,0 +1,111 @@ +FROM nvidia/cuda:12.8.1-devel-ubuntu24.04 + +# Set working directory +WORKDIR /app + +# Add build argument to conditionally skip llama.cpp build +ARG SKIP_LLAMA_BUILD=false + +# Install system dependencies with noninteractive mode to avoid prompts +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + build-essential cmake git curl wget lsof vim unzip sqlite3 \ + python3-pip python3-venv python3-full python3-poetry pipx \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/bin/python3 /usr/bin/python + +# Create a virtual environment to avoid PEP 668 restrictions +RUN python -m venv /app/venv +ENV PATH="/app/venv/bin:$PATH" +ENV VIRTUAL_ENV="/app/venv" + +# Use the virtual environment's pip to install packages +RUN pip install --upgrade pip \ + && pip install poetry \ + && poetry config virtualenvs.create false + +# Create directories +RUN mkdir -p /app/dependencies /app/data/sqlite /app/data/chroma_db /app/logs /app/run /app/resources + +# Copy dependency files - Files that rarely change +COPY dependencies/graphrag-1.2.1.dev27.tar.gz /app/dependencies/ +COPY dependencies/llama.cpp.zip /app/dependencies/ + +# Copy GPU checker script +COPY docker/app/check_gpu_support.sh /app/ +COPY docker/app/check_torch_cuda.py /app/ +RUN chmod +x /app/check_gpu_support.sh + +# Unpack llama.cpp and build with CUDA support (conditionally, based on SKIP_LLAMA_BUILD) +RUN if [ "$SKIP_LLAMA_BUILD" = "false" ]; then \ + echo "=====================================================================" && \ + echo "STARTING LLAMA.CPP BUILD WITH CUDA SUPPORT - THIS WILL TAKE SOME TIME" && \ + echo "=====================================================================" && \ + LLAMA_LOCAL_ZIP="dependencies/llama.cpp.zip" && \ + echo "Using local llama.cpp archive..." && \ + unzip -q "$LLAMA_LOCAL_ZIP" && \ + cd llama.cpp && \ + mkdir -p build && \ + cd build && \ + echo "Starting CMake configuration with CUDA support..." && \ + cmake -DGGML_CUDA=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DLLAMA_NATIVE=OFF \ + -DCMAKE_CUDA_FLAGS="-Wno-deprecated-gpu-targets" \ + .. && \ + echo "Starting build process (this will take several minutes)..." && \ + cmake --build . --config Release -j --verbose && \ + echo "Build completed successfully" && \ + chmod +x /app/llama.cpp/build/bin/llama-server /app/llama.cpp/build/bin/llama-cli && \ + echo "====================================================================" && \ + echo "CUDA BUILD COMPLETED SUCCESSFULLY! GPU ACCELERATION IS NOW AVAILABLE" && \ + echo "===================================================================="; \ + else \ + echo "=====================================================================" && \ + echo "SKIPPING LLAMA.CPP BUILD (SKIP_LLAMA_BUILD=$SKIP_LLAMA_BUILD)" && \ + echo "Using existing llama.cpp build from Docker volume" && \ + echo "=====================================================================" && \ + LLAMA_LOCAL_ZIP="dependencies/llama.cpp.zip" && \ + echo "Just unpacking llama.cpp archive (no build)..." && \ + unzip -q "$LLAMA_LOCAL_ZIP" && \ + cd llama.cpp && \ + mkdir -p build; \ + fi + +# Mark as GPU-optimized build for runtime reference +RUN mkdir -p /app/data && \ + echo "{ \"gpu_optimized\": true, \"optimized_on\": \"$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\" }" > /app/data/gpu_optimized.json && \ + echo "Created GPU-optimized marker file" + +# Copy project configuration - Files that occasionally change +COPY pyproject.toml README.md /app/ + +# Fix for potential package installation issues with Poetry +RUN pip install --upgrade setuptools wheel +RUN poetry install --no-interaction --no-root || poetry install --no-interaction --no-root --without dev +RUN pip install --force-reinstall dependencies/graphrag-1.2.1.dev27.tar.gz + +# Copy source code - Files that frequently change +COPY docker/ /app/docker/ +COPY lpm_kernel/ /app/lpm_kernel/ + +# Check module import +RUN python -c "import lpm_kernel; print('Module import check passed')" + +# Set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app \ + BASE_DIR=/app/data \ + LOCAL_LOG_DIR=/app/logs \ + RUN_DIR=/app/run \ + RESOURCES_DIR=/app/resources \ + APP_ROOT=/app \ + FLASK_APP=lpm_kernel.app \ + LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH + +# Expose ports +EXPOSE 8002 8080 + +# Set the startup command +CMD ["bash", "-c", "echo 'Checking SQLite database...' && if [ ! -s /app/data/sqlite/lpm.db ]; then echo 'SQLite database not found or empty, initializing...' && mkdir -p /app/data/sqlite && sqlite3 /app/data/sqlite/lpm.db '.read /app/docker/sqlite/init.sql' && echo 'SQLite database initialized successfully' && echo 'Tables created:' && sqlite3 /app/data/sqlite/lpm.db '.tables'; else echo 'SQLite database already exists, skipping initialization'; fi && echo 'Checking ChromaDB...' && if [ ! -d /app/data/chroma_db/documents ] || [ ! -d /app/data/chroma_db/document_chunks ]; then echo 'ChromaDB collections not found, initializing...' && python /app/docker/app/init_chroma.py && echo 'ChromaDB initialized successfully'; else echo 'ChromaDB already exists, skipping initialization'; fi && echo 'Starting application at ' $(date) >> /app/logs/backend.log && cd /app && python -m flask run --host=0.0.0.0 --port=${LOCAL_APP_PORT:-8002} >> /app/logs/backend.log 2>&1"] \ No newline at end of file diff --git a/Makefile b/Makefile index 3d1b8bc3..87d76183 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,13 @@ -.PHONY: install test format lint all setup start stop restart restart-backend restart-force help docker-build docker-up docker-down docker-build-backend docker-build-frontend docker-restart-backend docker-restart-frontend docker-restart-all +.PHONY: install test format lint all setup start stop restart restart-backend restart-force help docker-build docker-up docker-down docker-build-backend docker-build-frontend docker-restart-backend docker-restart-backend-fast docker-restart-backend-smart docker-restart-frontend docker-restart-all docker-check-cuda docker-use-gpu docker-use-cpu + +# Check for GPU flag file and set Docker Compose file accordingly +ifeq ($(wildcard .gpu_selected),) + # No GPU flag file found, use CPU configuration + DOCKER_COMPOSE_FILE := docker-compose.yml +else + # GPU flag file found, use GPU configuration + DOCKER_COMPOSE_FILE := docker-compose-gpu.yml +endif # Detect operating system and set environment ifeq ($(OS),Windows_NT) @@ -39,6 +48,9 @@ else COLOR_RED := \033[1;31m endif +# Default Docker Compose configuration (non-GPU) +DOCKER_COMPOSE_FILE := docker-compose.yml + # Show help message help: ifeq ($(WINDOWS),1) @@ -69,8 +81,12 @@ ifeq ($(WINDOWS),1) @echo make docker-build-backend - Build only backend Docker image @echo make docker-build-frontend - Build only frontend Docker image @echo make docker-restart-backend - Restart only backend container + @echo make docker-restart-backend-fast - Restart backend+cuda without rebuilding llama.cpp @echo make docker-restart-frontend - Restart only frontend container @echo make docker-restart-all - Restart all Docker containers + @echo make docker-check-cuda - Check CUDA support in containers + @echo make docker-use-gpu - Switch to GPU configuration + @echo make docker-use-cpu - Switch to CPU-only configuration @echo. @echo All Available Commands: @echo make help - Show this help message @@ -106,9 +122,13 @@ else @echo " make docker-down - Stop all Docker containers" @echo " make docker-build-backend - Build only backend Docker image" @echo " make docker-build-frontend - Build only frontend Docker image" - @echo " make docker-restart-backend - Restart only backend container" + @echo " make docker-restart-backend - Restart only backend container (with rebuild)" + @echo " make docker-restart-backend-fast - Restart backend+cuda without rebuilding llama.cpp" @echo " make docker-restart-frontend - Restart only frontend container" @echo " make docker-restart-all - Restart all Docker containers" + @echo " make docker-check-cuda - Check CUDA support in containers" + @echo " make docker-use-gpu - Switch to GPU configuration" + @echo " make docker-use-cpu - Switch to CPU-only configuration" @echo "" @echo "$(COLOR_BOLD)All Available Commands:$(COLOR_RESET)" @echo " make help - Show this help message" @@ -124,6 +144,27 @@ else fi endif +# Configuration switchers for Docker +docker-use-gpu: + @echo "Switching to GPU configuration..." +ifeq ($(WINDOWS),1) + @echo GPU mode enabled. Docker commands will use docker-compose-gpu.yml + @echo gpu > .gpu_selected +else + @echo "$(COLOR_GREEN)GPU mode enabled. Docker commands will use docker-compose-gpu.yml$(COLOR_RESET)" + @echo "gpu" > .gpu_selected +endif + +docker-use-cpu: + @echo "Switching to CPU-only configuration..." +ifeq ($(WINDOWS),1) + @echo CPU-only mode enabled. Docker commands will use docker-compose.yml + @rm -f .gpu_selected +else + @echo "$(COLOR_GREEN)CPU-only mode enabled. Docker commands will use docker-compose.yml$(COLOR_RESET)" + @rm -f .gpu_selected +endif + setup: ./scripts/setup.sh @@ -156,37 +197,99 @@ DOCKER_COMPOSE_CMD := $(shell if command -v docker-compose >/dev/null 2>&1; then endif docker-build: - $(DOCKER_COMPOSE_CMD) build +ifeq ($(WINDOWS),1) + @echo "Prompting for CUDA preference..." + @scripts\prompt_cuda.bat +else + @echo "Prompting for CUDA preference..." + @chmod +x ./scripts/prompt_cuda.sh + @./scripts/prompt_cuda.sh +endif + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) build docker-up: - $(DOCKER_COMPOSE_CMD) up -d + @echo "Building and starting Docker containers..." +ifeq ($(WINDOWS),1) + @echo "Prompting for CUDA preference..." + @scripts\prompt_cuda.bat + @echo "Checking CUDA preference..." + @cmd /c "if exist .gpu_selected ( echo CUDA support detected, using GPU configuration... & docker compose -f docker-compose-gpu.yml build & docker compose -f docker-compose-gpu.yml up -d ) else ( echo No CUDA support selected, using CPU-only configuration... & docker compose -f docker-compose.yml build & docker compose -f docker-compose.yml up -d )" +else + @echo "Prompting for CUDA preference..." + @chmod +x ./scripts/prompt_cuda.sh + @./scripts/prompt_cuda.sh + @echo "Checking CUDA preference..." + @if [ -f .gpu_selected ]; then \ + echo "CUDA support detected, using GPU configuration..."; \ + $(DOCKER_COMPOSE_CMD) -f docker-compose-gpu.yml build; \ + $(DOCKER_COMPOSE_CMD) -f docker-compose-gpu.yml up -d; \ + else \ + echo "No CUDA support selected, using CPU-only configuration..."; \ + $(DOCKER_COMPOSE_CMD) -f docker-compose.yml build; \ + $(DOCKER_COMPOSE_CMD) -f docker-compose.yml up -d; \ + fi +endif + @echo "Container startup complete" + @echo "Check CUDA support with: make docker-check-cuda" docker-down: - $(DOCKER_COMPOSE_CMD) down + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) down docker-build-backend: - $(DOCKER_COMPOSE_CMD) build backend + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) build backend docker-build-frontend: - $(DOCKER_COMPOSE_CMD) build frontend + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) build frontend +# Standard backend restart with complete rebuild docker-restart-backend: - $(DOCKER_COMPOSE_CMD) stop backend - $(DOCKER_COMPOSE_CMD) rm -f backend - $(DOCKER_COMPOSE_CMD) build backend || { echo "$(COLOR_RED)❌ Backend build failed! Aborting operation...$(COLOR_RESET)"; exit 1; } - $(DOCKER_COMPOSE_CMD) up -d backend + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) stop backend + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) rm -f backend + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) build backend || { echo "$(COLOR_RED)❌ Backend build failed! Aborting operation...$(COLOR_RESET)"; exit 1; } + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) up -d backend + + +# Fast backend restart: preserves llama.cpp build +docker-restart-backend-fast: + @echo "Smart restarting backend container (preserving llama.cpp build)..." + @echo "Stopping backend container..." + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) stop backend + @echo "Removing backend container..." + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) rm -f backend + @echo "Building backend image with build-arg to skip llama.cpp build..." +ifeq ($(wildcard .gpu_selected),) + @echo "Using CPU configuration (docker-compose.yml)..." +else + @echo "Using GPU configuration (docker-compose-gpu.yml)..." +endif + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) build --build-arg SKIP_LLAMA_BUILD=true backend || { echo "$(COLOR_RED)❌ Backend build failed! Aborting operation...$(COLOR_RESET)"; exit 1; } + @echo "Starting backend container..." + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) up -d backend + @echo "Backend container smart-restarted successfully" + @echo "Check CUDA support with: make docker-check-cuda" docker-restart-frontend: - $(DOCKER_COMPOSE_CMD) stop frontend - $(DOCKER_COMPOSE_CMD) rm -f frontend - $(DOCKER_COMPOSE_CMD) build frontend || { echo "$(COLOR_RED)❌ Frontend build failed! Aborting operation...$(COLOR_RESET)"; exit 1; } - $(DOCKER_COMPOSE_CMD) up -d frontend + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) stop frontend + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) rm -f frontend + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) build frontend || { echo "$(COLOR_RED)❌ Frontend build failed! Aborting operation...$(COLOR_RESET)"; exit 1; } + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) up -d frontend docker-restart-all: - $(DOCKER_COMPOSE_CMD) stop - $(DOCKER_COMPOSE_CMD) rm -f - $(DOCKER_COMPOSE_CMD) build || { echo "$(COLOR_RED)❌ Build failed! Aborting operation...$(COLOR_RESET)"; exit 1; } - $(DOCKER_COMPOSE_CMD) up -d + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) stop + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) rm -f + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) build || { echo "$(COLOR_RED)❌ Build failed! Aborting operation...$(COLOR_RESET)"; exit 1; } + $(DOCKER_COMPOSE_CMD) -f $(DOCKER_COMPOSE_FILE) up -d + +# New command to check CUDA support in containers +docker-check-cuda: + @echo "Checking CUDA support in Docker containers..." +ifeq ($(WINDOWS),1) + @echo Running CUDA support check in backend container + @docker exec second-me-backend /app/check_gpu_support.sh || echo No GPU support detected in backend container +else + @echo "$(COLOR_CYAN)Running CUDA support check in backend container:$(COLOR_RESET)" + @docker exec second-me-backend /app/check_gpu_support.sh || echo "$(COLOR_RED)No GPU support detected in backend container$(COLOR_RESET)" +endif install: poetry install diff --git a/README.md b/README.md index e3e264ca..4d84da6f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![Discord](https://img.shields.io/badge/Chat-Discord-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/GpWHQNUwrg) [![Twitter](https://img.shields.io/badge/Follow-@SecondMe_AI-1DA1F2?style=flat-square&logo=x&logoColor=white)](https://x.com/SecondMe_AI1) [![Reddit](https://img.shields.io/badge/Join-Reddit-FF4500?style=flat-square&logo=reddit&logoColor=white)](https://www.reddit.com/r/SecondMeAI/) +[![View FAQ](https://img.shields.io/badge/FAQ-GitBook-blue?style=flat-square)](https://secondme.gitbook.io/secondme/faq) @@ -61,6 +62,14 @@ Star and join us, and you will receive all release notifications from GitHub wit

+## FAQ + +Got questions about running Second Me, model setup, or memory configuration? + +📖 [Check out the FAQ here](https://secondme.gitbook.io/secondme/faq) + + + ## Quick Start ### 📊 Model Deployment Memory and Supported Model Size Reference Guide @@ -124,8 +133,8 @@ make help > **Note**: Integrated Setup provides best performance, especially for larger models, as it runs directly on your host system without containerization overhead. #### Prerequisites -- Python 3.10+ installed on your system -- Node.js 18+ and npm installed +- Python 3.12+ installed on your system (using uv) +- Node.js 23+ and npm installed - Basic build tools (cmake, make, etc.) #### Setup Steps @@ -136,17 +145,32 @@ git clone git@github.com:Mindverse/Second-Me.git cd Second-Me ``` -2. Run the integrated setup (installs all dependencies and prepares the environment) +2. Setup Python Environment Using uv + +```bash +# Install uv +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Create virtual environment with Python 3.12 +uv venv --python 3.12 + +# Activate the virtual environment +source .venv/bin/activate # Unix/macOS +# or +# .venv\Scripts\activate # Windows +``` + +3. Install dependencies ```bash make setup ``` -3. Start all services +4. Start all services ```bash make restart ``` -4. After services are started, open your browser and visit: +5. After services are started, open your browser and visit: ```bash http://localhost:3000 ``` diff --git a/docker-compose-gpu.yml b/docker-compose-gpu.yml new file mode 100644 index 00000000..4d3c3459 --- /dev/null +++ b/docker-compose-gpu.yml @@ -0,0 +1,74 @@ +services: + backend: + build: + context: . + dockerfile: ${DOCKER_BACKEND_DOCKERFILE:-Dockerfile.backend.cuda} + container_name: second-me-backend + restart: unless-stopped + ports: + - "8002:8002" + - "8080:8080" + volumes: + - ./data:/app/data + - ./logs:/app/logs + - ./run:/app/run + - ./resources:/app/resources + - ./docker:/app/docker + - ./.env:/app/.env + - llama-cpp-build:/app/llama.cpp/build # Persist the llama.cpp build + environment: + # Environment variables + - LOCAL_APP_PORT=8002 + - IN_DOCKER_ENV=1 + - PLATFORM=${PLATFORM:-linux} + - USE_CUDA=1 + extra_hosts: + - "host.docker.internal:host-gateway" + deploy: + resources: + limits: + # Set container memory limit to 64GB + memory: 64G + reservations: + # Memory reservation + memory: 6G + devices: + - driver: nvidia + count: all + capabilities: [gpu] + networks: + - second-me-network + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + container_name: second-me-frontend + restart: unless-stopped + ports: + - "3000:3000" + volumes: + - ./logs:/app/logs + - ./resources:/app/resources + environment: + - VITE_API_BASE_URL=http://backend:8002 + depends_on: + - backend + deploy: + resources: + limits: + # Set container memory limit to 2GB + memory: 2G + reservations: + # Memory reservation + memory: 1G + networks: + - second-me-network + +networks: + second-me-network: + driver: bridge + +volumes: + llama-cpp-build: + driver: local \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4d19bd1e..152cb9b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - ./resources:/app/resources - ./docker:/app/docker - ./.env:/app/.env + - llama-cpp-build:/app/llama.cpp/build # Persist the llama.cpp build environment: # Environment variables - LOCAL_APP_PORT=8002 @@ -62,3 +63,7 @@ services: networks: second-me-network: driver: bridge + +volumes: + llama-cpp-build: + driver: local diff --git a/docker/app/check_gpu_support.sh b/docker/app/check_gpu_support.sh new file mode 100644 index 00000000..6c3cce97 --- /dev/null +++ b/docker/app/check_gpu_support.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Helper script to check if GPU support is available at runtime + +echo "=== GPU Support Check ===" + +# Check if llama-server binary exists and is linked to CUDA libraries +if [ -f "/app/llama.cpp/build/bin/llama-server" ]; then + echo "llama-server binary found, checking for CUDA linkage..." + CUDA_LIBS=$(ldd /app/llama.cpp/build/bin/llama-server | grep -i "cuda\|nvidia") + + if [ -n "$CUDA_LIBS" ]; then + echo "✅ llama-server is built with CUDA support:" + echo "$CUDA_LIBS" + echo "GPU acceleration is available" + + # Check for GPU optimization marker file (optional, not required) + GPU_MARKER_FILE="/app/data/gpu_optimized.json" + if [ -f "$GPU_MARKER_FILE" ]; then + GPU_OPTIMIZED=$(grep -o '"gpu_optimized": *true' "$GPU_MARKER_FILE" || echo "false") + OPTIMIZED_DATE=$(grep -o '"optimized_on": *"[^"]*"' "$GPU_MARKER_FILE" | cut -d'"' -f4) + + if [[ "$GPU_OPTIMIZED" == *"true"* ]]; then + echo "📝 GPU-optimized build marker found (built on: $OPTIMIZED_DATE)" + else + echo "📝 GPU marker file found but not marked as optimized (built on: $OPTIMIZED_DATE)" + fi + else + echo "📝 No GPU optimization marker file found, but CUDA support is detected in binary" + fi + + # Check if NVIDIA GPU is accessible at runtime + if nvidia-smi &>/dev/null; then + echo "🔍 NVIDIA GPU is available at runtime" + echo "=== GPU ACCELERATION IS READY TO USE ===" + exit 0 + else + echo "⚠️ WARNING: llama-server has CUDA support, but NVIDIA GPU is not accessible" + echo "Check that Docker is running with GPU access (--gpus all)" + exit 1 + fi + else + echo "❌ llama-server is not linked with CUDA libraries" + echo "Container was built without CUDA support" + fi +else + echo "❌ llama-server binary not found at /app/llama.cpp/build/bin/llama-server" +fi + +# Final check for GPU hardware +if nvidia-smi &>/dev/null; then + echo "🔍 NVIDIA GPU is available at runtime, but llama-server doesn't support CUDA" + echo "To enable GPU support, rebuild using: make docker-up (and select CUDA support when prompted)" + exit 1 +else + echo "❌ No NVIDIA GPU detected at runtime" + exit 1 +fi \ No newline at end of file diff --git a/docker/app/check_torch_cuda.py b/docker/app/check_torch_cuda.py new file mode 100644 index 00000000..82bf604b --- /dev/null +++ b/docker/app/check_torch_cuda.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import torch +import subprocess +import sys +import os + +print("=== PyTorch CUDA Version Information ===") +print(f"PyTorch version: {torch.__version__}") + +if torch.cuda.is_available(): + print(f"CUDA available: Yes") + print(f"CUDA version used by PyTorch: {torch.version.cuda}") + print(f"cuDNN version: {torch.backends.cudnn.version() if torch.backends.cudnn.is_available() else 'Not available'}") + print(f"GPU device name: {torch.cuda.get_device_name(0)}") + + # Try to check system CUDA version + try: + nvcc_output = subprocess.check_output(["nvcc", "--version"]).decode("utf-8") + print("\nSystem NVCC version:") + print(nvcc_output) + except: + print("\nNVCC not found in PATH") + + # Check CUDA libraries + try: + print("\nChecking required CUDA libraries:") + for lib in ["libcudart.so", "libcublas.so", "libcublasLt.so"]: + print(f"\nSearching for {lib}:") + find_result = subprocess.run(f"find /usr -name '{lib}*'", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if find_result.returncode == 0 and find_result.stdout: + print(find_result.stdout.decode("utf-8")) + else: + print(f"No {lib} found in /usr") + except Exception as e: + print(f"Error checking libraries: {e}") + + # Check LD_LIBRARY_PATH + print("\nLD_LIBRARY_PATH:") + print(os.environ.get("LD_LIBRARY_PATH", "Not set")) + +else: + print("CUDA not available") + +# Check system CUDA installation +print("\n=== System CUDA Information ===") +try: + nvidia_smi = subprocess.check_output(["nvidia-smi"]).decode("utf-8") + print("NVIDIA-SMI output:") + print(nvidia_smi) +except: + print("nvidia-smi not found or not working") \ No newline at end of file diff --git a/docker/app/rebuild_llama_cuda.sh b/docker/app/rebuild_llama_cuda.sh new file mode 100644 index 00000000..1f69c82e --- /dev/null +++ b/docker/app/rebuild_llama_cuda.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# Script to rebuild llama.cpp with CUDA support at runtime +# This ensures the build happens with full knowledge of the GPU environment + +set -e # Exit on error but don't print each command (for cleaner logs) +cd /app + +echo "========== STARTING LLAMA.CPP CUDA REBUILD PROCESS ==========" +echo "Current directory: $(pwd)" + +# First check if CUDA is actually available in the container +echo "Verifying NVIDIA drivers and CUDA availability..." +if ! command -v nvidia-smi &> /dev/null; then + echo "WARNING: NVIDIA drivers not found. Cannot build with CUDA support!" + echo "Make sure the container has access to the GPU and NVIDIA Container Toolkit is installed." + echo "Consider running Docker with: --gpus all" + exit 0 # Exit without error as there's no point trying to build with CUDA when no GPU is detected +fi + +# Run nvidia-smi to check GPU access +echo "Detected NVIDIA GPU:" +nvidia-smi || { + echo "ERROR: nvidia-smi command failed. GPU is not properly accessible from the container." + echo "Make sure you're running Docker with GPU access enabled (--gpus all)" + exit 0 # Exit without error since there's no GPU access +} + +# Install build dependencies +echo "Installing build dependencies..." +apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + wget \ + cmake \ + git \ + ca-certificates \ + gnupg \ + libopenblas-dev + +# Clean up apt cache to free space +apt-get clean +rm -rf /var/lib/apt/lists/* + +# Install CUDA using NVIDIA's official Debian 12 network installation method +echo "Installing CUDA using NVIDIA's official method for Debian 12..." +wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb +dpkg -i cuda-keyring_1.1-1_all.deb +rm cuda-keyring_1.1-1_all.deb +apt-get update + +# Install CUDA packages needed for building llama.cpp with CUDA support +apt-get install -y --fix-missing --no-install-recommends cuda-compiler-12-8 +apt-get clean +rm -rf /var/lib/apt/lists/* + +apt-get update +apt-get install -y --fix-missing --no-install-recommends cuda-runtime-12-8 +apt-get clean +rm -rf /var/lib/apt/lists/* + +apt-get update +apt-get install -y --fix-missing --no-install-recommends cuda-libraries-dev-12-8 +apt-get clean +rm -rf /var/lib/apt/lists/* + +# Set up environment for build +export PATH=/usr/local/cuda-12.8/bin:${PATH} +export LD_LIBRARY_PATH=/usr/local/cuda-12.8/lib64:${LD_LIBRARY_PATH} +export CUDA_HOME=/usr/local/cuda-12.8 +# Set CUDACXX environment variable explicitly to help CMake find the CUDA compiler +export CUDACXX=/usr/local/cuda-12.8/bin/nvcc +export CMAKE_CUDA_COMPILER=/usr/local/cuda-12.8/bin/nvcc + +# Verify CUDA compiler is available +echo "Verifying CUDA compiler (nvcc) is available:" +which nvcc || echo "ERROR: nvcc not found in PATH!" +nvcc --version || echo "ERROR: nvcc not working properly!" + +echo "CUDA environment:" +echo "- CUDA_HOME: $CUDA_HOME" +echo "- CUDACXX: $CUDACXX" +echo "- CMAKE_CUDA_COMPILER: $CMAKE_CUDA_COMPILER" +echo "- PATH includes CUDA: $PATH" +echo "- LD_LIBRARY_PATH: $LD_LIBRARY_PATH" + +# Show available disk space +echo "Available disk space:" +df -h + +# Use local build approach to avoid volume mount issues +echo "Building llama.cpp with CUDA in a local directory..." +cd /tmp +rm -rf llama_build +mkdir -p llama_build +cd llama_build + +# Clone a fresh copy of llama.cpp - this avoids volume mount issues +echo "Cloning fresh copy of llama.cpp..." +git clone https://github.com/ggerganov/llama.cpp.git . + +# Configure and build with CUDA support +mkdir -p build +cd build +echo "Configuring with CMake..." +cmake -DGGML_CUDA=ON \ + -DCMAKE_CUDA_ARCHITECTURES=all \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DLLAMA_NATIVE=OFF \ + -DCMAKE_CUDA_FLAGS="-Wno-deprecated-gpu-targets" \ + .. + +echo "Building llama.cpp with CUDA support..." +cmake --build . --config Release --target all -j $(nproc) + +if [ -f "bin/llama-server" ]; then + echo "Build successful! Copying binaries to /app/llama.cpp/build/bin/" + mkdir -p /app/llama.cpp/build/bin + cp bin/llama-server /app/llama.cpp/build/bin/ + cp bin/llama-cli /app/llama.cpp/build/bin/ 2>/dev/null || true + chmod +x /app/llama.cpp/build/bin/llama-server /app/llama.cpp/build/bin/llama-cli + + # Create GPU optimized marker + echo "{ \"gpu_optimized\": true, \"optimized_on\": \"$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\" }" > /app/data/gpu_optimized.json + + echo "Testing CUDA support in built binary..." + LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH /app/llama.cpp/build/bin/llama-server --version + echo "" + echo "========== CUDA BUILD COMPLETED SUCCESSFULLY ==========" +else + echo "ERROR: Build failed - llama-server executable not found!" + exit 1 +fi \ No newline at end of file diff --git a/docker/sqlite/init.sql b/docker/sqlite/init.sql index d0639c25..cd3a5233 100644 --- a/docker/sqlite/init.sql +++ b/docker/sqlite/init.sql @@ -188,6 +188,11 @@ CREATE TABLE IF NOT EXISTS user_llm_configs ( embedding_api_key VARCHAR(200), embedding_model_name VARCHAR(200), + -- Thinking configuration + thinking_model_name VARCHAR(200), + thinking_endpoint VARCHAR(200), + thinking_api_key VARCHAR(200), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/lpm_kernel/train/__init__.py b/logs/logs.lnk similarity index 100% rename from lpm_kernel/train/__init__.py rename to logs/logs.lnk diff --git a/lpm_frontend/.eslintrc.js b/lpm_frontend/.eslintrc.js index 4583f90f..40e72cc9 100644 --- a/lpm_frontend/.eslintrc.js +++ b/lpm_frontend/.eslintrc.js @@ -17,5 +17,15 @@ module.exports = { ], rules: { '@next/next/no-sync-scripts': 'off' + }, + settings: { + 'import/resolver': { + typescript: { + project: 'Second-Me/lpm_frontend/tsconfig.json' + }, + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'] + } + } } }; diff --git a/lpm_frontend/src/app/dashboard/playground/chat/page.tsx b/lpm_frontend/src/app/dashboard/playground/chat/page.tsx index 815025ef..921f9d53 100644 --- a/lpm_frontend/src/app/dashboard/playground/chat/page.tsx +++ b/lpm_frontend/src/app/dashboard/playground/chat/page.tsx @@ -13,10 +13,13 @@ import { import type { ChatRequest } from '@/hooks/useSSE'; import { useSSE } from '@/hooks/useSSE'; import { useLoadInfoStore } from '@/store/useLoadInfoStore'; +import { getTrainingParams } from '@/service/train'; // Use the Message type directly from storage type Message = StorageMessage; +type ModelType = 'chat' | 'thinking'; + interface PlaygroundSettings { enableL0Retrieval: boolean; enableL1Retrieval: boolean; @@ -36,17 +39,44 @@ const generateMessageId = () => { }; export default function PlaygroundChat() { + const loadInfo = useLoadInfoStore((state) => state.loadInfo); + + const { sendStreamMessage, streaming, streamContent, stopSSE } = useSSE(); + const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); const [messages, setMessages] = useState([]); - const { sendStreamMessage, streaming, streamContent, stopSSE } = useSSE(); + const [modelType, setModelType] = useState(undefined); - const loadInfo = useLoadInfoStore((state) => state.loadInfo); const originPrompt = useMemo(() => { const name = loadInfo?.name || 'user'; - return `You are ${name}'s "Second Me", which is a personalized AI created by ${name}. You can help ${name} answer questions based on your understanding of ${name}'s background information and past records.`; - }, [loadInfo]); + if (modelType === 'chat') { + return `You are ${name}'s "Second Me", which is a personalized AI created by ${name}. You can help ${name} answer questions based on your understanding of ${name}'s background information and past records.`; + } + + if (modelType === 'thinking') { + return `You are ${name}'s "Second Me", and you are currently in conversation with ${name}. + Your task is to help ${name} answer relevant questions based on your understanding of ${name}'s background information and past records. + Please ensure your answers meet ${name}'s needs and provide precise solutions based on their historical information and personal preferences. + + When thinking, please follow these steps and output the results clearly in order: + 1. Consider the connection between questions and background: Review ${name}'s past records and personal information, analyzing the connections between their questions and these records. + 2. Derive answers to questions: Based on ${name}'s historical data and specific question content, conduct reasoning and analysis to ensure accuracy and relevance of answers. + 3. Generate high-quality responses: Distill answers that best meet ${name}'s needs and present them systematically with high information density. + + Your output format must follow the following structure: + + + As "Second Me"'s thinking process, analyze the relationships between ${name}'s background information, historical records and the questions raised, deriving reasonable solution approaches. + + + This is the final answer for ${name}, ensuring the response is precise and meets their needs, while being systematic and information-dense. + `; + } + + return ''; + }, [loadInfo, modelType]); const originSettings = useMemo(() => { return { enableL0Retrieval: true, @@ -72,6 +102,23 @@ export default function PlaygroundChat() { }); }, [originPrompt]); + useEffect(() => { + getTrainingParams() + .then((res) => { + if (res.data.code === 0) { + const data = res.data.data; + + localStorage.setItem('trainingParams', JSON.stringify(data)); + setModelType(data.is_cot ? 'thinking' : 'chat'); + } else { + throw new Error(res.data.message); + } + }) + .catch((error) => { + console.error(error.message); + }); + }, []); + const scrollToBottom = () => { if (messagesEndRef.current) { const container = messagesEndRef.current.parentElement; @@ -173,7 +220,7 @@ export default function PlaygroundChat() { }; // Update message list, adding user message and empty assistant message - let newMessages = [...messages, userMessage, assistantMessage]; + let newMessages = [...messages, userMessage]; const systemMessage: Message = { id: generateMessageId(), @@ -186,21 +233,22 @@ export default function PlaygroundChat() { }; if (!newMessages.find((item) => item.role === 'system')) { - newMessages = [systemMessage, ...newMessages] + newMessages = [systemMessage, ...newMessages]; } else { newMessages = newMessages.map((msg) => { if (msg.role === 'system') { return { ...msg, content: originPrompt }; } + return msg; }); } - setMessages(newMessages); + setMessages([...newMessages, assistantMessage]); // Save messages to session if (activeSessionId) { - chatStorage.saveSessionMessages(activeSessionId, newMessages); + chatStorage.saveSessionMessages(activeSessionId, [...newMessages, assistantMessage]); // If it's the first message in a new session, update session title if (messages.length === 0) { @@ -306,8 +354,8 @@ export default function PlaygroundChat() { index === messages.length - 1 && message.role === 'assistant' } - role={message.role} message={message.content} + role={message.role} timestamp={message.timestamp} /> ))} diff --git a/lpm_frontend/src/app/dashboard/train/memories/page.tsx b/lpm_frontend/src/app/dashboard/train/memories/page.tsx index 87b74d07..1d0c15c2 100644 --- a/lpm_frontend/src/app/dashboard/train/memories/page.tsx +++ b/lpm_frontend/src/app/dashboard/train/memories/page.tsx @@ -217,7 +217,6 @@ export default function TrainPage() { @@ -236,6 +235,7 @@ export default function TrainPage() { ) : null } onClose={() => setSelectedInfo(null)} + open={!!selectedInfo && !!trainSectionInfo[selectedInfo]} title={selectedInfo ? trainSectionInfo[selectedInfo].name : ''} /> diff --git a/lpm_frontend/src/app/dashboard/train/training/page.tsx b/lpm_frontend/src/app/dashboard/train/training/page.tsx index c201c94a..bd74182f 100644 --- a/lpm_frontend/src/app/dashboard/train/training/page.tsx +++ b/lpm_frontend/src/app/dashboard/train/training/page.tsx @@ -4,13 +4,19 @@ import { useState, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import InfoModal from '@/components/InfoModal'; import type { TrainingConfig } from '@/service/train'; -import { startTrain, stopTrain, retrain, getTrainingParams, resetProgress } from '@/service/train'; +import { + startTrain, + stopTrain, + retrain, + getTrainingParams, + checkCudaAvailability, + resetProgress +} from '@/service/train'; import { useTrainingStore } from '@/store/useTrainingStore'; import { getMemoryList } from '@/service/memory'; import { message, Modal } from 'antd'; import { useModelConfigStore } from '@/store/useModelConfigStore'; import CelebrationEffect from '@/components/Celebration'; -import { getModelConfig } from '@/service/modelConfig'; import TrainingLog from '@/components/train/TrainingLog'; import TrainingProgress from '@/components/train/TrainingProgress'; import TrainingConfiguration from '@/components/train/TrainingConfiguration'; @@ -60,59 +66,68 @@ const baseModelOptions = [ } ]; +// Title and explanation section +const pageTitle = 'Training Process'; +const pageDescription = + 'Transform your memories into a personalized AI model that thinks and communicates like you.'; + export default function TrainingPage() { - // Title and explanation section - const pageTitle = 'Training Process'; - const pageDescription = - 'Transform your memories into a personalized AI model that thinks and communicates like you.'; + const checkTrainStatus = useTrainingStore((state) => state.checkTrainStatus); + const resetTrainingState = useTrainingStore((state) => state.resetTrainingState); + const trainingError = useTrainingStore((state) => state.error); + const setStatus = useTrainingStore((state) => state.setStatus); + const fetchModelConfig = useModelConfigStore((state) => state.fetchModelConfig); + const modelConfig = useModelConfigStore((store) => store.modelConfig); + const status = useTrainingStore((state) => state.status); + const trainingProgress = useTrainingStore((state) => state.trainingProgress); + const serviceStarted = useTrainingStore((state) => state.serviceStarted); + + const router = useRouter(); const [selectedInfo, setSelectedInfo] = useState(false); - const [isTraining, setIsTraining] = useState(false); + const isTraining = useTrainingStore((state) => state.isTraining); + const setIsTraining = useTrainingStore((state) => state.setIsTraining); const [trainingParams, setTrainingParams] = useState({} as TrainingConfig); - const [nowTrainingParams, setNowTrainingParams] = useState(null); const [trainActionLoading, setTrainActionLoading] = useState(false); - - const containerRef = useRef(null); - const firstLoadRef = useRef(true); const [showCelebration, setShowCelebration] = useState(false); const [showMemoryModal, setShowMemoryModal] = useState(false); - const modelConfig = useModelConfigStore((store) => store.modelConfig); - const updateModelConfig = useModelConfigStore((store) => store.updateModelConfig); const cleanupEventSourceRef = useRef<(() => void) | undefined>(); + const containerRef = useRef(null); + const firstLoadRef = useRef(true); + const pollingStopRef = useRef(false); - const [changeBaseModel, setChangeBaseModel] = useState(false); + const [cudaAvailable, setCudaAvailable] = useState(false); + const trainSuspended = useTrainingStore((state) => state.trainSuspended); + const setTrainSuspended = useTrainingStore((state) => state.setTrainSuspended); useEffect(() => { - const localTrainingParams = JSON.parse(localStorage.getItem('trainingParams') || '{}'); - - setChangeBaseModel(localTrainingParams?.model_name !== trainingParams.model_name); - }, [trainingParams.model_name]); + fetchModelConfig(); + }, []); useEffect(() => { - getModelConfig().then((res) => { - if (res.data.code == 0) { - const data = res.data.data || {}; - - updateModelConfig(data); - } else { - message.error(res.data.message); - } - }); - }, []); + // Check CUDA availability once on load + checkCudaAvailability() + .then((res) => { + if (res.data.code === 0) { + const { cuda_available, cuda_info } = res.data.data; - const pollingStopRef = useRef(false); - const router = useRouter(); + setCudaAvailable(cuda_available); - const status = useTrainingStore((state) => state.status); - const trainingProgress = useTrainingStore((state) => state.trainingProgress); - const [isResume, setIsResume] = useState( - trainingProgress.status === 'suspended' || trainingProgress.status === 'failed' - ); - const checkTrainStatus = useTrainingStore((state) => state.checkTrainStatus); - const resetTrainingState = useTrainingStore((state) => state.resetTrainingState); - const trainingError = useTrainingStore((state) => state.error); - const setStatus = useTrainingStore((state) => state.setStatus); + if (cuda_available) { + console.log('CUDA is available:', cuda_info); + } else { + console.log('CUDA is not available on this system'); + } + } else { + message.error(res.data.message || 'Failed to check CUDA availability'); + } + }) + .catch((err) => { + console.error('CUDA availability check failed', err); + message.error('CUDA availability check failed'); + }); + }, []); // Start polling training progress const startPolling = () => { @@ -151,10 +166,6 @@ export default function TrainingPage() { pollingStopRef.current = true; }; - useEffect(() => { - setIsResume(trainingProgress.status === 'suspended' || trainingProgress.status === 'failed'); - }, [trainingProgress]); - useEffect(() => { if (status === 'trained' || trainingError) { stopPolling(); @@ -171,13 +182,6 @@ export default function TrainingPage() { } }, [status, trainingError]); - // Monitor training status changes, scroll to bottom when status becomes 'training' - useEffect(() => { - if (status === 'training') { - scrollToBottom(); - } - }, [status]); - // Check training status once when component loads useEffect(() => { // Check if user has at least 3 memories @@ -201,14 +205,6 @@ export default function TrainingPage() { // Only proceed with training status check if memory check passes checkTrainStatus(); - - // Check if we were in the middle of retraining - const isRetraining = localStorage.getItem('isRetraining') === 'true'; - - if (isRetraining) { - // If we were retraining, set status to training - startGetTrainingProgress(); - } }; checkMemoryCount(); @@ -225,7 +221,9 @@ export default function TrainingPage() { if (firstLoadRef.current) { scrollPageToBottom(); - scrollToBottom(); + + // On first load, start polling and get training progress. + startGetTrainingProgress(); } } // If training is completed or failed, stop polling @@ -266,7 +264,6 @@ export default function TrainingPage() { const data = res.data.data; setTrainingParams(data); - setNowTrainingParams(data); localStorage.setItem('trainingParams', JSON.stringify(data)); } else { @@ -294,11 +291,6 @@ export default function TrainingPage() { firstLoadRef.current = false; }; - const scrollToBottom = () => { - // This function is kept for backward compatibility - // The actual scrolling is now handled by the TrainingLog component - }; - const updateTrainingParams = (params: TrainingConfig) => { setTrainingParams((state: TrainingConfig) => ({ ...state, ...params })); }; @@ -308,45 +300,32 @@ export default function TrainingPage() { const eventSource = new EventSource('/api/trainprocess/logs'); eventSource.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - - setTrainingDetails((prev) => { - const newLogs = [ - ...prev.slice(-100), - { - message: data.message, - timestamp: new Date().toISOString() - } - ]; - - // Save logs to localStorage - // localStorage.setItem('trainingLogs', JSON.stringify(newLogs)); - - return newLogs; - }); - } catch { - setTrainingDetails((prev) => { - const newLogs = [ - ...prev.slice(-100), - { - message: event.data, - timestamp: new Date().toISOString() - } - ]; - - // Save logs to localStorage - // localStorage.setItem('trainingLogs', JSON.stringify(newLogs)); - - return newLogs; - }); - } + // Don't try to parse as JSON, just use the raw text data directly + const logMessage = event.data; + + setTrainingDetails((prev) => { + const newLogs = [ + ...prev.slice(-500), // Keep more log entries (500 instead of 100) + { + message: logMessage, + timestamp: new Date().toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + } + ]; + + // Save logs to localStorage for persistence between page refreshes + localStorage.setItem('trainingLogs', JSON.stringify(newLogs)); + + return newLogs; + }); }; eventSource.onerror = (error) => { console.error('EventSource failed:', error); eventSource.close(); - message.error('Failed to get training logs'); }; return () => { @@ -369,7 +348,7 @@ export default function TrainingPage() { if (res.data.code === 0) { setIsTraining(false); - setIsResume(true); + setTrainSuspended(true); } else { message.error(res.data.message || 'Failed to stop training'); } @@ -385,9 +364,7 @@ export default function TrainingPage() { resetProgress() .then((res) => { if (res.data.code === 0) { - setTrainingParams(nowTrainingParams || ({} as TrainingConfig)); - setNowTrainingParams(null); - setIsResume(false); + setTrainSuspended(false); resetTrainingState(); } else { throw new Error(res.data.message || 'Failed to reset progress'); @@ -411,19 +388,15 @@ export default function TrainingPage() { resetTrainingState(); try { - // updateTrainLog(); - setNowTrainingParams(trainingParams); - console.log('Using startTrain API to train new model:', trainingParams.model_name); const res = await startTrain({ - ...(isResume && !changeBaseModel ? {} : trainingParams), + ...trainingParams, model_name: trainingParams.model_name }); if (res.data.code === 0) { // Save training configuration and start polling localStorage.setItem('trainingParams', JSON.stringify(trainingParams)); - setChangeBaseModel(false); scrollPageToBottom(); startGetTrainingProgress(); } else { @@ -433,7 +406,6 @@ export default function TrainingPage() { } catch (error: unknown) { console.error('Error starting training:', error); setIsTraining(false); - setNowTrainingParams(null); if (error instanceof Error) { message.error(error.message || 'Failed to start training'); @@ -484,6 +456,12 @@ export default function TrainingPage() { return; } + if (!isTraining && serviceStarted) { + message.error('Model is already running, please stop it first'); + + return; + } + setTrainActionLoading(true); // If training is in progress, stop it @@ -495,7 +473,7 @@ export default function TrainingPage() { } // If the same model has already been trained and status is 'trained' or 'running', perform retraining - if (!changeBaseModel && (status === 'trained' || status === 'running')) { + if (status === 'trained') { await handleRetrainModel(); } else { // Otherwise start new training @@ -553,23 +531,21 @@ export default function TrainingPage() { {/* Training Configuration Component */} {/* Only show training progress after training starts */} - {(status === 'training' || status === 'trained' || status === 'running') && - renderTrainingProgress()} + {(status === 'training' || status === 'trained') && renderTrainingProgress()} {/* Always show training log regardless of training status */} {renderTrainingLog()} diff --git a/lpm_frontend/src/app/home/components/Footer/index.tsx b/lpm_frontend/src/app/home/components/Footer/index.tsx index 30e33d60..f58a3e84 100644 --- a/lpm_frontend/src/app/home/components/Footer/index.tsx +++ b/lpm_frontend/src/app/home/components/Footer/index.tsx @@ -24,7 +24,7 @@ const Footer = (props: IProps) => {
diff --git a/lpm_frontend/src/app/home/page.tsx b/lpm_frontend/src/app/home/page.tsx index de573415..8473f25a 100644 --- a/lpm_frontend/src/app/home/page.tsx +++ b/lpm_frontend/src/app/home/page.tsx @@ -4,11 +4,11 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import CreateSecondMe from '@/app/home/components/Create'; import dynamic from 'next/dynamic'; -import { getUploadCount } from '@/service/info'; +import type { ILoadInfo } from '@/service/info'; +import { getCurrentInfo, getUploadCount } from '@/service/info'; import { ROUTER_PATH } from '@/utils/router'; import Footer from './components/Footer'; import SocialMedia from './components/SocialMedia'; -import { useLoadInfoStore } from '@/store/useLoadInfoStore'; import { message } from 'antd'; const NetworkSphere = dynamic(() => import('@/components/NetworkSphere'), { @@ -23,8 +23,21 @@ export default function Home() { const [isMounted, setIsMounted] = useState(false); const [contentVisible, setContentVisible] = useState(false); - const loadInfo = useLoadInfoStore((state) => state.loadInfo); - const firstLoaded = useLoadInfoStore((state) => state.firstLoaded); + const [loading, setLoading] = useState(true); + const [loadInfo, setLoadInfo] = useState(null); + + useEffect(() => { + getCurrentInfo() + .then((res) => { + if (res.data.code === 0) { + setLoadInfo(res.data.data); + localStorage.setItem('upload', JSON.stringify(res.data.data)); + } + }) + .finally(() => { + setLoading(false); + }); + }, []); useEffect(() => { setIsMounted(true); @@ -115,7 +128,7 @@ export default function Home() {
- {firstLoaded && ( + {!loading && (
diff --git a/lpm_frontend/src/app/standalone/role/[roleId]/page.tsx b/lpm_frontend/src/app/standalone/role/[roleId]/page.tsx index f6c47447..9a2d1f71 100644 --- a/lpm_frontend/src/app/standalone/role/[roleId]/page.tsx +++ b/lpm_frontend/src/app/standalone/role/[roleId]/page.tsx @@ -57,7 +57,7 @@ export default function RoleChat() { const storedMessages = roleplayChatStorage.getMessages(role_id); const systemMessage: IChatMessage = { id: generateMessageId(), - content: role?.system_prompt ||'', + content: role?.system_prompt || '', role: 'system', timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', @@ -123,11 +123,11 @@ export default function RoleChat() { }; // Update message list, add user message and empty assistant message - let newMessages = [...messages, userMessage, assistantMessage]; + let newMessages = [...messages, userMessage]; const systemMessage: IChatMessage = { id: generateMessageId(), - content: role?.system_prompt ||'', + content: role.system_prompt || '', role: 'system', timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', @@ -136,20 +136,20 @@ export default function RoleChat() { }; if (!newMessages.find((item) => item.role === 'system')) { - newMessages = [systemMessage, ...newMessages] + newMessages = [systemMessage, ...newMessages]; } else { newMessages = newMessages.map((msg) => { if (msg.role === 'system') { - return { ...msg, content: role?.system_prompt ||'' }; + return { ...msg, content: role.system_prompt || '' }; } + return msg; }); } - - setMessages(newMessages); + setMessages([...newMessages, assistantMessage]); // Save messages - roleplayChatStorage.saveMessages(role_id, newMessages); + roleplayChatStorage.saveMessages(role_id, [...newMessages, assistantMessage]); // Send request const chatRequest: ChatRequest = { @@ -263,8 +263,8 @@ export default function RoleChat() { index === messages.length - 1 && msg.role === 'assistant' } - role={msg.role} message={msg.content} + role={msg.role} timestamp={msg.timestamp} /> ))} diff --git a/lpm_frontend/src/components/ModelStatus/index.tsx b/lpm_frontend/src/components/ModelStatus/index.tsx index 3804e105..9a96d54a 100644 --- a/lpm_frontend/src/components/ModelStatus/index.tsx +++ b/lpm_frontend/src/components/ModelStatus/index.tsx @@ -1,5 +1,5 @@ -import { useTrainingStore } from '@/store/useTrainingStore'; -import { startService, stopService, getServiceStatus } from '@/service/train'; +import { Status, statusRankMap, useTrainingStore } from '@/store/useTrainingStore'; +import { startService, stopService } from '@/service/train'; import { StatusBar } from '../StatusBar'; import { useRef, useEffect, useState, useMemo } from 'react'; import { message } from 'antd'; @@ -13,6 +13,8 @@ import { import RegisterUploadModal from '../upload/RegisterUploadModal'; import { useLoadInfoStore } from '@/store/useLoadInfoStore'; +import TrainingTipModal from '../upload/TraingTipModal'; +import { getMemoryList } from '@/service/memory'; const StatusDot = ({ active }: { active: boolean }) => (
( export function ModelStatus() { const status = useTrainingStore((state) => state.status); const setStatus = useTrainingStore((state) => state.setStatus); + const serviceStarted = useTrainingStore((state) => state.serviceStarted); const isServiceStarting = useTrainingStore((state) => state.isServiceStarting); const isServiceStopping = useTrainingStore((state) => state.isServiceStopping); const setServiceStarting = useTrainingStore((state) => state.setServiceStarting); const setServiceStopping = useTrainingStore((state) => state.setServiceStopping); + const fetchServiceStatus = useTrainingStore((state) => state.fetchServiceStatus); + const isTraining = useTrainingStore((state) => state.isTraining); const [messageApi, contextHolder] = message.useMessage(); @@ -36,43 +41,37 @@ export function ModelStatus() { }, [loadInfo]); const [showRegisterModal, setShowRegisterModal] = useState(false); + const [showtrainingModal, setShowtrainingModal] = useState(false); const handleRegistryClick = () => { - if (status !== 'trained' && status !== 'running') { - messageApi.info({ - content: 'Please train your model first', - duration: 1 - }); - } else if (status === 'trained') { + if (!serviceStarted) { messageApi.info({ content: 'Please start your model service first', duration: 1 }); - } else if (status === 'running') { + } else { setShowRegisterModal(true); } }; - const fetchServiceStatus = async () => { + const fetchMemories = async () => { try { - const statusRes = await getServiceStatus(); + const memoryRes = await getMemoryList(); - if (statusRes.data.code === 0) { - const isRunning = statusRes.data.data.is_running; + if (memoryRes.data.code === 0) { + const memories = memoryRes.data.data; - if (isRunning) { - setStatus('running'); - setServiceStarting(false); - } else if (status === 'running') { - setStatus('trained'); + if (memories.length > 0 && statusRankMap[status] < statusRankMap[Status.MEMORY_UPLOAD]) { + setStatus(Status.MEMORY_UPLOAD); } } } catch (error) { - console.error('Error checking initial service status:', error); + console.error('Error fetching memories:', error); } }; useEffect(() => { + fetchMemories(); fetchServiceStatus(); return () => { @@ -94,13 +93,12 @@ export function ModelStatus() { // Start new polling interval pollingInterval.current = setInterval(() => { - getServiceStatus() - .then((statusRes) => { - if (statusRes.data.code === 0) { - const isRunning = statusRes.data.data.is_running; + fetchServiceStatus() + .then((res) => { + if (res.data.code === 0) { + const isRunning = res.data.data.is_running; if (isRunning) { - setStatus('running'); setServiceStarting(false); clearPolling(); } @@ -117,13 +115,12 @@ export function ModelStatus() { // Start new polling interval pollingInterval.current = setInterval(() => { - getServiceStatus() - .then((statusRes) => { - if (statusRes.data.code === 0) { - const isRunning = statusRes.data.data.is_running; + fetchServiceStatus() + .then((res) => { + if (res.data.code === 0) { + const isRunning = res.data.data.is_running; if (!isRunning) { - setStatus('trained'); setServiceStopping(false); clearPolling(); } @@ -135,49 +132,69 @@ export function ModelStatus() { }, 3000); }; - const handleServiceAction = () => { - const config = JSON.parse(localStorage.getItem('trainingConfig') || '{}'); + const handleStartService = () => { + const config = JSON.parse(localStorage.getItem('trainingParams') || '{}'); - if (status === 'running') { - setServiceStopping(true); - stopService() - .then((res) => { - if (res.data.code === 0) { - messageApi.success({ content: 'Service stopping...', duration: 1 }); - startStopPolling(); - } else { - messageApi.error({ content: res.data.message!, duration: 1 }); - setServiceStopping(false); - } - }) - .catch((error) => { - console.error('Error stopping service:', error); - messageApi.error({ - content: error.response?.data?.message || error.message, - duration: 1 - }); + if (!config.model_name) { + message.error('Please train a base model first'); + + return; + } + + setServiceStarting(true); + startService({ model_name: config.model_name }) + .then((res) => { + if (res.data.code === 0) { + messageApi.success({ content: 'Service starting...', duration: 1 }); + startPolling(); + } else { + setServiceStarting(false); + messageApi.error({ content: res.data.message!, duration: 1 }); + } + }) + .catch((error) => { + console.error('Error starting service:', error); + setServiceStarting(false); + messageApi.error({ + content: error.response?.data?.message || error.message, + duration: 1 + }); + }); + }; + + const handleStopService = () => { + setServiceStopping(true); + stopService() + .then((res) => { + if (res.data.code === 0) { + messageApi.success({ content: 'Service stopping...', duration: 1 }); + startStopPolling(); + } else { + messageApi.error({ content: res.data.message!, duration: 1 }); setServiceStopping(false); + } + }) + .catch((error) => { + console.error('Error stopping service:', error); + messageApi.error({ + content: error.response?.data?.message || error.message, + duration: 1 }); + setServiceStopping(false); + }); + }; + + const handleServiceAction = () => { + if (serviceStarted) { + handleStopService(); } else { - setServiceStarting(true); - startService({ model_name: config.baseModel || 'Qwen2.5-0.5B-Instruct' }) - .then((res) => { - if (res.data.code === 0) { - messageApi.success({ content: 'Service starting...', duration: 1 }); - startPolling(); - } else { - setServiceStarting(false); - messageApi.error({ content: res.data.message!, duration: 1 }); - } - }) - .catch((error) => { - console.error('Error starting service:', error); - setServiceStarting(false); - messageApi.error({ - content: error.response?.data?.message || error.message, - duration: 1 - }); - }); + if (isTraining) { + setShowtrainingModal(true); + + return; + } + + handleStartService(); } }; @@ -205,7 +222,7 @@ export function ModelStatus() { {isServiceStarting ? 'Starting...' : 'Stopping...'} - ) : status === 'running' ? ( + ) : serviceStarted ? ( <> @@ -244,6 +261,14 @@ export function ModelStatus() {
setShowRegisterModal(false)} open={showRegisterModal} /> + { + handleStartService(); + setShowtrainingModal(false); + }} + onClose={() => setShowtrainingModal(false)} + open={showtrainingModal} + />
); } diff --git a/lpm_frontend/src/components/StatusBar/index.tsx b/lpm_frontend/src/components/StatusBar/index.tsx index 5c9e319c..98beac6c 100644 --- a/lpm_frontend/src/components/StatusBar/index.tsx +++ b/lpm_frontend/src/components/StatusBar/index.tsx @@ -13,14 +13,6 @@ export function StatusBar({ status }: StatusBarProps) { ] as const; const getStepState = (stepStatus: (typeof steps)[number]['status']) => { - // If current status is running, all steps should be shown as completed, and trained should be active - if (status === 'running') { - return { - isActive: stepStatus === 'trained', - isCompleted: stepStatus !== 'trained' - }; - } - const statusOrder = ['seed_identity', 'memory_upload', 'training', 'trained']; const currentIndex = statusOrder.indexOf(status); const stepIndex = statusOrder.indexOf(stepStatus); diff --git a/lpm_frontend/src/components/ThinkingModelModal/index.tsx b/lpm_frontend/src/components/ThinkingModelModal/index.tsx new file mode 100644 index 00000000..dd5f2cdc --- /dev/null +++ b/lpm_frontend/src/components/ThinkingModelModal/index.tsx @@ -0,0 +1,126 @@ +import type { IThinkingModelParams } from '@/service/modelConfig'; +import { updateThinkingConfig } from '@/service/modelConfig'; +import { useModelConfigStore } from '@/store/useModelConfigStore'; +import { Input, message, Modal } from 'antd'; +import { useEffect, useState } from 'react'; + +interface IProps { + open: boolean; + onClose: () => void; +} + +const ThinkingModelModal = (props: IProps) => { + const { open, onClose: handleCancel } = props; + + const fetchModelConfig = useModelConfigStore((store) => store.fetchModelConfig); + const [thinkingModelParams, setThinkingModelParams] = useState( + {} as IThinkingModelParams + ); + const updateThinkingModelConfig = useModelConfigStore((store) => store.updateThinkingModelConfig); + const thinkingModelConfig = useModelConfigStore((store) => store.thinkingModelConfig); + + useEffect(() => { + if (open) { + fetchModelConfig(); + } + }, [open]); + + useEffect(() => { + setThinkingModelParams(thinkingModelConfig); + }, [thinkingModelConfig]); + + const handleUpdate = () => { + const thinkingConfigComplete = + !!thinkingModelParams.thinking_model_name && + !!thinkingModelParams.thinking_api_key && + !!thinkingModelParams.thinking_endpoint; + + if (!thinkingConfigComplete) { + message.error('Please fill in all thinking model configuration fields'); + + return; + } + + updateThinkingConfig(thinkingModelParams) + .then((res) => { + if (res.data.code == 0) { + updateThinkingModelConfig(thinkingModelParams); + handleCancel(); + } else { + throw new Error(res.data.message); + } + }) + .catch((error) => { + console.error(error.message || 'Failed to update model config'); + }); + }; + + return ( + { + handleUpdate(); + }} + open={open} + > +
+
Thinking model
+
Currently only supports DeepSeek
+
+
+
+
+ + + setThinkingModelParams({ + ...thinkingModelParams, + thinking_model_name: e.target.value + }) + } + value={thinkingModelParams.thinking_model_name} + /> +
+ +
+ + {/* form is to disable autoComplete */} +
+ + setThinkingModelParams({ + ...thinkingModelParams, + thinking_api_key: e.target.value + }) + } + value={thinkingModelParams.thinking_api_key} + /> + +
+
+ +
+ + + setThinkingModelParams({ + ...thinkingModelParams, + thinking_endpoint: e.target.value + }) + } + value={thinkingModelParams.thinking_endpoint} + /> +
+
+
+ ); +}; + +export default ThinkingModelModal; diff --git a/lpm_frontend/src/components/modelConfigModal/index.tsx b/lpm_frontend/src/components/modelConfigModal/index.tsx index 78276599..f03015ba 100644 --- a/lpm_frontend/src/components/modelConfigModal/index.tsx +++ b/lpm_frontend/src/components/modelConfigModal/index.tsx @@ -1,6 +1,6 @@ -import { getModelConfig, updateModelConfig } from '@/service/modelConfig'; -import { useModelConfigStore } from '@/store/useModelConfigStore'; -import { Input, message, Modal, Radio } from 'antd'; +import { updateModelConfig } from '../../service/modelConfig'; +import { useModelConfigStore } from '../../store/useModelConfigStore'; +import { Input, Modal, Radio } from 'antd'; import Image from 'next/image'; import { useCallback, useEffect, useState } from 'react'; import { QuestionCircleOutlined } from '@ant-design/icons'; @@ -28,22 +28,21 @@ const options = [ const ModelConfigModal = (props: IProps) => { const { open, onClose } = props; const modelConfig = useModelConfigStore((store) => store.modelConfig); - const updateLocalModelConfig = useModelConfigStore((store) => store.updateModelConfig); - + const baseModelConfig = useModelConfigStore((store) => store.baseModelConfig); + const updateBaseModelConfig = useModelConfigStore((store) => store.updateBaseModelConfig); + const fetchModelConfig = useModelConfigStore((store) => store.fetchModelConfig); + const localProviderType = useModelConfigStore((store) => store.modelConfig.provider_type); const [modelType, setModelType] = useState(''); useEffect(() => { - getModelConfig().then((res) => { - if (res.data.code == 0) { - const data = res.data.data || {}; + if (open) { + fetchModelConfig(); + } + }, [open]); - updateLocalModelConfig(data); - setModelType(data.provider_type); - } else { - message.error(res.data.message); - } - }); - }, []); + useEffect(() => { + setModelType(localProviderType); + }, [localProviderType]); const renderEmpty = () => { return ( @@ -69,10 +68,10 @@ const ModelConfigModal = (props: IProps) => { { - updateLocalModelConfig({ ...modelConfig, key: e.target.value }); + updateBaseModelConfig({ ...baseModelConfig, key: e.target.value }); }} placeholder="Enter your OpenAI API key" - value={modelConfig.key} + value={baseModelConfig.key} />
You can get your API key from{' '} @@ -89,7 +88,7 @@ const ModelConfigModal = (props: IProps) => {
); - }, [modelConfig]); + }, [baseModelConfig]); const renderCustom = useCallback(() => { return ( @@ -106,10 +105,10 @@ const ModelConfigModal = (props: IProps) => { className="w-full" data-form-type="other" onChange={(e) => { - updateLocalModelConfig({ ...modelConfig, chat_model_name: e.target.value }); + updateBaseModelConfig({ ...baseModelConfig, chat_model_name: e.target.value }); }} spellCheck="false" - value={modelConfig.chat_model_name} + value={baseModelConfig.chat_model_name} /> @@ -122,10 +121,10 @@ const ModelConfigModal = (props: IProps) => { className="w-full" data-form-type="other" onChange={(e) => { - updateLocalModelConfig({ ...modelConfig, chat_api_key: e.target.value }); + updateBaseModelConfig({ ...baseModelConfig, chat_api_key: e.target.value }); }} spellCheck="false" - value={modelConfig.chat_api_key} + value={baseModelConfig.chat_api_key} /> @@ -136,9 +135,9 @@ const ModelConfigModal = (props: IProps) => { autoComplete="off" className="w-full" onChange={(e) => { - updateLocalModelConfig({ ...modelConfig, chat_endpoint: e.target.value }); + updateBaseModelConfig({ ...baseModelConfig, chat_endpoint: e.target.value }); }} - value={modelConfig.chat_endpoint} + value={baseModelConfig.chat_endpoint} /> @@ -151,9 +150,12 @@ const ModelConfigModal = (props: IProps) => { { - updateLocalModelConfig({ ...modelConfig, embedding_model_name: e.target.value }); + updateBaseModelConfig({ + ...baseModelConfig, + embedding_model_name: e.target.value + }); }} - value={modelConfig.embedding_model_name} + value={baseModelConfig.embedding_model_name} /> @@ -162,9 +164,9 @@ const ModelConfigModal = (props: IProps) => { { - updateLocalModelConfig({ ...modelConfig, embedding_api_key: e.target.value }); + updateBaseModelConfig({ ...baseModelConfig, embedding_api_key: e.target.value }); }} - value={modelConfig.embedding_api_key} + value={baseModelConfig.embedding_api_key} /> @@ -174,31 +176,27 @@ const ModelConfigModal = (props: IProps) => { { - updateLocalModelConfig({ ...modelConfig, embedding_endpoint: e.target.value }); + updateBaseModelConfig({ ...baseModelConfig, embedding_endpoint: e.target.value }); }} - value={modelConfig.embedding_endpoint} + value={baseModelConfig.embedding_endpoint} /> ); - }, [modelConfig]); + }, [baseModelConfig, updateBaseModelConfig]); const handleUpdate = () => { - // When None is selected, save an empty provider_type instead of deleting the config - const providerType = modelType || ''; - - updateModelConfig({ ...modelConfig, provider_type: providerType }) + updateModelConfig(modelConfig) .then((res) => { if (res.data.code == 0) { - updateLocalModelConfig({ ...modelConfig, provider_type: providerType }); onClose(); } else { throw new Error(res.data.message); } }) - .catch((error: any) => { - message.error(error.message || 'Failed to update model config'); + .catch((error) => { + console.error(error.message || 'Failed to update model config'); }); }; @@ -246,7 +244,10 @@ const ModelConfigModal = (props: IProps) => {

setModelType(e.target.value)} + onChange={(e) => { + setModelType(e.target.value); + updateBaseModelConfig({ ...baseModelConfig, provider_type: e.target.value }); + }} optionType="button" options={options} value={modelType ? modelType : ''} diff --git a/lpm_frontend/src/components/svgs/stopIcon.tsx b/lpm_frontend/src/components/svgs/stopIcon.tsx new file mode 100644 index 00000000..50505c04 --- /dev/null +++ b/lpm_frontend/src/components/svgs/stopIcon.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; + +interface StopIconProps { + className?: string; +} + +const StopIcon = (props: StopIconProps) => { + const { className } = props; + + return ( + + + + ); +}; + +export default StopIcon; diff --git a/lpm_frontend/src/components/train/TrainingConfiguration.tsx b/lpm_frontend/src/components/train/TrainingConfiguration.tsx index 7e2e9aaf..3e3fe1d1 100644 --- a/lpm_frontend/src/components/train/TrainingConfiguration.tsx +++ b/lpm_frontend/src/components/train/TrainingConfiguration.tsx @@ -1,17 +1,20 @@ 'use client'; import type React from 'react'; -import { Fragment, useEffect, useMemo, useState } from 'react'; +import { Fragment, useMemo, useState } from 'react'; import { Listbox, Transition } from '@headlessui/react'; import { PlayIcon, StopIcon } from '@heroicons/react/24/outline'; -import { EVENT } from '@/utils/event'; -import { InputNumber, Radio, Spin, Tooltip } from 'antd'; +import { EVENT } from '../../utils/event'; +import { Checkbox, InputNumber, message, Radio, Spin, Tooltip } from 'antd'; import type { TrainingConfig } from '@/service/train'; import { QuestionCircleOutlined } from '@ant-design/icons'; import OpenAiModelIcon from '../svgs/OpenAiModelIcon'; import CustomModelIcon from '../svgs/CustomModelIcon'; import ColumnArrowIcon from '../svgs/ColumnArrowIcon'; import DoneIcon from '../svgs/DoneIcon'; +import ThinkingModelModal from '../ThinkingModelModal'; +import { useModelConfigStore } from '@/store/useModelConfigStore'; +import classNames from 'classnames'; interface BaseModelOption { value: string; @@ -29,14 +32,13 @@ interface TrainingConfigurationProps { isTraining: boolean; updateTrainingParams: (params: TrainingConfig) => void; status: string; - isResume: boolean; + trainSuspended: boolean; handleResetProgress: () => void; - nowTrainingParams: TrainingConfig | null; - changeBaseModel: boolean; handleTrainingAction: () => Promise; trainActionLoading: boolean; setSelectedInfo: React.Dispatch>; trainingParams: TrainingConfig; + cudaAvailable: boolean; } const synthesisModeOptions = [ @@ -51,28 +53,39 @@ const TrainingConfiguration: React.FC = ({ isTraining, updateTrainingParams, trainingParams, - nowTrainingParams, status, handleResetProgress, - isResume, - changeBaseModel, + trainSuspended, trainActionLoading, handleTrainingAction, - setSelectedInfo + setSelectedInfo, + cudaAvailable }) => { - const [disabledChangeParams, setDisabledChangeParams] = useState(false); + const [openThinkingModel, setOpenThinkingModel] = useState(false); + const [showThinkingWarning, setShowThinkingWarning] = useState(false); + const thinkingModelConfig = useModelConfigStore((state) => state.thinkingModelConfig); + + const disabledChangeParams = useMemo(() => { + return isTraining || trainSuspended; + }, [isTraining, trainSuspended]); + + const thinkingConfigComplete = useMemo(() => { + return ( + !!thinkingModelConfig.thinking_model_name && + !!thinkingModelConfig.thinking_api_key && + !!thinkingModelConfig.thinking_endpoint + ); + }, [thinkingModelConfig]); const trainButtonText = useMemo(() => { return isTraining ? 'Stop Training' - : !changeBaseModel - ? status === 'trained' || status === 'running' - ? 'Retrain' - : isResume - ? 'Resume Training' - : 'Start Training' - : 'Start Training'; - }, [isTraining, status, isResume, changeBaseModel]); + : status === 'trained' + ? 'Retrain' + : trainSuspended + ? 'Resume Training' + : 'Start Training'; + }, [isTraining, status, trainSuspended]); const trainButtonIcon = useMemo(() => { return isTraining ? ( @@ -86,10 +99,6 @@ const TrainingConfiguration: React.FC = ({ ); }, [isTraining, trainActionLoading]); - useEffect(() => { - setDisabledChangeParams(isTraining || (isResume && !changeBaseModel)); - }, [isTraining, isResume, changeBaseModel]); - return (
@@ -116,272 +125,342 @@ const TrainingConfiguration: React.FC = ({

-
-
-
-

- Step 1: Choose Support Model for Data Synthesis -

- {!modelConfig?.provider_type ? ( -
-
- - -
- - Model used for processing and synthesizing your memory data - -
- ) : ( -
-
{ +
+
+

+ Step 1: Choose Support Model for Data Synthesis +

+ {!modelConfig?.provider_type ? ( +
+
+ + -
- - Model used for processing and synthesizing your memory data - + Configure Support Model +
- )} -
-
Data Synthesis Mode
- - updateTrainingParams({ - ...trainingParams, - data_synthesis_mode: e.target.value - }) - } - optionType="button" - options={synthesisModeOptions} - value={ - disabledChangeParams && nowTrainingParams && !changeBaseModel - ? nowTrainingParams.data_synthesis_mode - : trainingParams.data_synthesis_mode - } - /> - - Low: Fast data synthesis. Medium: Balanced synthesis and speed. High: Rich - synthesis, slower speed. + Model used for processing and synthesizing your memory data
-
+ ) : ( +
+
+ Model Used :   + {modelConfig.provider_type === 'openai' ? ( + + ) : ( + + )} + + {modelConfig.provider_type === 'openai' ? 'OpenAI' : 'Custom Model'} + + +
+ + Model used for processing and synthesizing your memory data
- updateTrainingParams({ model_name: value })} - value={trainingParams.model_name} - > -
- - - {baseModelOptions.find((option) => option.value === trainingParams.model_name) - ?.label || 'Select a model...'} - - - - - - - - {baseModelOptions.map((option) => ( - - `relative cursor-pointer select-none py-2 pl-10 pr-4 ${active ? 'bg-blue-100 text-blue-900' : 'text-gray-900'}` - } - value={option.value} - > - {({ selected }) => ( - <> - - {option.label} + )} +
+
Data Synthesis Mode
+ + updateTrainingParams({ + ...trainingParams, + data_synthesis_mode: e.target.value + }) + } + optionType="button" + options={synthesisModeOptions} + value={trainingParams.data_synthesis_mode} + /> + + + Low: Fast data synthesis. Medium: Balanced synthesis and speed. High: Rich + synthesis, slower speed. + +
+
+ +
+
+

+ Step 2: Choose Base Model for Training Second Me +

+ + Base model for training your Second Me. Choose based on your available system + resources. + +
+ updateTrainingParams({ model_name: value })} + value={trainingParams.model_name} + > +
+ + + {baseModelOptions.find((option) => option.value === trainingParams.model_name) + ?.label || 'Select a model...'} + + + + + + + + {baseModelOptions.map((option) => ( + + `relative cursor-pointer select-none py-2 pl-10 pr-4 ${active ? 'bg-blue-100 text-blue-900' : 'text-gray-900'}` + } + value={option.value} + > + {({ selected }) => ( + <> + + {option.label} + + {selected ? ( + + - {selected ? ( - - - - ) : null} - - )} - - ))} - - -
-
+ ) : null} + + )} + + ))} + + +
+
+
+ +
+
+

+ Step 3: Configure Advanced Training Parameters +

+
+ Adjust these parameters to control training quality and performance. Recommended + settings will ensure stable training. +
+
+
+
+
Learning Rate
+ + + +
+ { + if (value == null) { + return; + } -
-
-

- Step 3: Configure Advanced Training Parameters -

+ updateTrainingParams({ ...trainingParams, learning_rate: value }); + }} + status={ + trainingParams.learning_rate == 0.005 || trainingParams.learning_rate == 0.00003 + ? 'warning' + : undefined + } + step={0.0001} + value={trainingParams.learning_rate} + />
- Adjust these parameters to control training quality and performance. Recommended - settings will ensure stable training. + Enter a value between 0.00003 and 0.005 (recommended: 0.0001)
-
-
-
-
Learning Rate
- - - -
- { - if (value == null) { - return; - } - - updateTrainingParams({ ...trainingParams, learning_rate: value }); - }} - status={ - trainingParams.learning_rate == 0.005 || - trainingParams.learning_rate == 0.00003 - ? 'warning' - : undefined +
+
+
Number of Epochs
+ + + +
+ { + if (value == null) { + return; } - step={0.0001} - value={ - disabledChangeParams && nowTrainingParams && !changeBaseModel - ? nowTrainingParams.learning_rate - : trainingParams.learning_rate + + updateTrainingParams({ ...trainingParams, number_of_epochs: value }); + }} + status={ + trainingParams.number_of_epochs == 10 || trainingParams.number_of_epochs == 1 + ? 'warning' + : undefined + } + step={1} + value={trainingParams.number_of_epochs} + /> +
+ Enter an integer between 1 and 10 (recommended: 2) +
+
+
+
+
Concurrency Threads
+ + + +
+ { + if (value == null) { + return; } - /> -
- Enter a value between 0.00003 and 0.005 (recommended: 0.0001) -
+ + updateTrainingParams({ ...trainingParams, concurrency_threads: value }); + }} + status={ + trainingParams.concurrency_threads == 10 || + trainingParams.concurrency_threads == 1 + ? 'warning' + : undefined + } + step={1} + value={trainingParams.concurrency_threads} + /> +
+ Enter an integer between 1 and 10 (recommended: 2)
-
-
-
Number of Epochs
- - - -
- { - if (value == null) { - return; - } +
- updateTrainingParams({ ...trainingParams, number_of_epochs: value }); - }} - status={ - trainingParams.number_of_epochs == 10 || trainingParams.number_of_epochs == 1 - ? 'warning' - : undefined - } - step={1} - value={ - disabledChangeParams && nowTrainingParams && !changeBaseModel - ? nowTrainingParams.number_of_epochs - : trainingParams.number_of_epochs - } - /> -
- Enter an integer between 1 and 10 (recommended: 2) -
+
+
+
Enable CUDA GPU Acceleration
+ + +
-
-
-
Concurrency Threads
- - - -
- { - if (value == null) { - return; - } +
+
+
+
- updateTrainingParams({ ...trainingParams, concurrency_threads: value }); - }} - status={ - trainingParams.concurrency_threads == 10 || - trainingParams.concurrency_threads == 1 - ? 'warning' - : undefined - } - step={1} - value={ - disabledChangeParams && nowTrainingParams && !changeBaseModel - ? nowTrainingParams.concurrency_threads - : trainingParams.concurrency_threads +
+
+ Step 4: Configure Advanced Behavior +
+ +
+ { + e.stopPropagation(); + + if (!thinkingConfigComplete) { + setShowThinkingWarning(true); + + if (!showThinkingWarning) { + setTimeout(() => setShowThinkingWarning(false), 2000); } - /> -
- Enter an integer between 1 and 10 (recommended: 2) -
-
+ + return; + } + + updateTrainingParams({ ...trainingParams, is_cot: e.target.checked }); + }} + /> +
{ + if (disabledChangeParams) return; + + setOpenThinkingModel(true); + }} + > + Thinking Model
@@ -390,20 +469,7 @@ const TrainingConfiguration: React.FC = ({
{isTraining && (
- - - + Full stop only when the current step is complete
)} @@ -429,6 +495,8 @@ const TrainingConfiguration: React.FC = ({
+ + setOpenThinkingModel(false)} open={openThinkingModel} />
); }; diff --git a/lpm_frontend/src/components/train/TrainingLog.tsx b/lpm_frontend/src/components/train/TrainingLog.tsx index 93ca2995..aceee8b4 100644 --- a/lpm_frontend/src/components/train/TrainingLog.tsx +++ b/lpm_frontend/src/components/train/TrainingLog.tsx @@ -11,11 +11,12 @@ const TrainingLog: React.FC = ({ trainingDetails }: TrainingLo const consoleEndRef = useRef(null); const [isUserScrolling, setIsUserScrolling] = useState(false); const userScrollTimeout = useRef(null); + const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(true); // Smooth scroll console to bottom const smoothScrollConsole = () => { if (consoleEndRef.current && !isUserScrolling) { - const consoleContainer = consoleEndRef.current.closest('.overflow-y-auto'); + const consoleContainer = consoleEndRef.current; if (consoleContainer instanceof HTMLElement) { consoleContainer.scrollTo({ @@ -29,22 +30,42 @@ const TrainingLog: React.FC = ({ trainingDetails }: TrainingLo useEffect(() => { // Set up scroll event listener to detect user scrolling const handleUserScroll = () => { - setIsUserScrolling(true); + if (!consoleEndRef.current) return; + + const consoleContainer = consoleEndRef.current.closest('.overflow-y-auto'); + + if (!(consoleContainer instanceof HTMLElement)) return; + + // Check if scrolled away from bottom + const isScrolledToBottom = + Math.abs((consoleContainer.scrollHeight - consoleContainer.scrollTop) - consoleContainer.clientHeight) < 50; + + // If scrolled away from bottom, consider it manual scrolling + if (!isScrolledToBottom) { + setIsUserScrolling(true); - // Clear any existing timeout - if (userScrollTimeout.current) { - clearTimeout(userScrollTimeout.current); - } + // Clear any existing timeout + if (userScrollTimeout.current) { + clearTimeout(userScrollTimeout.current); + } - // Reset the flag after a short delay - userScrollTimeout.current = setTimeout(() => { + // Reset the flag after a delay + userScrollTimeout.current = setTimeout(() => { + setIsUserScrolling(false); + }, 5000); // 5 seconds delay before allowing auto-scroll again + } else { + // If at bottom, not considered manual scrolling setIsUserScrolling(false); - }, 2000); // 2 seconds delay before allowing auto-scroll again + if (userScrollTimeout.current) { + clearTimeout(userScrollTimeout.current); + userScrollTimeout.current = null; + } + } }; // Find the console container and attach the scroll listener if (consoleEndRef.current) { - const consoleContainer = consoleEndRef.current.closest('.overflow-y-auto'); + const consoleContainer = consoleEndRef.current; if (consoleContainer instanceof HTMLElement) { consoleContainer.addEventListener('scroll', handleUserScroll); @@ -65,12 +86,24 @@ const TrainingLog: React.FC = ({ trainingDetails }: TrainingLo if (trainingDetails.length > 0) { smoothScrollConsole(); } - }, [trainingDetails]); + }, [trainingDetails, isAutoScrollEnabled]); + + const toggleAutoScroll = () => { + setIsAutoScrollEnabled(!isAutoScrollEnabled); + if (!isAutoScrollEnabled) { + // If we're re-enabling auto-scroll, scroll to bottom immediately + setIsUserScrolling(false); + setTimeout(smoothScrollConsole, 50); + } + }; return (

Training Log

-
+
{trainingDetails.length > 0 ? ( trainingDetails.map((detail, index) => ( @@ -83,7 +116,6 @@ const TrainingLog: React.FC = ({ trainingDetails }: TrainingLo No training logs available. Start training to see logs here.
)} -
diff --git a/lpm_frontend/src/components/train/TrainingProgress.tsx b/lpm_frontend/src/components/train/TrainingProgress.tsx index f6d0aaf0..943bf066 100644 --- a/lpm_frontend/src/components/train/TrainingProgress.tsx +++ b/lpm_frontend/src/components/train/TrainingProgress.tsx @@ -34,7 +34,7 @@ const TrainingProgress = (props: TrainingProgressProps) => {

Training Progress (may take long with more data and larger model)

- {(status === 'trained' || status === 'running') && ( + {status === 'trained' && ( Training Complete diff --git a/lpm_frontend/src/components/upload/RegisterUploadModal.tsx b/lpm_frontend/src/components/upload/RegisterUploadModal.tsx index ca8650e5..3e53e5c9 100644 --- a/lpm_frontend/src/components/upload/RegisterUploadModal.tsx +++ b/lpm_frontend/src/components/upload/RegisterUploadModal.tsx @@ -15,7 +15,6 @@ import type { Upload } from '@/service/upload'; import { registerUpload, deleteUpload, connectUpload } from '@/service/upload'; import { useUploadStore } from '@/store/useUploadStore'; -import { useTrainingStore } from '@/store/useTrainingStore'; import { updateRegisteredUpload } from '@/utils/localRegisteredUpload'; import NetWorkMemberList from './NetWorkMemberList'; import { useLoadInfoStore } from '@/store/useLoadInfoStore'; @@ -29,7 +28,6 @@ interface RegisterUploadModalProps { export default function RegisterUploadModal({ open, onClose }: RegisterUploadModalProps) { const [currentUpload, setCurrentUpload] = useState(null); - const statusTimeoutRef = useRef(); const addUpload = useUploadStore((state) => state.addUpload); const removeUpload = useUploadStore((state) => state.removeUpload); @@ -50,7 +48,6 @@ export default function RegisterUploadModal({ open, onClose }: RegisterUploadMod }, [loadInfo]); const [messageApi, contextHolder] = message.useMessage(); - const status = useTrainingStore((state) => state.status); useEffect(() => { if (open) { @@ -59,12 +56,6 @@ export default function RegisterUploadModal({ open, onClose }: RegisterUploadMod } }, [open, registerStatus]); - useEffect(() => { - if (status !== 'running' && statusTimeoutRef.current) { - clearInterval(statusTimeoutRef.current); - } - }, [status]); - useEffect(() => { // Clean up all polling when component closes if (!open && pollingIntervalsRef.current) { diff --git a/lpm_frontend/src/components/upload/TraingTipModal.tsx b/lpm_frontend/src/components/upload/TraingTipModal.tsx new file mode 100644 index 00000000..1328aa3f --- /dev/null +++ b/lpm_frontend/src/components/upload/TraingTipModal.tsx @@ -0,0 +1,25 @@ +import { Modal } from 'antd'; + +interface IProps { + open: boolean; + onClose: () => void; + confirm: () => void; +} + +const TrainingTipModal = (props: IProps) => { + const { open, onClose: handleCancel, confirm: handleConfirm } = props; + + return ( + + Turning on services during training may result in runtime model replacement + + ); +}; + +export default TrainingTipModal; diff --git a/lpm_frontend/src/layouts/DashboardLayout/Menu/index.tsx b/lpm_frontend/src/layouts/DashboardLayout/Menu/index.tsx index 12276ebf..79996760 100644 --- a/lpm_frontend/src/layouts/DashboardLayout/Menu/index.tsx +++ b/lpm_frontend/src/layouts/DashboardLayout/Menu/index.tsx @@ -23,15 +23,22 @@ const Menu = () => { const pathname = usePathname(); const router = useRouter(); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); - const status = useTrainingStore((state) => state.status); const loadInfo = useLoadInfoStore((state) => state.loadInfo); const clearLoadInfo = useLoadInfoStore((state) => state.clearLoadInfo); + const serviceStarted = useTrainingStore((state) => state.serviceStarted); const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [deleteConfirmText, setDeleteConfirmText] = useState(''); const [deleteConfirmLoading, setDeleteConfirmLoading] = useState(false); const [showModelConfig, setShowModelConfig] = useState(false); + const isTraining = useTrainingStore((state) => state.isTraining); + const trainSuspended = useTrainingStore((state) => state.trainSuspended); + + const disabledChangeParams = useMemo(() => { + return isTraining || trainSuspended; + }, [isTraining, trainSuspended]); + const isRegistered = useMemo(() => { return loadInfo?.status === 'online'; }, [loadInfo]); @@ -72,13 +79,7 @@ const Menu = () => { path.startsWith(ROUTER_PATH.PLAYGROUND) || path === ROUTER_PATH.APPLICATIONS ) { - if (status !== 'running' && status !== 'trained') { - e.preventDefault(); - message.info({ - content: 'Please train your model first', - duration: 2 - }); - } else if (status === 'trained') { + if (!serviceStarted) { e.preventDefault(); message.info({ content: 'Please start your model service first', @@ -255,10 +256,7 @@ const Menu = () => {