From 219cbab1e36493cc25c5ff2cb5443072463e9a7c Mon Sep 17 00:00:00 2001 From: Saifeddine ALOUI Date: Thu, 30 Jan 2025 23:27:43 +0100 Subject: [PATCH] Added progress when scanning files and fixed some bugs in the API --- lightrag/api/lightrag_server.py | 89 +- lightrag/api/static/index.html | 2 +- .../api/static/js/{lightrag_api.js => api.js} | 778 +++++++++--------- 3 files changed, 470 insertions(+), 399 deletions(-) rename lightrag/api/static/js/{lightrag_api.js => api.js} (91%) diff --git a/lightrag/api/lightrag_server.py b/lightrag/api/lightrag_server.py index e162f5ec..e4da6a4e 100644 --- a/lightrag/api/lightrag_server.py +++ b/lightrag/api/lightrag_server.py @@ -1,4 +1,24 @@ from fastapi import FastAPI, HTTPException, File, UploadFile, Form, Request +# Backend (Python) +# Add this to store progress globally +from typing import Dict +import threading + +# Global progress tracker +scan_progress: Dict = { + "is_scanning": False, + "current_file": "", + "indexed_count": 0, + "total_files": 0, + "progress": 0 +} + +# Lock for thread-safe operations +progress_lock = threading.Lock() + +import json +import os + from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import logging @@ -538,7 +558,7 @@ class DocumentManager: # Create input directory if it doesn't exist self.input_dir.mkdir(parents=True, exist_ok=True) - def scan_directory(self) -> List[Path]: + def scan_directory_for_new_files(self) -> List[Path]: """Scan input directory for new files""" new_files = [] for ext in self.supported_extensions: @@ -547,6 +567,14 @@ class DocumentManager: new_files.append(file_path) return new_files + def scan_directory(self) -> List[Path]: + """Scan input directory for new files""" + new_files = [] + for ext in self.supported_extensions: + for file_path in self.input_dir.rglob(f"*{ext}"): + new_files.append(file_path) + return new_files + def mark_as_indexed(self, file_path: Path): """Mark a file as indexed""" self.indexed_files.add(file_path) @@ -730,7 +758,7 @@ def create_app(args): # Startup logic if args.auto_scan_at_startup: try: - new_files = doc_manager.scan_directory() + new_files = doc_manager.scan_directory_for_new_files() for file_path in new_files: try: await index_file(file_path) @@ -982,43 +1010,56 @@ def create_app(args): else: logging.warning(f"No content extracted from file: {file_path}") + @app.post("/documents/scan", dependencies=[Depends(optional_api_key)]) async def scan_for_new_documents(): - """ - Manually trigger scanning for new documents in the directory managed by `doc_manager`. - - This endpoint facilitates manual initiation of a document scan to identify and index new files. - It processes all newly detected files, attempts indexing each file, logs any errors that occur, - and returns a summary of the operation. - - Returns: - dict: A dictionary containing: - - "status" (str): Indicates success or failure of the scanning process. - - "indexed_count" (int): The number of successfully indexed documents. - - "total_documents" (int): Total number of documents that have been indexed so far. - - Raises: - HTTPException: If an error occurs during the document scanning process, a 500 status - code is returned with details about the exception. - """ + """Trigger the scanning process""" + global scan_progress + try: - new_files = doc_manager.scan_directory() - indexed_count = 0 + with progress_lock: + if scan_progress["is_scanning"]: + return {"status": "already_scanning"} + + scan_progress["is_scanning"] = True + scan_progress["indexed_count"] = 0 + scan_progress["progress"] = 0 + + new_files = doc_manager.scan_directory_for_new_files() + scan_progress["total_files"] = len(new_files) for file_path in new_files: try: + with progress_lock: + scan_progress["current_file"] = os.path.basename(file_path) + await index_file(file_path) - indexed_count += 1 + + with progress_lock: + scan_progress["indexed_count"] += 1 + scan_progress["progress"] = (scan_progress["indexed_count"] / scan_progress["total_files"]) * 100 + except Exception as e: logging.error(f"Error indexing file {file_path}: {str(e)}") return { "status": "success", - "indexed_count": indexed_count, + "indexed_count": scan_progress["indexed_count"], "total_documents": len(doc_manager.indexed_files), } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + finally: + with progress_lock: + scan_progress["is_scanning"] = False + + @app.get("/documents/scan-progress") + async def get_scan_progress(): + """Get the current scanning progress""" + with progress_lock: + return scan_progress + + @app.post("/documents/upload", dependencies=[Depends(optional_api_key)]) async def upload_to_input_dir(file: UploadFile = File(...)): @@ -1849,7 +1890,7 @@ def create_app(args): "status": "healthy", "working_directory": str(args.working_dir), "input_directory": str(args.input_dir), - "indexed_files": files, + "indexed_files": [str(f) for f in files], "indexed_files_count": len(files), "configuration": { # LLM configuration binding/host address (if applicable)/model (if applicable) diff --git a/lightrag/api/static/index.html b/lightrag/api/static/index.html index 60900c03..c9659d5e 100644 --- a/lightrag/api/static/index.html +++ b/lightrag/api/static/index.html @@ -98,7 +98,7 @@ - + diff --git a/lightrag/api/static/js/lightrag_api.js b/lightrag/api/static/js/api.js similarity index 91% rename from lightrag/api/static/js/lightrag_api.js rename to lightrag/api/static/js/api.js index 3c2ff69c..65aa53be 100644 --- a/lightrag/api/static/js/lightrag_api.js +++ b/lightrag/api/static/js/api.js @@ -1,375 +1,405 @@ -// State management -const state = { - apiKey: localStorage.getItem('apiKey') || '', - files: [], - indexedFiles: [], - currentPage: 'file-manager' -}; - -// Utility functions -const showToast = (message, duration = 3000) => { - const toast = document.getElementById('toast'); - toast.querySelector('div').textContent = message; - toast.classList.remove('hidden'); - setTimeout(() => toast.classList.add('hidden'), duration); -}; - -const fetchWithAuth = async (url, options = {}) => { - const headers = { - ...(options.headers || {}), - ...(state.apiKey ? { 'Authorization': `Bearer ${state.apiKey}` } : {}) - }; - return fetch(url, { ...options, headers }); -}; - -// Page renderers -const pages = { - 'file-manager': () => ` -
-

File Manager

- -
- - -
- -
-

Selected Files

-
-
- - - - -
-

Indexed Files

-
-
- - - -
- `, - - 'query': () => ` -
-

Query Database

- -
-
- - -
- -
- - -
- - - -
-
-
- `, - - 'knowledge-graph': () => ` -
-
- - - -

Under Construction

-

Knowledge graph visualization will be available in a future update.

-
-
- `, - - 'status': () => ` -
-

System Status

-
-
-

System Health

-
-
-
-

Configuration

-
-
-
-
- `, - - 'settings': () => ` -
-

Settings

- -
-
-
- - -
- - -
-
-
- ` -}; - -// Page handlers -const handlers = { - 'file-manager': () => { - const fileInput = document.getElementById('fileInput'); - const dropZone = fileInput.parentElement.parentElement; - const fileList = document.querySelector('#fileList div'); - const indexedFiles = document.querySelector('#indexedFiles div'); - const uploadBtn = document.getElementById('uploadBtn'); - - const updateFileList = () => { - fileList.innerHTML = state.files.map(file => ` -
- ${file.name} - -
- `).join(''); - }; - - const updateIndexedFiles = async () => { - const response = await fetchWithAuth('/health'); - const data = await response.json(); - indexedFiles.innerHTML = data.indexed_files.map(file => ` -
- ${file} -
- `).join(''); - }; - - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('border-blue-500'); - }); - - dropZone.addEventListener('dragleave', () => { - dropZone.classList.remove('border-blue-500'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('border-blue-500'); - const files = Array.from(e.dataTransfer.files); - state.files.push(...files); - updateFileList(); - }); - - fileInput.addEventListener('change', () => { - state.files.push(...Array.from(fileInput.files)); - updateFileList(); - }); - - uploadBtn.addEventListener('click', async () => { - if (state.files.length === 0) { - showToast('Please select files to upload'); - return; - } - let apiKey = localStorage.getItem('apiKey') || ''; - const progress = document.getElementById('uploadProgress'); - const progressBar = progress.querySelector('div'); - const statusText = document.getElementById('uploadStatus'); - progress.classList.remove('hidden'); - - for (let i = 0; i < state.files.length; i++) { - const formData = new FormData(); - formData.append('file', state.files[i]); - - try { - await fetch('/documents/upload', { - method: 'POST', - headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {}, - body: formData - }); - - const percentage = ((i + 1) / state.files.length) * 100; - progressBar.style.width = `${percentage}%`; - statusText.textContent = `${i + 1}/${state.files.length}`; - } catch (error) { - console.error('Upload error:', error); - } - } - progress.classList.add('hidden'); - }); - rescanBtn.addEventListener('click', async () => { - let apiKey = localStorage.getItem('apiKey') || ''; - const progress = document.getElementById('uploadProgress'); - const progressBar = progress.querySelector('div'); - const statusText = document.getElementById('uploadStatus'); - progress.classList.remove('hidden'); - try { - const scan_output = await fetch('/documents/scan', { - method: 'GET', - }); - statusText.textContent = scan_output.data; - } catch (error) { - console.error('Upload error:', error); - } - progress.classList.add('hidden'); - }); - updateIndexedFiles(); - }, - - 'query': () => { - const queryBtn = document.getElementById('queryBtn'); - const queryInput = document.getElementById('queryInput'); - const queryMode = document.getElementById('queryMode'); - const queryResult = document.getElementById('queryResult'); - - let apiKey = localStorage.getItem('apiKey') || ''; - - queryBtn.addEventListener('click', async () => { - const query = queryInput.value.trim(); - if (!query) { - showToast('Please enter a query'); - return; - } - - queryBtn.disabled = true; - queryBtn.innerHTML = ` - - - - - Processing... - `; - - try { - const response = await fetchWithAuth('/query', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query, - mode: queryMode.value, - stream: false, - only_need_context: false - }) - }); - - const data = await response.json(); - queryResult.innerHTML = marked.parse(data.response); - } catch (error) { - showToast('Error processing query'); - } finally { - queryBtn.disabled = false; - queryBtn.textContent = 'Send Query'; - } - }); - }, - - 'status': async () => { - const healthStatus = document.getElementById('healthStatus'); - const configStatus = document.getElementById('configStatus'); - - try { - const response = await fetchWithAuth('/health'); - const data = await response.json(); - - healthStatus.innerHTML = ` -
-
-
- ${data.status} -
-
-

Working Directory: ${data.working_directory}

-

Input Directory: ${data.input_directory}

-

Indexed Files: ${data.indexed_files_count}

-
-
- `; - - configStatus.innerHTML = Object.entries(data.configuration) - .map(([key, value]) => ` -
- ${key}: - ${value} -
- `).join(''); - } catch (error) { - showToast('Error fetching status'); - } - }, - - 'settings': () => { - const saveBtn = document.getElementById('saveSettings'); - const apiKeyInput = document.getElementById('apiKeyInput'); - - saveBtn.addEventListener('click', () => { - state.apiKey = apiKeyInput.value; - localStorage.setItem('apiKey', state.apiKey); - showToast('Settings saved successfully'); - }); - } -}; - -// Navigation handling -document.querySelectorAll('.nav-item').forEach(item => { - item.addEventListener('click', (e) => { - e.preventDefault(); - const page = item.dataset.page; - document.getElementById('content').innerHTML = pages[page](); - if (handlers[page]) handlers[page](); - state.currentPage = page; - }); -}); - -// Initialize with file manager -document.getElementById('content').innerHTML = pages['file-manager'](); -handlers['file-manager'](); - -// Global functions -window.removeFile = (fileName) => { - state.files = state.files.filter(file => file.name !== fileName); - document.querySelector('#fileList div').innerHTML = state.files.map(file => ` -
- ${file.name} - -
- `).join(''); +// State management +const state = { + apiKey: localStorage.getItem('apiKey') || '', + files: [], + indexedFiles: [], + currentPage: 'file-manager' +}; + +// Utility functions +const showToast = (message, duration = 3000) => { + const toast = document.getElementById('toast'); + toast.querySelector('div').textContent = message; + toast.classList.remove('hidden'); + setTimeout(() => toast.classList.add('hidden'), duration); +}; + +const fetchWithAuth = async (url, options = {}) => { + const headers = { + ...(options.headers || {}), + ...(state.apiKey ? { 'Authorization': `Bearer ${state.apiKey}` } : {}) + }; + return fetch(url, { ...options, headers }); +}; + +// Page renderers +const pages = { + 'file-manager': () => ` +
+

File Manager

+ +
+ + +
+ +
+

Selected Files

+
+
+ + + + + +
+

Indexed Files

+
+
+ + +
+ `, + + 'query': () => ` +
+

Query Database

+ +
+
+ + +
+ +
+ + +
+ + + +
+
+
+ `, + + 'knowledge-graph': () => ` +
+
+ + + +

Under Construction

+

Knowledge graph visualization will be available in a future update.

+
+
+ `, + + 'status': () => ` +
+

System Status

+
+
+

System Health

+
+
+
+

Configuration

+
+
+
+
+ `, + + 'settings': () => ` +
+

Settings

+ +
+
+
+ + +
+ + +
+
+
+ ` +}; + +// Page handlers +const handlers = { + 'file-manager': () => { + const fileInput = document.getElementById('fileInput'); + const dropZone = fileInput.parentElement.parentElement; + const fileList = document.querySelector('#fileList div'); + const indexedFiles = document.querySelector('#indexedFiles div'); + const uploadBtn = document.getElementById('uploadBtn'); + + const updateFileList = () => { + fileList.innerHTML = state.files.map(file => ` +
+ ${file.name} + +
+ `).join(''); + }; + + const updateIndexedFiles = async () => { + const response = await fetchWithAuth('/health'); + const data = await response.json(); + indexedFiles.innerHTML = data.indexed_files.map(file => ` +
+ ${file} +
+ `).join(''); + }; + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('border-blue-500'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('border-blue-500'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('border-blue-500'); + const files = Array.from(e.dataTransfer.files); + state.files.push(...files); + updateFileList(); + }); + + fileInput.addEventListener('change', () => { + state.files.push(...Array.from(fileInput.files)); + updateFileList(); + }); + + uploadBtn.addEventListener('click', async () => { + if (state.files.length === 0) { + showToast('Please select files to upload'); + return; + } + let apiKey = localStorage.getItem('apiKey') || ''; + const progress = document.getElementById('uploadProgress'); + const progressBar = progress.querySelector('div'); + const statusText = document.getElementById('uploadStatus'); + progress.classList.remove('hidden'); + + for (let i = 0; i < state.files.length; i++) { + const formData = new FormData(); + formData.append('file', state.files[i]); + + try { + await fetch('/documents/upload', { + method: 'POST', + headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {}, + body: formData + }); + + const percentage = ((i + 1) / state.files.length) * 100; + progressBar.style.width = `${percentage}%`; + statusText.textContent = `${i + 1}/${state.files.length}`; + } catch (error) { + console.error('Upload error:', error); + } + } + progress.classList.add('hidden'); + }); + + rescanBtn.addEventListener('click', async () => { + const progress = document.getElementById('uploadProgress'); + const progressBar = progress.querySelector('div'); + const statusText = document.getElementById('uploadStatus'); + progress.classList.remove('hidden'); + + try { + // Start the scanning process + const scanResponse = await fetch('/documents/scan', { + method: 'POST', + }); + + if (!scanResponse.ok) { + throw new Error('Scan failed to start'); + } + + // Start polling for progress + const pollInterval = setInterval(async () => { + const progressResponse = await fetch('/documents/scan-progress'); + const progressData = await progressResponse.json(); + + // Update progress bar + progressBar.style.width = `${progressData.progress}%`; + + // Update status text + if (progressData.total_files > 0) { + statusText.textContent = `Processing ${progressData.current_file} (${progressData.indexed_count}/${progressData.total_files})`; + } + + // Check if scanning is complete + if (!progressData.is_scanning) { + clearInterval(pollInterval); + progress.classList.add('hidden'); + statusText.textContent = 'Scan complete!'; + } + }, 1000); // Poll every second + + } catch (error) { + console.error('Upload error:', error); + progress.classList.add('hidden'); + statusText.textContent = 'Error during scanning process'; + } + }); + + + updateIndexedFiles(); + }, + + 'query': () => { + const queryBtn = document.getElementById('queryBtn'); + const queryInput = document.getElementById('queryInput'); + const queryMode = document.getElementById('queryMode'); + const queryResult = document.getElementById('queryResult'); + + let apiKey = localStorage.getItem('apiKey') || ''; + + queryBtn.addEventListener('click', async () => { + const query = queryInput.value.trim(); + if (!query) { + showToast('Please enter a query'); + return; + } + + queryBtn.disabled = true; + queryBtn.innerHTML = ` + + + + + Processing... + `; + + try { + const response = await fetchWithAuth('/query', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query, + mode: queryMode.value, + stream: false, + only_need_context: false + }) + }); + + const data = await response.json(); + queryResult.innerHTML = marked.parse(data.response); + } catch (error) { + showToast('Error processing query'); + } finally { + queryBtn.disabled = false; + queryBtn.textContent = 'Send Query'; + } + }); + }, + + 'status': async () => { + const healthStatus = document.getElementById('healthStatus'); + const configStatus = document.getElementById('configStatus'); + + try { + const response = await fetchWithAuth('/health'); + const data = await response.json(); + + healthStatus.innerHTML = ` +
+
+
+ ${data.status} +
+
+

Working Directory: ${data.working_directory}

+

Input Directory: ${data.input_directory}

+

Indexed Files: ${data.indexed_files_count}

+
+
+ `; + + configStatus.innerHTML = Object.entries(data.configuration) + .map(([key, value]) => ` +
+ ${key}: + ${value} +
+ `).join(''); + } catch (error) { + showToast('Error fetching status'); + } + }, + + 'settings': () => { + const saveBtn = document.getElementById('saveSettings'); + const apiKeyInput = document.getElementById('apiKeyInput'); + + saveBtn.addEventListener('click', () => { + state.apiKey = apiKeyInput.value; + localStorage.setItem('apiKey', state.apiKey); + showToast('Settings saved successfully'); + }); + } +}; + +// Navigation handling +document.querySelectorAll('.nav-item').forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); + const page = item.dataset.page; + document.getElementById('content').innerHTML = pages[page](); + if (handlers[page]) handlers[page](); + state.currentPage = page; + }); +}); + +// Initialize with file manager +document.getElementById('content').innerHTML = pages['file-manager'](); +handlers['file-manager'](); + +// Global functions +window.removeFile = (fileName) => { + state.files = state.files.filter(file => file.name !== fileName); + document.querySelector('#fileList div').innerHTML = state.files.map(file => ` +
+ ${file.name} + +
+ `).join(''); };