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:
- Authorization header with JWT token containing tenant_id
- 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-KB-ID header (optional, if not in token)
@ -189,9 +190,13 @@ async def get_tenant_context(
role_str = "viewer"
metadata = {}
# Check API Key first
# Check if authentication is configured
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"
role_str = "admin"
elif authorization:
@ -217,6 +222,10 @@ async def get_tenant_context(
username = token_data.get("username")
metadata = token_data.get("metadata", {})
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:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View file

@ -2081,7 +2081,10 @@ def create_document_routes(
@router.post(
"/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.
@ -2089,14 +2092,18 @@ def create_document_routes(
and processes them. If a scanning process is already running, it returns a status indicating
that fact.
Args:
background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
ScanResponse: A response object containing the scanning status and track_id
"""
# Generate track_id with "scan" prefix for scanning operation
track_id = generate_track_id("scan")
# Start the scanning process in the background with track_id
background_tasks.add_task(run_scanning_process, rag, doc_manager, track_id)
# Start the scanning process in the background with track_id (use tenant-specific RAG)
background_tasks.add_task(run_scanning_process, tenant_rag, doc_manager, track_id)
return ScanResponse(
status="scanning_started",
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)]
)
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.
@ -2119,6 +2128,7 @@ def create_document_routes(
Args:
background_tasks: FastAPI BackgroundTasks for async processing
file (UploadFile): The file to be uploaded. It must have an allowed extension.
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
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}",
)
# Check if filename already exists in doc_status storage
existing_doc_data = await rag.doc_status.get_doc_by_file_path(safe_filename)
# Check if filename already exists in doc_status storage (tenant-specific)
existing_doc_data = await tenant_rag.doc_status.get_doc_by_file_path(safe_filename)
if existing_doc_data:
# Get document status and track_id from existing document
status = existing_doc_data.get("status", "unknown")
@ -2164,8 +2174,8 @@ def create_document_routes(
track_id = generate_track_id("upload")
# Add to background tasks and get track_id
background_tasks.add_task(pipeline_index_file, rag, file_path, track_id)
# Add to background tasks and get track_id (use tenant-specific RAG)
background_tasks.add_task(pipeline_index_file, tenant_rag, file_path, track_id)
return InsertResponse(
status="success",
@ -2182,7 +2192,9 @@ def create_document_routes(
"/text", response_model=InsertResponse, dependencies=[Depends(combined_auth)]
)
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.
@ -2193,6 +2205,7 @@ def create_document_routes(
Args:
request (InsertTextRequest): The request body containing the text to be inserted.
background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
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).
"""
try:
# Check if file_source already exists in doc_status storage
# Check if file_source already exists in doc_status storage (tenant-specific)
if (
request.file_source
and request.file_source.strip()
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
)
if existing_doc_data:
@ -2221,10 +2234,10 @@ def create_document_routes(
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)
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:
# Content already exists, return duplicated with existing track_id
status = existing_doc.get("status", "unknown")
@ -2240,7 +2253,7 @@ def create_document_routes(
background_tasks.add_task(
pipeline_index_texts,
rag,
tenant_rag,
[request.text],
file_sources=[request.file_source],
track_id=track_id,
@ -2262,7 +2275,9 @@ def create_document_routes(
dependencies=[Depends(combined_auth)],
)
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.
@ -2273,6 +2288,7 @@ def create_document_routes(
Args:
request (InsertTextsRequest): The request body containing the list of texts.
background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
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).
"""
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:
for file_source in request.file_sources:
if (
@ -2289,7 +2305,7 @@ def create_document_routes(
and file_source.strip()
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
)
if existing_doc_data:
@ -2303,11 +2319,11 @@ def create_document_routes(
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:
sanitized_text = sanitize_text_for_encoding(text)
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:
# Content already exists, return duplicated with existing track_id
status = existing_doc.get("status", "unknown")
@ -2323,7 +2339,7 @@ def create_document_routes(
background_tasks.add_task(
pipeline_index_texts,
rag,
tenant_rag,
request.texts,
file_sources=request.file_sources,
track_id=track_id,
@ -2342,7 +2358,9 @@ def create_document_routes(
@router.delete(
"", 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.
@ -2350,6 +2368,9 @@ def create_document_routes(
It uses the storage drop methods to properly clean up all data and removes all files
from the input directory.
Args:
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
ClearDocumentsResponse: A response object containing the status and message.
- status="success": All documents and files were successfully cleared.
@ -2368,12 +2389,12 @@ def create_document_routes(
get_namespace_lock,
)
# Get pipeline status and lock
# Get pipeline status and lock (tenant-specific)
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", workspace=rag.workspace
"pipeline_status", workspace=tenant_rag.workspace
)
# Check and set status with lock
@ -2403,20 +2424,20 @@ def create_document_routes(
)
try:
# Use drop method to clear all data
# Use drop method to clear all data (tenant-specific)
drop_tasks = []
storages = [
rag.text_chunks,
rag.full_docs,
rag.full_entities,
rag.full_relations,
rag.entity_chunks,
rag.relation_chunks,
rag.entities_vdb,
rag.relationships_vdb,
rag.chunks_vdb,
rag.chunk_entity_relation_graph,
rag.doc_status,
tenant_rag.text_chunks,
tenant_rag.full_docs,
tenant_rag.full_entities,
tenant_rag.full_relations,
tenant_rag.entity_chunks,
tenant_rag.relation_chunks,
tenant_rag.entities_vdb,
tenant_rag.relationships_vdb,
tenant_rag.chunks_vdb,
tenant_rag.chunk_entity_relation_graph,
tenant_rag.doc_status,
]
# Log storage drop start
@ -2760,6 +2781,7 @@ def create_document_routes(
async def delete_document(
delete_request: DeleteDocRequest,
background_tasks: BackgroundTasks,
tenant_rag: LightRAG = Depends(get_tenant_rag),
) -> DeleteDocByIdResponse:
"""
Delete documents and all their associated data by their IDs using background processing.
@ -2774,6 +2796,7 @@ def create_document_routes(
Args:
delete_request (DeleteDocRequest): The request containing the document IDs and deletion options.
background_tasks: FastAPI BackgroundTasks for async processing
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
DeleteDocByIdResponse: The result of the deletion operation.
@ -2793,10 +2816,10 @@ def create_document_routes(
)
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", workspace=rag.workspace
"pipeline_status", workspace=tenant_rag.workspace
)
# Check if pipeline is busy with proper lock
@ -2808,10 +2831,10 @@ def create_document_routes(
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_delete_documents,
rag,
tenant_rag,
doc_manager,
doc_ids,
delete_request.delete_file,
@ -2835,7 +2858,10 @@ def create_document_routes(
response_model=ClearCacheResponse,
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.
@ -2844,6 +2870,7 @@ def create_document_routes(
Args:
request (ClearCacheRequest): The request body (ignored for compatibility).
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
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).
"""
try:
# Call the aclear_cache method (no modes parameter)
await rag.aclear_cache()
# Call the aclear_cache method (no modes parameter) - tenant-specific
await tenant_rag.aclear_cache()
# Prepare success message
message = "Successfully cleared all cache"
@ -2869,12 +2896,16 @@ def create_document_routes(
response_model=DeletionResult,
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.
Args:
request (DeleteEntityRequest): The request body containing the entity name.
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
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).
"""
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":
raise HTTPException(status_code=404, detail=result.message)
if result.status == "fail":
@ -2904,12 +2935,16 @@ def create_document_routes(
response_model=DeletionResult,
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.
Args:
request (DeleteRelationRequest): The request body containing the source and target entity names.
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
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).
"""
try:
result = await rag.adelete_by_relation(
result = await tenant_rag.adelete_by_relation(
source_entity=request.source_entity,
target_entity=request.target_entity,
)
@ -3139,7 +3174,10 @@ def create_document_routes(
response_model=ReprocessResponse,
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.
@ -3156,6 +3194,10 @@ def create_document_routes(
pipeline status. The reprocessed documents retain their original track_id from
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:
ReprocessResponse: Response with status and message.
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).
"""
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
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")
return ReprocessResponse(
@ -3185,7 +3227,9 @@ def create_document_routes(
response_model=CancelPipelineResponse,
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.
@ -3198,6 +3242,9 @@ def create_document_routes(
The cancellation is graceful and ensures data consistency. Documents that have
completed processing will remain in PROCESSED status.
Args:
tenant_rag: Tenant-specific RAG instance (injected dependency)
Returns:
CancelPipelineResponse: Response with status and message
- status="cancellation_requested": Cancellation flag has been set
@ -3213,10 +3260,10 @@ def create_document_routes(
)
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", workspace=rag.workspace
"pipeline_status", workspace=tenant_rag.workspace
)
async with pipeline_status_lock:

View file

@ -23,6 +23,7 @@ import PaginationControls from '@/components/ui/PaginationControls'
import {
scanNewDocuments,
getDocumentsPaginated,
getPipelineStatus,
DocsStatusesResponse,
DocStatus,
DocStatusResponse,
@ -223,6 +224,7 @@ export default function DocumentManager() {
const { t, i18n } = useTranslation()
const health = useBackendState.use.health()
const pipelineBusy = useBackendState.use.pipelineBusy()
const setPipelineBusy = useBackendState.use.setPipelineBusy()
// Legacy state for backward compatibility
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
@ -810,6 +812,18 @@ export default function DocumentManager() {
}
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) {
if (isMountedRef.current) {
const errorClassification = classifyError(err);
@ -833,7 +847,7 @@ export default function DocumentManager() {
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
const fetchPaginatedDocuments = useCallback(async (

View file

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

View file

@ -153,7 +153,14 @@
"showButton": "Show",
"hideButton": "Hide",
"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": {
"title": "Pipeline Status",

View file

@ -153,7 +153,14 @@
"showButton": "Afficher",
"hideButton": "Masquer",
"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": {
"title": "État du Pipeline",

View file

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

View file

@ -153,7 +153,14 @@
"showButton": "顯示",
"hideButton": "隱藏",
"showFileNameTooltip": "顯示檔案名稱",
"hideFileNameTooltip": "隱藏檔案名稱"
"hideFileNameTooltip": "隱藏檔案名稱",
"loading": "正在載入文件...",
"loadingHint": "請稍候",
"emptyWithPipelineTitle": "處理進行中",
"emptyWithPipelineDescription": "文件正在流水線中處理。您可以掃描新文件或查看流水線狀態。",
"scanForDocuments": "掃描文件",
"viewPipeline": "查看流水線",
"emptyHint": "使用上方按鈕上傳文件,或掃描輸入資料夾中的文件。"
},
"pipelineStatus": {
"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
## Todo List Status
@ -10,30 +10,46 @@
- [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 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
- 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 `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
- 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
- Restart Vite dev server (Ctrl+C and `bun run dev`) to apply proxy configuration change
- Test API tab now shows Swagger UI
- Test graph/retrieval operations filter by KB when switching knowledgebases
- Test all screens with tenant/KB switching to verify data isolation
- Verify API tab displays Swagger UI correctly
- Test retrieval queries work without authentication errors
## Lessons/Insights
- Swagger UI loads from `/docs` but assets come from `/static/swagger-ui/*` - both paths need proxying
- Vite's `base: '/webui/'` setting redirects non-proxied paths, causing 404s for Swagger assets
- Proxy configuration changes require dev server restart to take effect
- When `X-Tenant-ID` is provided in headers, `get_tenant_context_optional` propagates errors instead of falling back to None
- The auth logic in `dependencies.py` was inconsistent with `utils_api.py` - both now respect "no auth required" mode
- Swagger UI loads static assets from a separate path (`/static/`) that needs explicit proxy configuration
## Files Modified
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
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