feat: Enhance multi-tenant support by allowing unauthenticated access and updating document routes to use tenant-specific RAG instances

This commit is contained in:
Raphaël MANSUY 2025-12-05 00:55:09 +08:00
parent 730c406749
commit e4962dd2a5
12 changed files with 286 additions and 73 deletions

View file

@ -169,6 +169,7 @@ async def get_tenant_context(
Multi-tenant requests must include: Multi-tenant requests must include:
- Authorization header with JWT token containing tenant_id - Authorization header with JWT token containing tenant_id
- OR X-API-Key header with valid API key - OR X-API-Key header with valid API key
- OR no auth if auth is not configured (auth_mode=disabled)
- X-Tenant-ID header (optional, if not in token) - X-Tenant-ID header (optional, if not in token)
- X-KB-ID header (optional, if not in token) - X-KB-ID header (optional, if not in token)
@ -189,9 +190,13 @@ async def get_tenant_context(
role_str = "viewer" role_str = "viewer"
metadata = {} metadata = {}
# Check API Key first # Check if authentication is configured
api_key = os.getenv("LIGHTRAG_API_KEY") or global_args.key api_key = os.getenv("LIGHTRAG_API_KEY") or global_args.key
if api_key and x_api_key and x_api_key == api_key: api_key_configured = bool(api_key)
auth_configured = bool(auth_handler.accounts)
# Check API Key first
if api_key_configured and x_api_key and x_api_key == api_key:
username = "system_admin" username = "system_admin"
role_str = "admin" role_str = "admin"
elif authorization: elif authorization:
@ -217,6 +222,10 @@ async def get_tenant_context(
username = token_data.get("username") username = token_data.get("username")
metadata = token_data.get("metadata", {}) metadata = token_data.get("metadata", {})
role_str = token_data.get("role", "viewer") role_str = token_data.get("role", "viewer")
elif not auth_configured and not api_key_configured:
# No auth configured - allow unauthenticated access with guest user
username = "guest"
role_str = "viewer"
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,

View file

@ -2081,7 +2081,10 @@ def create_document_routes(
@router.post( @router.post(
"/scan", response_model=ScanResponse, dependencies=[Depends(combined_auth)] "/scan", response_model=ScanResponse, dependencies=[Depends(combined_auth)]
) )
async def scan_for_new_documents(background_tasks: BackgroundTasks): async def scan_for_new_documents(
background_tasks: BackgroundTasks,
tenant_rag: LightRAG = Depends(get_tenant_rag)
):
""" """
Trigger the scanning process for new documents. Trigger the scanning process for new documents.
@ -2089,14 +2092,18 @@ def create_document_routes(
and processes them. If a scanning process is already running, it returns a status indicating and processes them. If a scanning process is already running, it returns a status indicating
that fact. that fact.
Args:
background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
ScanResponse: A response object containing the scanning status and track_id ScanResponse: A response object containing the scanning status and track_id
""" """
# Generate track_id with "scan" prefix for scanning operation # Generate track_id with "scan" prefix for scanning operation
track_id = generate_track_id("scan") track_id = generate_track_id("scan")
# Start the scanning process in the background with track_id # Start the scanning process in the background with track_id (use tenant-specific RAG)
background_tasks.add_task(run_scanning_process, rag, doc_manager, track_id) background_tasks.add_task(run_scanning_process, tenant_rag, doc_manager, track_id)
return ScanResponse( return ScanResponse(
status="scanning_started", status="scanning_started",
message="Scanning process has been initiated in the background", message="Scanning process has been initiated in the background",
@ -2107,7 +2114,9 @@ def create_document_routes(
"/upload", response_model=InsertResponse, dependencies=[Depends(combined_auth)] "/upload", response_model=InsertResponse, dependencies=[Depends(combined_auth)]
) )
async def upload_to_input_dir( async def upload_to_input_dir(
background_tasks: BackgroundTasks, file: UploadFile = File(...) background_tasks: BackgroundTasks,
file: UploadFile = File(...),
tenant_rag: LightRAG = Depends(get_tenant_rag)
): ):
""" """
Upload a file to the input directory and index it. Upload a file to the input directory and index it.
@ -2119,6 +2128,7 @@ def create_document_routes(
Args: Args:
background_tasks: FastAPI BackgroundTasks for async processing background_tasks: FastAPI BackgroundTasks for async processing
file (UploadFile): The file to be uploaded. It must have an allowed extension. file (UploadFile): The file to be uploaded. It must have an allowed extension.
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
InsertResponse: A response object containing the upload status and a message. InsertResponse: A response object containing the upload status and a message.
@ -2137,8 +2147,8 @@ def create_document_routes(
detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}", detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
) )
# Check if filename already exists in doc_status storage # Check if filename already exists in doc_status storage (tenant-specific)
existing_doc_data = await rag.doc_status.get_doc_by_file_path(safe_filename) existing_doc_data = await tenant_rag.doc_status.get_doc_by_file_path(safe_filename)
if existing_doc_data: if existing_doc_data:
# Get document status and track_id from existing document # Get document status and track_id from existing document
status = existing_doc_data.get("status", "unknown") status = existing_doc_data.get("status", "unknown")
@ -2164,8 +2174,8 @@ def create_document_routes(
track_id = generate_track_id("upload") track_id = generate_track_id("upload")
# Add to background tasks and get track_id # Add to background tasks and get track_id (use tenant-specific RAG)
background_tasks.add_task(pipeline_index_file, rag, file_path, track_id) background_tasks.add_task(pipeline_index_file, tenant_rag, file_path, track_id)
return InsertResponse( return InsertResponse(
status="success", status="success",
@ -2182,7 +2192,9 @@ def create_document_routes(
"/text", response_model=InsertResponse, dependencies=[Depends(combined_auth)] "/text", response_model=InsertResponse, dependencies=[Depends(combined_auth)]
) )
async def insert_text( async def insert_text(
request: InsertTextRequest, background_tasks: BackgroundTasks request: InsertTextRequest,
background_tasks: BackgroundTasks,
tenant_rag: LightRAG = Depends(get_tenant_rag)
): ):
""" """
Insert text into the RAG system. Insert text into the RAG system.
@ -2193,6 +2205,7 @@ def create_document_routes(
Args: Args:
request (InsertTextRequest): The request body containing the text to be inserted. request (InsertTextRequest): The request body containing the text to be inserted.
background_tasks: FastAPI BackgroundTasks for async processing background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
InsertResponse: A response object containing the status of the operation. InsertResponse: A response object containing the status of the operation.
@ -2201,13 +2214,13 @@ def create_document_routes(
HTTPException: If an error occurs during text processing (500). HTTPException: If an error occurs during text processing (500).
""" """
try: try:
# Check if file_source already exists in doc_status storage # Check if file_source already exists in doc_status storage (tenant-specific)
if ( if (
request.file_source request.file_source
and request.file_source.strip() and request.file_source.strip()
and request.file_source != "unknown_source" and request.file_source != "unknown_source"
): ):
existing_doc_data = await rag.doc_status.get_doc_by_file_path( existing_doc_data = await tenant_rag.doc_status.get_doc_by_file_path(
request.file_source request.file_source
) )
if existing_doc_data: if existing_doc_data:
@ -2221,10 +2234,10 @@ def create_document_routes(
track_id=existing_track_id, track_id=existing_track_id,
) )
# Check if content already exists by computing content hash (doc_id) # Check if content already exists by computing content hash (doc_id) (tenant-specific)
sanitized_text = sanitize_text_for_encoding(request.text) sanitized_text = sanitize_text_for_encoding(request.text)
content_doc_id = compute_mdhash_id(sanitized_text, prefix="doc-") content_doc_id = compute_mdhash_id(sanitized_text, prefix="doc-")
existing_doc = await rag.doc_status.get_by_id(content_doc_id) existing_doc = await tenant_rag.doc_status.get_by_id(content_doc_id)
if existing_doc: if existing_doc:
# Content already exists, return duplicated with existing track_id # Content already exists, return duplicated with existing track_id
status = existing_doc.get("status", "unknown") status = existing_doc.get("status", "unknown")
@ -2240,7 +2253,7 @@ def create_document_routes(
background_tasks.add_task( background_tasks.add_task(
pipeline_index_texts, pipeline_index_texts,
rag, tenant_rag,
[request.text], [request.text],
file_sources=[request.file_source], file_sources=[request.file_source],
track_id=track_id, track_id=track_id,
@ -2262,7 +2275,9 @@ def create_document_routes(
dependencies=[Depends(combined_auth)], dependencies=[Depends(combined_auth)],
) )
async def insert_texts( async def insert_texts(
request: InsertTextsRequest, background_tasks: BackgroundTasks request: InsertTextsRequest,
background_tasks: BackgroundTasks,
tenant_rag: LightRAG = Depends(get_tenant_rag)
): ):
""" """
Insert multiple texts into the RAG system. Insert multiple texts into the RAG system.
@ -2273,6 +2288,7 @@ def create_document_routes(
Args: Args:
request (InsertTextsRequest): The request body containing the list of texts. request (InsertTextsRequest): The request body containing the list of texts.
background_tasks: FastAPI BackgroundTasks for async processing background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
InsertResponse: A response object containing the status of the operation. InsertResponse: A response object containing the status of the operation.
@ -2281,7 +2297,7 @@ def create_document_routes(
HTTPException: If an error occurs during text processing (500). HTTPException: If an error occurs during text processing (500).
""" """
try: try:
# Check if any file_sources already exist in doc_status storage # Check if any file_sources already exist in doc_status storage (tenant-specific)
if request.file_sources: if request.file_sources:
for file_source in request.file_sources: for file_source in request.file_sources:
if ( if (
@ -2289,7 +2305,7 @@ def create_document_routes(
and file_source.strip() and file_source.strip()
and file_source != "unknown_source" and file_source != "unknown_source"
): ):
existing_doc_data = await rag.doc_status.get_doc_by_file_path( existing_doc_data = await tenant_rag.doc_status.get_doc_by_file_path(
file_source file_source
) )
if existing_doc_data: if existing_doc_data:
@ -2303,11 +2319,11 @@ def create_document_routes(
track_id=existing_track_id, track_id=existing_track_id,
) )
# Check if any content already exists by computing content hash (doc_id) # Check if any content already exists by computing content hash (doc_id) (tenant-specific)
for text in request.texts: for text in request.texts:
sanitized_text = sanitize_text_for_encoding(text) sanitized_text = sanitize_text_for_encoding(text)
content_doc_id = compute_mdhash_id(sanitized_text, prefix="doc-") content_doc_id = compute_mdhash_id(sanitized_text, prefix="doc-")
existing_doc = await rag.doc_status.get_by_id(content_doc_id) existing_doc = await tenant_rag.doc_status.get_by_id(content_doc_id)
if existing_doc: if existing_doc:
# Content already exists, return duplicated with existing track_id # Content already exists, return duplicated with existing track_id
status = existing_doc.get("status", "unknown") status = existing_doc.get("status", "unknown")
@ -2323,7 +2339,7 @@ def create_document_routes(
background_tasks.add_task( background_tasks.add_task(
pipeline_index_texts, pipeline_index_texts,
rag, tenant_rag,
request.texts, request.texts,
file_sources=request.file_sources, file_sources=request.file_sources,
track_id=track_id, track_id=track_id,
@ -2342,7 +2358,9 @@ def create_document_routes(
@router.delete( @router.delete(
"", response_model=ClearDocumentsResponse, dependencies=[Depends(combined_auth)] "", response_model=ClearDocumentsResponse, dependencies=[Depends(combined_auth)]
) )
async def clear_documents(): async def clear_documents(
tenant_rag: LightRAG = Depends(get_tenant_rag)
):
""" """
Clear all documents from the RAG system. Clear all documents from the RAG system.
@ -2350,6 +2368,9 @@ def create_document_routes(
It uses the storage drop methods to properly clean up all data and removes all files It uses the storage drop methods to properly clean up all data and removes all files
from the input directory. from the input directory.
Args:
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
ClearDocumentsResponse: A response object containing the status and message. ClearDocumentsResponse: A response object containing the status and message.
- status="success": All documents and files were successfully cleared. - status="success": All documents and files were successfully cleared.
@ -2368,12 +2389,12 @@ def create_document_routes(
get_namespace_lock, get_namespace_lock,
) )
# Get pipeline status and lock # Get pipeline status and lock (tenant-specific)
pipeline_status = await get_namespace_data( pipeline_status = await get_namespace_data(
"pipeline_status", workspace=rag.workspace "pipeline_status", workspace=tenant_rag.workspace
) )
pipeline_status_lock = get_namespace_lock( pipeline_status_lock = get_namespace_lock(
"pipeline_status", workspace=rag.workspace "pipeline_status", workspace=tenant_rag.workspace
) )
# Check and set status with lock # Check and set status with lock
@ -2403,20 +2424,20 @@ def create_document_routes(
) )
try: try:
# Use drop method to clear all data # Use drop method to clear all data (tenant-specific)
drop_tasks = [] drop_tasks = []
storages = [ storages = [
rag.text_chunks, tenant_rag.text_chunks,
rag.full_docs, tenant_rag.full_docs,
rag.full_entities, tenant_rag.full_entities,
rag.full_relations, tenant_rag.full_relations,
rag.entity_chunks, tenant_rag.entity_chunks,
rag.relation_chunks, tenant_rag.relation_chunks,
rag.entities_vdb, tenant_rag.entities_vdb,
rag.relationships_vdb, tenant_rag.relationships_vdb,
rag.chunks_vdb, tenant_rag.chunks_vdb,
rag.chunk_entity_relation_graph, tenant_rag.chunk_entity_relation_graph,
rag.doc_status, tenant_rag.doc_status,
] ]
# Log storage drop start # Log storage drop start
@ -2760,6 +2781,7 @@ def create_document_routes(
async def delete_document( async def delete_document(
delete_request: DeleteDocRequest, delete_request: DeleteDocRequest,
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
tenant_rag: LightRAG = Depends(get_tenant_rag),
) -> DeleteDocByIdResponse: ) -> DeleteDocByIdResponse:
""" """
Delete documents and all their associated data by their IDs using background processing. Delete documents and all their associated data by their IDs using background processing.
@ -2774,6 +2796,7 @@ def create_document_routes(
Args: Args:
delete_request (DeleteDocRequest): The request containing the document IDs and deletion options. delete_request (DeleteDocRequest): The request containing the document IDs and deletion options.
background_tasks: FastAPI BackgroundTasks for async processing background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
DeleteDocByIdResponse: The result of the deletion operation. DeleteDocByIdResponse: The result of the deletion operation.
@ -2793,10 +2816,10 @@ def create_document_routes(
) )
pipeline_status = await get_namespace_data( pipeline_status = await get_namespace_data(
"pipeline_status", workspace=rag.workspace "pipeline_status", workspace=tenant_rag.workspace
) )
pipeline_status_lock = get_namespace_lock( pipeline_status_lock = get_namespace_lock(
"pipeline_status", workspace=rag.workspace "pipeline_status", workspace=tenant_rag.workspace
) )
# Check if pipeline is busy with proper lock # Check if pipeline is busy with proper lock
@ -2808,10 +2831,10 @@ def create_document_routes(
doc_id=", ".join(doc_ids), doc_id=", ".join(doc_ids),
) )
# Add deletion task to background tasks # Add deletion task to background tasks (use tenant-specific RAG)
background_tasks.add_task( background_tasks.add_task(
background_delete_documents, background_delete_documents,
rag, tenant_rag,
doc_manager, doc_manager,
doc_ids, doc_ids,
delete_request.delete_file, delete_request.delete_file,
@ -2835,7 +2858,10 @@ def create_document_routes(
response_model=ClearCacheResponse, response_model=ClearCacheResponse,
dependencies=[Depends(combined_auth)], dependencies=[Depends(combined_auth)],
) )
async def clear_cache(request: ClearCacheRequest): async def clear_cache(
request: ClearCacheRequest,
tenant_rag: LightRAG = Depends(get_tenant_rag)
):
""" """
Clear all cache data from the LLM response cache storage. Clear all cache data from the LLM response cache storage.
@ -2844,6 +2870,7 @@ def create_document_routes(
Args: Args:
request (ClearCacheRequest): The request body (ignored for compatibility). request (ClearCacheRequest): The request body (ignored for compatibility).
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
ClearCacheResponse: A response object containing the status and message. ClearCacheResponse: A response object containing the status and message.
@ -2852,8 +2879,8 @@ def create_document_routes(
HTTPException: If an error occurs during cache clearing (500). HTTPException: If an error occurs during cache clearing (500).
""" """
try: try:
# Call the aclear_cache method (no modes parameter) # Call the aclear_cache method (no modes parameter) - tenant-specific
await rag.aclear_cache() await tenant_rag.aclear_cache()
# Prepare success message # Prepare success message
message = "Successfully cleared all cache" message = "Successfully cleared all cache"
@ -2869,12 +2896,16 @@ def create_document_routes(
response_model=DeletionResult, response_model=DeletionResult,
dependencies=[Depends(combined_auth)], dependencies=[Depends(combined_auth)],
) )
async def delete_entity(request: DeleteEntityRequest): async def delete_entity(
request: DeleteEntityRequest,
tenant_rag: LightRAG = Depends(get_tenant_rag)
):
""" """
Delete an entity and all its relationships from the knowledge graph. Delete an entity and all its relationships from the knowledge graph.
Args: Args:
request (DeleteEntityRequest): The request body containing the entity name. request (DeleteEntityRequest): The request body containing the entity name.
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
DeletionResult: An object containing the outcome of the deletion process. DeletionResult: An object containing the outcome of the deletion process.
@ -2883,7 +2914,7 @@ def create_document_routes(
HTTPException: If the entity is not found (404) or an error occurs (500). HTTPException: If the entity is not found (404) or an error occurs (500).
""" """
try: try:
result = await rag.adelete_by_entity(entity_name=request.entity_name) result = await tenant_rag.adelete_by_entity(entity_name=request.entity_name)
if result.status == "not_found": if result.status == "not_found":
raise HTTPException(status_code=404, detail=result.message) raise HTTPException(status_code=404, detail=result.message)
if result.status == "fail": if result.status == "fail":
@ -2904,12 +2935,16 @@ def create_document_routes(
response_model=DeletionResult, response_model=DeletionResult,
dependencies=[Depends(combined_auth)], dependencies=[Depends(combined_auth)],
) )
async def delete_relation(request: DeleteRelationRequest): async def delete_relation(
request: DeleteRelationRequest,
tenant_rag: LightRAG = Depends(get_tenant_rag)
):
""" """
Delete a relationship between two entities from the knowledge graph. Delete a relationship between two entities from the knowledge graph.
Args: Args:
request (DeleteRelationRequest): The request body containing the source and target entity names. request (DeleteRelationRequest): The request body containing the source and target entity names.
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
DeletionResult: An object containing the outcome of the deletion process. DeletionResult: An object containing the outcome of the deletion process.
@ -2918,7 +2953,7 @@ def create_document_routes(
HTTPException: If the relation is not found (404) or an error occurs (500). HTTPException: If the relation is not found (404) or an error occurs (500).
""" """
try: try:
result = await rag.adelete_by_relation( result = await tenant_rag.adelete_by_relation(
source_entity=request.source_entity, source_entity=request.source_entity,
target_entity=request.target_entity, target_entity=request.target_entity,
) )
@ -3139,7 +3174,10 @@ def create_document_routes(
response_model=ReprocessResponse, response_model=ReprocessResponse,
dependencies=[Depends(combined_auth)], dependencies=[Depends(combined_auth)],
) )
async def reprocess_failed_documents(background_tasks: BackgroundTasks): async def reprocess_failed_documents(
background_tasks: BackgroundTasks,
tenant_rag: LightRAG = Depends(get_tenant_rag)
):
""" """
Reprocess failed and pending documents. Reprocess failed and pending documents.
@ -3156,6 +3194,10 @@ def create_document_routes(
pipeline status. The reprocessed documents retain their original track_id from pipeline status. The reprocessed documents retain their original track_id from
initial upload, so use their original track_id to monitor progress. initial upload, so use their original track_id to monitor progress.
Args:
background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
ReprocessResponse: Response with status and message. ReprocessResponse: Response with status and message.
track_id is always empty string because reprocessed documents retain track_id is always empty string because reprocessed documents retain
@ -3165,9 +3207,9 @@ def create_document_routes(
HTTPException: If an error occurs while initiating reprocessing (500). HTTPException: If an error occurs while initiating reprocessing (500).
""" """
try: try:
# Start the reprocessing in the background # Start the reprocessing in the background (use tenant-specific RAG)
# Note: Reprocessed documents retain their original track_id from initial upload # Note: Reprocessed documents retain their original track_id from initial upload
background_tasks.add_task(rag.apipeline_process_enqueue_documents) background_tasks.add_task(tenant_rag.apipeline_process_enqueue_documents)
logger.info("Reprocessing of failed documents initiated") logger.info("Reprocessing of failed documents initiated")
return ReprocessResponse( return ReprocessResponse(
@ -3185,7 +3227,9 @@ def create_document_routes(
response_model=CancelPipelineResponse, response_model=CancelPipelineResponse,
dependencies=[Depends(combined_auth)], dependencies=[Depends(combined_auth)],
) )
async def cancel_pipeline(): async def cancel_pipeline(
tenant_rag: LightRAG = Depends(get_tenant_rag)
):
""" """
Request cancellation of the currently running pipeline. Request cancellation of the currently running pipeline.
@ -3198,6 +3242,9 @@ def create_document_routes(
The cancellation is graceful and ensures data consistency. Documents that have The cancellation is graceful and ensures data consistency. Documents that have
completed processing will remain in PROCESSED status. completed processing will remain in PROCESSED status.
Args:
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns: Returns:
CancelPipelineResponse: Response with status and message CancelPipelineResponse: Response with status and message
- status="cancellation_requested": Cancellation flag has been set - status="cancellation_requested": Cancellation flag has been set
@ -3213,10 +3260,10 @@ def create_document_routes(
) )
pipeline_status = await get_namespace_data( pipeline_status = await get_namespace_data(
"pipeline_status", workspace=rag.workspace "pipeline_status", workspace=tenant_rag.workspace
) )
pipeline_status_lock = get_namespace_lock( pipeline_status_lock = get_namespace_lock(
"pipeline_status", workspace=rag.workspace "pipeline_status", workspace=tenant_rag.workspace
) )
async with pipeline_status_lock: async with pipeline_status_lock:

View file

@ -23,6 +23,7 @@ import PaginationControls from '@/components/ui/PaginationControls'
import { import {
scanNewDocuments, scanNewDocuments,
getDocumentsPaginated, getDocumentsPaginated,
getPipelineStatus,
DocsStatusesResponse, DocsStatusesResponse,
DocStatus, DocStatus,
DocStatusResponse, DocStatusResponse,
@ -223,6 +224,7 @@ export default function DocumentManager() {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const health = useBackendState.use.health() const health = useBackendState.use.health()
const pipelineBusy = useBackendState.use.pipelineBusy() const pipelineBusy = useBackendState.use.pipelineBusy()
const setPipelineBusy = useBackendState.use.setPipelineBusy()
// Legacy state for backward compatibility // Legacy state for backward compatibility
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null) const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
@ -810,6 +812,18 @@ export default function DocumentManager() {
} }
updateComponentState(response); updateComponentState(response);
// Fetch tenant-specific pipeline status and update global state
// This ensures pipelineBusy reflects the current tenant's pipeline, not global state
try {
const pipelineStatus = await getPipelineStatus();
if (isMountedRef.current) {
setPipelineBusy(pipelineStatus.busy);
}
} catch (pipelineErr) {
// Silently ignore pipeline status fetch errors - not critical
console.warn('[DocumentManager] Failed to fetch pipeline status:', pipelineErr);
}
} catch (err) { } catch (err) {
if (isMountedRef.current) { if (isMountedRef.current) {
const errorClassification = classifyError(err); const errorClassification = classifyError(err);
@ -833,7 +847,7 @@ export default function DocumentManager() {
setIsRefreshing(false); setIsRefreshing(false);
} }
} }
}, [statusFilter, pagination.page, pagination.page_size, sortField, sortDirection, t, updateComponentState, withTimeout, classifyError, recordFailure]); }, [statusFilter, pagination.page, pagination.page_size, sortField, sortDirection, t, updateComponentState, withTimeout, classifyError, recordFailure, setPipelineBusy, isTenantContextReady]);
// New paginated data fetching function // New paginated data fetching function
const fetchPaginatedDocuments = useCallback(async ( const fetchPaginatedDocuments = useCallback(async (

View file

@ -153,7 +153,14 @@
"showButton": "عرض", "showButton": "عرض",
"hideButton": "إخفاء", "hideButton": "إخفاء",
"showFileNameTooltip": "عرض اسم الملف", "showFileNameTooltip": "عرض اسم الملف",
"hideFileNameTooltip": "إخفاء اسم الملف" "hideFileNameTooltip": "إخفاء اسم الملف",
"loading": "جارٍ تحميل المستندات...",
"loadingHint": "قد يستغرق هذا لحظة",
"emptyWithPipelineTitle": "المعالجة قيد التقدم",
"emptyWithPipelineDescription": "يتم معالجة المستندات في خط الأنابيب. يمكنك مسح المستندات الجديدة أو عرض حالة خط الأنابيب.",
"scanForDocuments": "مسح المستندات",
"viewPipeline": "عرض خط الأنابيب",
"emptyHint": "قم بتحميل المستندات باستخدام الزر أعلاه أو امسح المستندات في مجلد الإدخال."
}, },
"pipelineStatus": { "pipelineStatus": {
"title": "حالة خط الأنابيب", "title": "حالة خط الأنابيب",

View file

@ -153,7 +153,14 @@
"showButton": "Show", "showButton": "Show",
"hideButton": "Hide", "hideButton": "Hide",
"showFileNameTooltip": "Show file name", "showFileNameTooltip": "Show file name",
"hideFileNameTooltip": "Hide file name" "hideFileNameTooltip": "Hide file name",
"loading": "Loading documents...",
"loadingHint": "This may take a moment",
"emptyWithPipelineTitle": "Processing in Progress",
"emptyWithPipelineDescription": "Documents are being processed in the pipeline. You can scan for new documents or view the pipeline status.",
"scanForDocuments": "Scan for Documents",
"viewPipeline": "View Pipeline",
"emptyHint": "Upload documents using the button above or scan for documents in the input folder."
}, },
"pipelineStatus": { "pipelineStatus": {
"title": "Pipeline Status", "title": "Pipeline Status",

View file

@ -153,7 +153,14 @@
"showButton": "Afficher", "showButton": "Afficher",
"hideButton": "Masquer", "hideButton": "Masquer",
"showFileNameTooltip": "Afficher le nom du fichier", "showFileNameTooltip": "Afficher le nom du fichier",
"hideFileNameTooltip": "Masquer le nom du fichier" "hideFileNameTooltip": "Masquer le nom du fichier",
"loading": "Chargement des documents...",
"loadingHint": "Cela peut prendre un moment",
"emptyWithPipelineTitle": "Traitement en cours",
"emptyWithPipelineDescription": "Les documents sont en cours de traitement dans le pipeline. Vous pouvez scanner de nouveaux documents ou voir l'état du pipeline.",
"scanForDocuments": "Scanner les documents",
"viewPipeline": "Voir le pipeline",
"emptyHint": "Téléchargez des documents à l'aide du bouton ci-dessus ou scannez les documents dans le dossier d'entrée."
}, },
"pipelineStatus": { "pipelineStatus": {
"title": "État du Pipeline", "title": "État du Pipeline",

View file

@ -153,7 +153,14 @@
"showButton": "显示", "showButton": "显示",
"hideButton": "隐藏", "hideButton": "隐藏",
"showFileNameTooltip": "显示文件名", "showFileNameTooltip": "显示文件名",
"hideFileNameTooltip": "隐藏文件名" "hideFileNameTooltip": "隐藏文件名",
"loading": "正在加载文档...",
"loadingHint": "请稍候",
"emptyWithPipelineTitle": "处理进行中",
"emptyWithPipelineDescription": "文档正在流水线中处理。您可以扫描新文档或查看流水线状态。",
"scanForDocuments": "扫描文档",
"viewPipeline": "查看流水线",
"emptyHint": "使用上方按钮上传文档,或扫描输入文件夹中的文档。"
}, },
"pipelineStatus": { "pipelineStatus": {
"title": "流水线状态", "title": "流水线状态",

View file

@ -153,7 +153,14 @@
"showButton": "顯示", "showButton": "顯示",
"hideButton": "隱藏", "hideButton": "隱藏",
"showFileNameTooltip": "顯示檔案名稱", "showFileNameTooltip": "顯示檔案名稱",
"hideFileNameTooltip": "隱藏檔案名稱" "hideFileNameTooltip": "隱藏檔案名稱",
"loading": "正在載入文件...",
"loadingHint": "請稍候",
"emptyWithPipelineTitle": "處理進行中",
"emptyWithPipelineDescription": "文件正在流水線中處理。您可以掃描新文件或查看流水線狀態。",
"scanForDocuments": "掃描文件",
"viewPipeline": "查看流水線",
"emptyHint": "使用上方按鈕上傳文件,或掃描輸入資料夾中的文件。"
}, },
"pipelineStatus": { "pipelineStatus": {
"title": "流水線狀態", "title": "流水線狀態",

View file

@ -0,0 +1,21 @@
# Task Log: Translation Keys Fix
**Date:** 2025-01-25 16:35
**Mode:** beastmode
## Actions
- Added 7 missing translation keys to all 5 locale files (en, zh, zh_TW, fr, ar)
- Keys added: `loading`, `loadingHint`, `emptyWithPipelineTitle`, `emptyWithPipelineDescription`, `scanForDocuments`, `viewPipeline`, `emptyHint`
- Validated JSON syntax for all translation files
## Decisions
- Used contextually appropriate translations for each language
- Added translations to documentPanel.documentManager namespace to match existing structure
## Next Steps
- Refresh the browser to see the translated text instead of raw keys
- Verify all translation keys display correctly in Documents panel
## Lessons/Insights
- Translation keys were referenced in DocumentManager.tsx but never added to locale JSON files
- i18n fallback returns the key itself when translation is missing, causing the display issue

View file

@ -0,0 +1,28 @@
# Task Log: Fix Pipeline Status Multi-Tenant Mismatch
**Date:** 2025-01-25 16:45
**Mode:** beastmode
## Problem
The Documents panel was stuck showing "Processing in Progress" even when the tenant-specific pipeline was not busy. This happened because:
1. The global `/health` endpoint returns `pipeline_busy` from global namespace (not tenant-aware)
2. The `/documents/pipeline_status` endpoint returns tenant-specific pipeline status
3. The UI used the global `pipelineBusy` state from health endpoint to decide which empty state to show
## Actions
1. Added `getPipelineStatus` import to DocumentManager.tsx
2. Added `setPipelineBusy` hook to update global pipeline state
3. Modified `handleIntelligentRefresh` to fetch tenant-specific pipeline status after documents load
4. Updated dependency array to include `setPipelineBusy` and `isTenantContextReady`
## Decisions
- Chose to update global `pipelineBusy` state from DocumentManager rather than modifying the health endpoint
- This approach keeps the health endpoint simple and makes the Documents panel the source of truth for tenant-specific pipeline state
## Next Steps
- Refresh browser and verify Documents panel shows correct empty state
- When switching tenants/KBs, the pipeline status should update correctly
## Lessons/Insights
- In multi-tenant mode, global status endpoints (like /health) don't reflect tenant-specific state
- Components that need tenant-specific data should fetch it directly from tenant-aware endpoints

View file

@ -0,0 +1,43 @@
# Task Log: Multi-Tenant Document Routes Fix
**Date:** 2025-01-25 17:00
**Mode:** beastmode
## Problem
Document upload, scan, and other document operations were using the **global** RAG instance instead of the **tenant-specific** RAG instance. This caused:
1. Documents uploaded in one tenant's KB being stored in the global namespace
2. Pipeline status being read from global namespace instead of tenant-specific
3. Document count in header showing wrong number ("0 docs")
## Root Cause
In `lightrag/api/routers/document_routes.py`, many endpoints were using the closure-captured `rag` variable (global) instead of the `get_tenant_rag` dependency that provides tenant-specific RAG instances.
## Actions
Fixed the following endpoints to use `tenant_rag: LightRAG = Depends(get_tenant_rag)`:
1. **POST /documents/upload** - File upload now uses tenant-specific RAG
2. **POST /documents/scan** - Directory scan now uses tenant-specific RAG
3. **POST /documents/text** - Text insertion now uses tenant-specific RAG
4. **POST /documents/texts** - Batch text insertion now uses tenant-specific RAG
5. **DELETE /documents** (clear_documents) - Clear all now uses tenant-specific RAG
6. **DELETE /documents/delete** - Delete by ID now uses tenant-specific RAG
7. **POST /documents/clear_cache** - Cache clearing now uses tenant-specific RAG
8. **DELETE /documents/delete_entity** - Entity deletion now uses tenant-specific RAG
9. **DELETE /documents/delete_relation** - Relation deletion now uses tenant-specific RAG
10. **POST /documents/reprocess_failed** - Reprocess now uses tenant-specific RAG
11. **POST /documents/cancel_pipeline** - Pipeline cancel now uses tenant-specific RAG
## Decisions
- Used existing `get_tenant_rag` dependency pattern for consistency
- All document operations now properly scope to tenant/KB context from headers
## Next Steps
1. Restart backend server to apply changes
2. Test document upload in multi-tenant mode
3. Verify pipeline processes documents in correct tenant namespace
4. Verify document count in header updates correctly
## Lessons/Insights
- Multi-tenant systems require careful auditing of ALL endpoints to ensure proper tenant isolation
- Using dependency injection patterns (like `Depends(get_tenant_rag)`) makes it cleaner to manage tenant context
- Background tasks must receive the correct RAG instance at task creation time

View file

@ -1,6 +1,6 @@
# Task Log: Multi-Tenant Filtering & API Tab Fix # Task Log: Multi-Tenant Filtering & API Fixes
**Date:** 2025-01-27 12:30 **Date:** 2025-01-27 12:45
**Mode:** beastmode **Mode:** beastmode
## Todo List Status ## Todo List Status
@ -10,30 +10,46 @@
- [x] Step 3: Query/Retrieval routes multi-tenant filtering (3 endpoints updated) - [x] Step 3: Query/Retrieval routes multi-tenant filtering (3 endpoints updated)
- [x] Step 4: Update lightrag_server.py to pass rag_manager to all routes - [x] Step 4: Update lightrag_server.py to pass rag_manager to all routes
- [x] Step 5: Fix API tab visibility - Add /static proxy for Swagger UI assets - [x] Step 5: Fix API tab visibility - Add /static proxy for Swagger UI assets
- [ ] Step 6: Restart Vite dev server to apply proxy configuration change (user action required) - [x] Step 6: Fix "Network connection error" - Allow unauthenticated access when auth disabled
- [x] Step 7: Restart backend server to apply changes
## Actions ## Actions
- Updated `graph_routes.py`: Added `get_tenant_rag` dependency to all 10 graph endpoints - Updated `graph_routes.py`: Added `get_tenant_rag` dependency to all 10 graph endpoints
- Updated `query_routes.py`: Added `get_tenant_rag` dependency to 3 query endpoints (`/query`, `/query/stream`, `/query/data`) - Updated `query_routes.py`: Added `get_tenant_rag` dependency to 3 query endpoints
- Updated `lightrag_server.py`: Pass `rag_manager` to `create_graph_routes()` and `create_query_routes()` - Updated `lightrag_server.py`: Pass `rag_manager` to `create_graph_routes()` and `create_query_routes()`
- Updated `vite.config.ts`: Added `/static` proxy to fix Swagger UI asset loading - Updated `vite.config.ts`: Added `/static` proxy to fix Swagger UI asset loading
- Updated `dependencies.py`: Fixed `get_tenant_context` to allow unauthenticated access when `auth_configured=False` and `api_key_configured=False`
- Restarted backend server to apply the authentication fix
## Root Cause Analysis
### Issue 1: API Tab White/Blank
- **Cause**: Swagger UI loads from `/docs` but assets come from `/static/swagger-ui/*`
- **Problem**: Vite proxy wasn't configured for `/static` path
- **Fix**: Added `/static` proxy in vite.config.ts
### Issue 2: Network Connection Error on Retrieval
- **Cause**: `get_tenant_context` in `dependencies.py` required authentication even when auth was disabled
- **Problem**: When frontend sends `X-Tenant-ID` header, `get_tenant_context_optional` calls `get_tenant_context` directly, which always requires auth
- **Fix**: Added check for `auth_configured` and `api_key_configured` in `get_tenant_context` - if both are False, allow guest access
## Decisions ## Decisions
- Used same multi-tenant pattern across all routes: `get_tenant_rag` dependency returns tenant-specific LightRAG instance - Used same multi-tenant pattern across all routes: `get_tenant_rag` dependency returns tenant-specific LightRAG instance
- API tab fix: Added `/static` proxy rather than changing Swagger UI configuration - For auth fix: Added guest user with "viewer" role when no auth is configured
## Next Steps ## Next Steps
- Restart Vite dev server (Ctrl+C and `bun run dev`) to apply proxy configuration change - Test all screens with tenant/KB switching to verify data isolation
- Test API tab now shows Swagger UI - Verify API tab displays Swagger UI correctly
- Test graph/retrieval operations filter by KB when switching knowledgebases - Test retrieval queries work without authentication errors
## Lessons/Insights ## Lessons/Insights
- Swagger UI loads from `/docs` but assets come from `/static/swagger-ui/*` - both paths need proxying - When `X-Tenant-ID` is provided in headers, `get_tenant_context_optional` propagates errors instead of falling back to None
- Vite's `base: '/webui/'` setting redirects non-proxied paths, causing 404s for Swagger assets - The auth logic in `dependencies.py` was inconsistent with `utils_api.py` - both now respect "no auth required" mode
- Proxy configuration changes require dev server restart to take effect - Swagger UI loads static assets from a separate path (`/static/`) that needs explicit proxy configuration
## Files Modified ## Files Modified
1. `lightrag/api/routers/graph_routes.py` - Multi-tenant support for all graph endpoints 1. `lightrag/api/routers/graph_routes.py` - Multi-tenant support for all graph endpoints
2. `lightrag/api/routers/query_routes.py` - Multi-tenant support for all query endpoints 2. `lightrag/api/routers/query_routes.py` - Multi-tenant support for all query endpoints
3. `lightrag/api/lightrag_server.py` - Pass rag_manager to graph and query route creators 3. `lightrag/api/lightrag_server.py` - Pass rag_manager to graph and query route creators
4. `lightrag_webui/vite.config.ts` - Added `/static` proxy for Swagger UI assets 4. `lightrag/api/dependencies.py` - Allow unauthenticated access when auth is disabled
5. `lightrag_webui/vite.config.ts` - Added `/static` proxy for Swagger UI assets