Compare commits

...
Sign in to create a new pull request.

26 commits

Author SHA1 Message Date
hzywhite
9bc5f1578c
Merge branch 'main' into RAGAnything 2025-09-16 17:52:58 +08:00
hzywhite
680b7c5b89 merge 2025-09-16 14:32:36 +08:00
hzywhite
b9f7e1426c merge 2025-09-10 13:55:24 +08:00
hzywhite
fb3dd9dbb3 merge 2025-09-10 13:52:41 +08:00
hzywhite
d0709d5416 merge 2025-09-10 11:23:33 +08:00
hzywhite
12028a32c4
Merge branch 'main' into RAGAnything 2025-09-08 10:03:05 +08:00
hzywhite
173baf96b9 Summary 2025-09-08 09:55:35 +08:00
hzywhite
0dc11e0794 summary 2025-09-05 19:31:00 +08:00
hzywhite
8620ce0b01 Update __init__.py 2025-09-05 17:38:34 +08:00
hzywhite
8bd8888506 summary 2025-09-05 16:18:17 +08:00
hzywhite
8d4ef251c7 summary 2025-09-05 16:17:39 +08:00
hzywhite
27845023e6 summary 2025-09-05 16:17:20 +08:00
hzywhite
a33484bdb7 merge 2025-09-05 15:04:34 +08:00
hzywhite
e07d4bb70b merge 2025-09-05 15:04:04 +08:00
hzywhite
482a09d397 merge 2025-09-05 15:03:19 +08:00
hzywhite
8d800239d6 merge 2025-09-05 15:02:49 +08:00
hzywhite
e3ea87da24 merge 2025-09-05 15:01:50 +08:00
hzywhite
2a453fbe37 webui 2025-09-04 11:24:06 +08:00
hzywhite
7c8db78057 merge 2025-09-04 11:05:22 +08:00
hzywhite
82a0f8cc1f merge 2025-09-04 10:57:41 +08:00
hzywhite
e27031587d merge 2025-09-04 10:27:38 +08:00
hzywhite
bd533783e1 Update document_routes.py 2025-09-02 06:51:32 +08:00
hzywhite
cb003593df Update document_routes.py 2025-09-02 06:50:12 +08:00
hzywhite
745aa085db summary 2025-09-02 06:21:08 +08:00
hzywhite
36c81039b1 Summary 2025-09-02 06:15:29 +08:00
hzywhite
d8b2264d8b summary 2025-09-02 03:54:20 +08:00
71 changed files with 5119 additions and 70 deletions

View file

@ -25,6 +25,7 @@ from lightrag.api.utils_api import (
display_splash_screen, display_splash_screen,
check_env_file, check_env_file,
) )
from lightrag.llm.openai import openai_complete_if_cache, openai_embed
from .config import ( from .config import (
global_args, global_args,
update_uvicorn_mode_config, update_uvicorn_mode_config,
@ -61,6 +62,8 @@ from lightrag.kg.shared_storage import (
) )
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from lightrag.api.auth import auth_handler from lightrag.api.auth import auth_handler
from lightrag.ragmanager import RAGManager
from raganything import RAGAnything, RAGAnythingConfig
# use the .env that is inside the current folder # use the .env that is inside the current folder
# allows to use different .env file for each lightrag instance # allows to use different .env file for each lightrag instance
@ -619,10 +622,147 @@ def create_app(args):
logger.error(f"Failed to initialize LightRAG: {e}") logger.error(f"Failed to initialize LightRAG: {e}")
raise raise
# Initialize RAGAnything with comprehensive error handling
rag_anything = None
raganything_enabled = False
raganything_error_message = None
try:
api_key = get_env_value("LLM_BINDING_API_KEY", "", str)
base_url = get_env_value("LLM_BINDING_HOST", "", str)
# Validate required configuration
if not api_key:
raise ValueError(
"LLM_BINDING_API_KEY is required for RAGAnything functionality"
)
if not base_url:
raise ValueError(
"LLM_BINDING_HOST is required for RAGAnything functionality"
)
config = RAGAnythingConfig(
working_dir=args.working_dir or "./rag_storage",
parser="mineru", # Parser selection: mineru or docling
parse_method="auto", # Parse method: auto, ocr, or txt
enable_image_processing=True,
enable_table_processing=True,
enable_equation_processing=True,
)
# Define LLM model function
def llm_model_func(prompt, system_prompt=None, history_messages=[], **kwargs):
return openai_complete_if_cache(
"gpt-4o-mini",
prompt,
system_prompt=system_prompt,
history_messages=history_messages,
api_key=api_key,
base_url=base_url,
**kwargs,
)
# Define vision model function for image processing
def vision_model_func(
prompt, system_prompt=None, history_messages=[], image_data=None, **kwargs
):
if image_data:
return openai_complete_if_cache(
"gpt-4o",
"",
system_prompt=None,
history_messages=[],
messages=[
{"role": "system", "content": system_prompt}
if system_prompt
else None,
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_data}"
},
},
],
}
if image_data
else {"role": "user", "content": prompt},
],
api_key=api_key,
base_url=base_url,
**kwargs,
)
else:
return llm_model_func(prompt, system_prompt, history_messages, **kwargs)
# Define embedding function
raganything_embedding_func = EmbeddingFunc(
embedding_dim=3072,
max_token_size=8192,
func=lambda texts: openai_embed(
texts,
model="text-embedding-3-large",
api_key=api_key,
base_url=base_url,
),
)
# Initialize RAGAnything with new dataclass structure
logger.info("Initializing RAGAnything functionality...")
rag_anything = RAGAnything(
lightrag=rag,
config=config,
llm_model_func=llm_model_func,
vision_model_func=vision_model_func,
embedding_func=raganything_embedding_func,
)
logger.info("Check the download status of the RAGAnything parser...")
rag_anything.verify_parser_installation_once()
RAGManager.set_rag(rag_anything)
raganything_enabled = True
logger.info(
"The RAGAnything feature has been successfully enabled, supporting multimodal document processing functionality"
)
except ImportError as e:
raganything_error_message = (
f"RAGAnything dependency package not installed: {str(e)}"
)
logger.warning(f"{raganything_error_message}")
logger.info(
"Please run 'pip install raganything' to install dependency packages to enable multimodal document processing functionality"
)
except ValueError as e:
raganything_error_message = f"RAGAnything configuration error: {str(e)}"
logger.warning(f"{raganything_error_message}")
logger.info(
"Please check if the environment variables LLM-BINDING_API_KEY and LLM-BINDING_HOST are set correctly"
)
except Exception as e:
raganything_error_message = f"RAGAnything initialization failed: {str(e)}"
logger.error(f" {raganything_error_message}")
logger.info(
"The system will run in basic mode and only support standard document processing functions"
)
except Exception as e:
logger.error(f"Failed to initialize LightRAG: {e}")
raise
if not raganything_enabled:
logger.info(
"The system has been downgraded to basic mode, but LightRAG core functions are still available"
)
# Add routes # Add routes
app.include_router( app.include_router(
create_document_routes( create_document_routes(
rag, rag,
rag_anything,
doc_manager, doc_manager,
api_key, api_key,
) )

View file

@ -3,6 +3,8 @@ This module contains all document-related routes for the LightRAG API.
""" """
import asyncio import asyncio
import json
import uuid
from lightrag.utils import logger, get_pinyin_sort_key from lightrag.utils import logger, get_pinyin_sort_key
import aiofiles import aiofiles
import shutil import shutil
@ -18,6 +20,7 @@ from fastapi import (
File, File,
HTTPException, HTTPException,
UploadFile, UploadFile,
Form,
) )
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
@ -26,6 +29,7 @@ from lightrag.base import DeletionResult, DocProcessingStatus, DocStatus
from lightrag.utils import generate_track_id from lightrag.utils import generate_track_id
from lightrag.api.utils_api import get_combined_auth_dependency from lightrag.api.utils_api import get_combined_auth_dependency
from ..config import global_args from ..config import global_args
from raganything import RAGAnything
# Function to format datetime to ISO format string with timezone information # Function to format datetime to ISO format string with timezone information
@ -107,6 +111,81 @@ def sanitize_filename(filename: str, input_dir: Path) -> str:
return clean_name return clean_name
class SchemeConfig(BaseModel):
"""Configuration model for processing schemes.
Defines the processing framework and optional extractor to use for document processing.
Attributes:
framework (Literal['lightrag', 'raganything']): Processing framework to use.
- "lightrag": Standard LightRAG processing for text-based documents
- "raganything": Advanced multimodal processing with image/table/equation support
extractor (Literal['mineru', 'docling', '']): Document extraction tool to use.
- "mineru": MinerU parser for comprehensive document parsing
- "docling": Docling parser for office document processing
- "": Default/automatic extractor selection
modelSource (Literal["huggingface", "modelscope", "local", ""]): The model source used by Mineru.
- "huggingface": Using pre-trained models from the Hugging Face model library
- "modelscope": using model resources on ModelScope platform
- "local": Use custom models deployed locally
- "":Maintain the default model source configuration of the system (usually huggingface)
"""
framework: Literal["lightrag", "raganything"]
extractor: Literal["mineru", "docling", ""] = "" # 默认值
modelSource: Literal["huggingface", "modelscope", "local", ""] = ""
class Scheme(BaseModel):
"""Base model for processing schemes.
Attributes:
name (str): Human-readable name for the processing scheme
config (SchemeConfig): Configuration settings for the scheme
"""
name: str
config: SchemeConfig
class Scheme_include_id(Scheme):
"""Scheme model with unique identifier included.
Extends the base Scheme model to include a unique ID field for
identification and management operations.
Attributes:
id (int): Unique identifier for the scheme
name (str): Inherited from Scheme
config (SchemeConfig): Inherited from Scheme
"""
id: int
class SchemesResponse(BaseModel):
"""Response model for scheme management operations.
Used for all scheme-related endpoints to provide consistent response format
for scheme retrieval, creation, update, and deletion operations.
Attributes:
status (str): Operation status ("success", "error")
message (Optional[str]): Additional message with operation details
data (Optional[List[Dict[str, Any]]]): List of scheme objects when retrieving schemes
"""
status: str = Field(..., description="Operation status")
message: Optional[str] = Field(None, description="Additional message")
data: Optional[List[Dict[str, Any]]] = Field(None, description="List of schemes")
class ScanRequest(BaseModel):
"""Request model for document scanning operations."""
schemeConfig: SchemeConfig = Field(..., description="Scanning scheme configuration")
class ScanResponse(BaseModel): class ScanResponse(BaseModel):
"""Response model for document scanning operation """Response model for document scanning operation
@ -372,12 +451,20 @@ class DocStatusResponse(BaseModel):
default=None, description="Additional metadata about the document" default=None, description="Additional metadata about the document"
) )
file_path: str = Field(description="Path to the document file") file_path: str = Field(description="Path to the document file")
scheme_name: str = Field(
default=None, description="Name of the processing scheme used for this document"
)
multimodal_content: Optional[list[dict[str, Any]]] = Field(
default=None, description="Multimodal content of the document"
)
class Config: class Config:
json_schema_extra = { json_schema_extra = {
"example": { "example": {
"id": "doc_123456", "id": "doc_123456",
"content_summary": "Research paper on machine learning", "content_summary": "Research paper on machine learning",
"scheme_name": "lightrag",
"multimodal_content": [],
"content_length": 15240, "content_length": 15240,
"status": "PROCESSED", "status": "PROCESSED",
"created_at": "2025-03-31T12:34:56", "created_at": "2025-03-31T12:34:56",
@ -411,6 +498,8 @@ class DocsStatusesResponse(BaseModel):
{ {
"id": "doc_123", "id": "doc_123",
"content_summary": "Pending document", "content_summary": "Pending document",
"scheme_name": "lightrag",
"multimodal_content": [],
"content_length": 5000, "content_length": 5000,
"status": "PENDING", "status": "PENDING",
"created_at": "2025-03-31T10:00:00", "created_at": "2025-03-31T10:00:00",
@ -426,6 +515,8 @@ class DocsStatusesResponse(BaseModel):
{ {
"id": "doc_456", "id": "doc_456",
"content_summary": "Processed document", "content_summary": "Processed document",
"scheme_name": "lightrag",
"multimodal_content": [],
"content_length": 8000, "content_length": 8000,
"status": "PROCESSED", "status": "PROCESSED",
"created_at": "2025-03-31T09:00:00", "created_at": "2025-03-31T09:00:00",
@ -779,7 +870,7 @@ def get_unique_filename_in_enqueued(target_dir: Path, original_name: str) -> str
async def pipeline_enqueue_file( async def pipeline_enqueue_file(
rag: LightRAG, file_path: Path, track_id: str = None rag: LightRAG, file_path: Path, track_id: str = None, scheme_name: str = None
) -> tuple[bool, str]: ) -> tuple[bool, str]:
"""Add a file to the queue for processing """Add a file to the queue for processing
@ -787,6 +878,8 @@ async def pipeline_enqueue_file(
rag: LightRAG instance rag: LightRAG instance
file_path: Path to the saved file file_path: Path to the saved file
track_id: Optional tracking ID, if not provided will be generated track_id: Optional tracking ID, if not provided will be generated
scheme_name (str, optional): Processing scheme name for categorization.
Defaults to None
Returns: Returns:
tuple: (success: bool, track_id: str) tuple: (success: bool, track_id: str)
""" """
@ -1159,7 +1252,10 @@ async def pipeline_enqueue_file(
try: try:
await rag.apipeline_enqueue_documents( await rag.apipeline_enqueue_documents(
content, file_paths=file_path.name, track_id=track_id content,
file_paths=file_path.name,
track_id=track_id,
scheme_name=scheme_name,
) )
logger.info( logger.info(
@ -1243,17 +1339,21 @@ async def pipeline_enqueue_file(
logger.error(f"Error deleting file {file_path}: {str(e)}") logger.error(f"Error deleting file {file_path}: {str(e)}")
async def pipeline_index_file(rag: LightRAG, file_path: Path, track_id: str = None): async def pipeline_index_file(
rag: LightRAG, file_path: Path, track_id: str = None, scheme_name: str = None
):
"""Index a file with track_id """Index a file with track_id
Args: Args:
rag: LightRAG instance rag: LightRAG instance
file_path: Path to the saved file file_path: Path to the saved file
track_id: Optional tracking ID track_id: Optional tracking ID
scheme_name (str, optional): Processing scheme name for categorization.
Defaults to None
""" """
try: try:
success, returned_track_id = await pipeline_enqueue_file( success, returned_track_id = await pipeline_enqueue_file(
rag, file_path, track_id rag, file_path, track_id, scheme_name
) )
if success: if success:
await rag.apipeline_process_enqueue_documents() await rag.apipeline_process_enqueue_documents()
@ -1264,7 +1364,7 @@ async def pipeline_index_file(rag: LightRAG, file_path: Path, track_id: str = No
async def pipeline_index_files( async def pipeline_index_files(
rag: LightRAG, file_paths: List[Path], track_id: str = None rag: LightRAG, file_paths: List[Path], track_id: str = None, scheme_name: str = None
): ):
"""Index multiple files sequentially to avoid high CPU load """Index multiple files sequentially to avoid high CPU load
@ -1272,6 +1372,8 @@ async def pipeline_index_files(
rag: LightRAG instance rag: LightRAG instance
file_paths: Paths to the files to index file_paths: Paths to the files to index
track_id: Optional tracking ID to pass to all files track_id: Optional tracking ID to pass to all files
scheme_name (str, optional): Processing scheme name for categorization.
Defaults to None
""" """
if not file_paths: if not file_paths:
return return
@ -1285,7 +1387,9 @@ async def pipeline_index_files(
# Process files sequentially with track_id # Process files sequentially with track_id
for file_path in sorted_file_paths: for file_path in sorted_file_paths:
success, _ = await pipeline_enqueue_file(rag, file_path, track_id) success, _ = await pipeline_enqueue_file(
rag, file_path, track_id, scheme_name
)
if success: if success:
enqueued = True enqueued = True
@ -1297,6 +1401,61 @@ async def pipeline_index_files(
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
async def pipeline_index_files_raganything(
rag_anything: RAGAnything,
file_paths: List[Path],
scheme_name: str = None,
parser: str = None,
source: str = None,
):
"""Index multiple files using RAGAnything framework for multimodal processing.
Args:
rag_anything (RAGAnything): RAGAnything instance for multimodal document processing
file_paths (List[Path]): List of file paths to be processed
track_id (str, optional): Tracking ID for batch monitoring. Defaults to None.
scheme_name (str, optional): Processing scheme name for categorization.
Defaults to None.
parser (str, optional): Document extraction tool to use.
Defaults to None.
source (str, optional): The model source used by Mineru.
Defaults to None.
Note:
- Uses RAGAnything's process_document_complete_lightrag_api method for each file
- Supports multimodal content processing (images, tables, equations)
- Files are processed with "auto" parse method and "modelscope" source
- Output is saved to "./output" directory
- Errors are logged but don't stop processing of remaining files
"""
if not file_paths:
return
try:
# Use get_pinyin_sort_key for Chinese pinyin sorting
sorted_file_paths = sorted(
file_paths, key=lambda p: get_pinyin_sort_key(str(p))
)
# Process files sequentially with track_id
for file_path in sorted_file_paths:
success = await rag_anything.process_document_complete_lightrag_api(
file_path=str(file_path),
output_dir="./output",
parse_method="auto",
scheme_name=scheme_name,
parser=parser,
source=source,
)
if success:
pass
except Exception as e:
error_msg = f"Error indexing files: {str(e)}"
logger.error(error_msg)
logger.error(traceback.format_exc())
async def pipeline_index_texts( async def pipeline_index_texts(
rag: LightRAG, rag: LightRAG,
texts: List[str], texts: List[str],
@ -1326,24 +1485,67 @@ async def pipeline_index_texts(
async def run_scanning_process( async def run_scanning_process(
rag: LightRAG, doc_manager: DocumentManager, track_id: str = None rag: LightRAG,
rag_anything: RAGAnything,
doc_manager: DocumentManager,
track_id: str = None,
schemeConfig=None,
): ):
"""Background task to scan and index documents """Background task to scan and index documents
Args: Args:
rag: LightRAG instance rag: LightRAG instance
rag_anythingL: RAGAnything instance
doc_manager: DocumentManager instance doc_manager: DocumentManager instance
track_id: Optional tracking ID to pass to all scanned files track_id: Optional tracking ID to pass to all scanned files
schemeConfig: Scanning scheme configuration.
Defaults to None
""" """
try: try:
new_files = doc_manager.scan_directory_for_new_files() new_files = doc_manager.scan_directory_for_new_files()
total_files = len(new_files) total_files = len(new_files)
logger.info(f"Found {total_files} files to index.") logger.info(f"Found {total_files} files to index.")
from lightrag.kg.shared_storage import get_namespace_data
pipeline_status = await get_namespace_data("pipeline_status")
is_pipeline_scan_busy = pipeline_status.get("scan_disabled", False)
is_pipeline_busy = pipeline_status.get("busy", False)
scheme_name = schemeConfig.framework
extractor = schemeConfig.extractor
modelSource = schemeConfig.modelSource
if new_files: if new_files:
# Process all files at once with track_id # Process all files at once with track_id
await pipeline_index_files(rag, new_files, track_id) if is_pipeline_busy:
logger.info(f"Scanning process completed: {total_files} files Processed.") logger.info(
"Pipe is currently busy, skipping processing to avoid conflicts..."
)
return
if is_pipeline_scan_busy:
logger.info(
"Pipe is currently busy, skipping processing to avoid conflicts..."
)
return
if scheme_name == "lightrag":
await pipeline_index_files(
rag, new_files, track_id, scheme_name=scheme_name
)
logger.info(
f"Scanning process completed with lightrag: {total_files} files Processed."
)
elif scheme_name == "raganything":
await pipeline_index_files_raganything(
rag_anything,
new_files,
scheme_name=scheme_name,
parser=extractor,
source=modelSource,
)
logger.info(
f"Scanning process completed with raganything: {total_files} files Processed."
)
else: else:
# No new files to index, check if there are any documents in the queue # No new files to index, check if there are any documents in the queue
logger.info( logger.info(
@ -1554,15 +1756,250 @@ async def background_delete_documents(
def create_document_routes( def create_document_routes(
rag: LightRAG, doc_manager: DocumentManager, api_key: Optional[str] = None rag: LightRAG,
rag_anything: RAGAnything,
doc_manager: DocumentManager,
api_key: Optional[str] = None,
): ):
# Create combined auth dependency for document routes # Create combined auth dependency for document routes
combined_auth = get_combined_auth_dependency(api_key) combined_auth = get_combined_auth_dependency(api_key)
@router.get(
"/schemes",
response_model=SchemesResponse,
dependencies=[Depends(combined_auth)],
)
async def get_all_schemes():
"""Get all available processing schemes.
Retrieves the complete list of processing schemes from the schemes.json file.
Each scheme defines a processing framework (lightrag/raganything) and
optional extractor configuration (mineru/docling).
Returns:
SchemesResponse: Response containing:
- status (str): Operation status ("success")
- message (str): Success message
- data (List[Dict]): List of all available schemes with their configurations
Raises:
HTTPException: If file reading fails or JSON parsing errors occur (500)
"""
SCHEMES_FILE = Path("./examples/schemes.json")
if SCHEMES_FILE.exists():
with open(SCHEMES_FILE, "r", encoding="utf-8") as f:
try:
current_data = json.load(f)
except json.JSONDecodeError:
current_data = []
else:
current_data = []
SCHEMES_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(SCHEMES_FILE, "w") as f:
json.dump(current_data, f)
return SchemesResponse(
status="success",
message="Schemes retrieved successfully",
data=current_data,
)
@router.post(
"/schemes",
response_model=SchemesResponse,
dependencies=[Depends(combined_auth)],
)
async def save_schemes(schemes: list[Scheme_include_id]):
"""Save/update processing schemes in batch.
Updates existing schemes with new configuration data. This endpoint performs
a partial update by modifying existing schemes based on their IDs while
preserving other schemes in the file.
Args:
schemes (list[Scheme_include_id]): List of schemes to update, each containing:
- id (int): Unique identifier of the scheme to update
- name (str): Display name for the scheme
- config (SchemeConfig): Configuration object with framework and extractor settings
Returns:
SchemesResponse: Response containing:
- status (str): Operation status ("success")
- message (str): Success message with count of saved schemes
- data (List[Dict]): Updated list of all schemes after modification
Raises:
HTTPException: If file operations fail or JSON processing errors occur (500)
"""
try:
SCHEMES_FILE = Path("./examples/schemes.json")
if SCHEMES_FILE.exists():
with open(SCHEMES_FILE, "r", encoding="utf-8") as f:
try:
current_data = json.load(f)
except json.JSONDecodeError:
current_data = []
else:
current_data = []
updated_item = {
"id": schemes[0].id,
"name": schemes[0].name,
"config": {
"framework": schemes[0].config.framework,
"extractor": schemes[0].config.extractor,
"modelSource": schemes[0].config.modelSource,
},
}
# 保存新方案
for item in current_data:
if item["id"] == updated_item["id"]:
item["name"] = updated_item["name"]
item["config"]["framework"] = updated_item["config"]["framework"]
item["config"]["extractor"] = updated_item["config"]["extractor"]
item["config"]["modelSource"] = updated_item["config"][
"modelSource"
]
break
# 写回文件
with open(SCHEMES_FILE, "w", encoding="utf-8") as f:
json.dump(current_data, f, indent=4)
# 返回响应(从文件重新读取确保一致性)
with open(SCHEMES_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
return SchemesResponse(
status="success",
message=f"Successfully saved {len(schemes)} schemes",
data=data,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/schemes/add",
response_model=Scheme_include_id,
dependencies=[Depends(combined_auth)],
)
async def add_scheme(scheme: Scheme):
"""Add a new processing scheme.
Creates a new processing scheme with auto-generated ID and saves it to the
schemes configuration file. The new scheme will be available for document
processing operations.
Args:
scheme (Scheme): New scheme to add, containing:
- name (str): Display name for the scheme
- config (SchemeConfig): Configuration with framework and extractor settings
Returns:
Scheme_include_id: The created scheme with auto-generated ID, containing:
- id (int): Auto-generated unique identifier
- name (str): Display name of the scheme
- config (SchemeConfig): Processing configuration
Raises:
HTTPException: If file operations fail or ID generation conflicts occur (500)
"""
try:
SCHEMES_FILE = Path("./examples/schemes.json")
if SCHEMES_FILE.exists():
with open(SCHEMES_FILE, "r", encoding="utf-8") as f:
try:
current_data = json.load(f)
except json.JSONDecodeError:
current_data = []
else:
current_data = []
# 生成新ID简单实现实际项目应该用数据库自增ID
new_id = uuid.uuid4().int >> 96 # 生成一个较小的整数ID
while new_id in current_data:
new_id = uuid.uuid4().int >> 96
new_scheme = {
"id": new_id,
"name": scheme.name,
"config": {
"framework": scheme.config.framework,
"extractor": scheme.config.extractor,
"modelSource": scheme.config.modelSource,
},
}
current_data.append(new_scheme)
with open(SCHEMES_FILE, "w", encoding="utf-8") as f:
json.dump(current_data, f, ensure_ascii=False, indent=2)
return new_scheme
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete(
"/schemes/{scheme_id}",
response_model=Dict[str, str],
dependencies=[Depends(combined_auth)],
)
async def delete_scheme(scheme_id: int):
"""Delete a specific processing scheme by ID.
Removes a processing scheme from the configuration file. Once deleted,
the scheme will no longer be available for document processing operations.
Args:
scheme_id (int): Unique identifier of the scheme to delete
Returns:
Dict[str, str]: Success message containing:
- message (str): Confirmation message with the deleted scheme ID
Raises:
HTTPException:
- 404: If the scheme with the specified ID is not found
- 500: If file operations fail or other errors occur
"""
try:
SCHEMES_FILE = Path("./examples/schemes.json")
if SCHEMES_FILE.exists():
with open(SCHEMES_FILE, "r", encoding="utf-8") as f:
try:
current_data = json.load(f)
except json.JSONDecodeError:
current_data = []
else:
current_data = []
current_data_dict = {scheme["id"]: scheme for scheme in current_data}
if scheme_id not in current_data_dict: # 直接检查 id 是否存在
raise HTTPException(status_code=404, detail="Scheme not found")
for i, scheme in enumerate(current_data):
if scheme["id"] == scheme_id:
del current_data[i] # 直接删除列表中的元素
break
with open(SCHEMES_FILE, "w", encoding="utf-8") as f:
json.dump(current_data, f, ensure_ascii=False, indent=2)
return {"message": f"Scheme {scheme_id} deleted successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@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(
request: ScanRequest, background_tasks: BackgroundTasks
):
""" """
Trigger the scanning process for new documents. Trigger the scanning process for new documents.
@ -1577,7 +2014,14 @@ def create_document_routes(
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
background_tasks.add_task(run_scanning_process, rag, doc_manager, track_id) background_tasks.add_task(
run_scanning_process,
rag,
rag_anything,
doc_manager,
track_id,
schemeConfig=request.schemeConfig,
)
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",
@ -1588,7 +2032,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(...),
schemeId: str = Form(...),
): ):
""" """
Upload a file to the input directory and index it. Upload a file to the input directory and index it.
@ -1599,7 +2045,9 @@ 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
schemeId (str): ID of the processing scheme to use for this file. The scheme
determines whether to use LightRAG or RAGAnything framework for processing.
Returns: Returns:
InsertResponse: A response object containing the upload status and a message. InsertResponse: A response object containing the upload status and a message.
@ -1632,8 +2080,62 @@ def create_document_routes(
track_id = generate_track_id("upload") track_id = generate_track_id("upload")
def load_config():
try:
SCHEMES_FILE = Path("./examples/schemes.json")
with open(SCHEMES_FILE, "r") as f:
schemes = json.load(f)
for scheme in schemes:
if str(scheme.get("id")) == schemeId:
return scheme.get("config", {})
return {}
except Exception as e:
logger.error(
f"Failed to load config for scheme {schemeId}: {str(e)}"
)
return {}
config = load_config()
current_framework = config.get("framework")
current_extractor = config.get("extractor")
current_modelSource = config.get("modelSource")
doc_pre_id = f"doc-pre-{safe_filename}"
if current_framework and current_framework == "lightrag":
# Add to background tasks and get track_id # Add to background tasks and get track_id
background_tasks.add_task(pipeline_index_file, rag, file_path, track_id) background_tasks.add_task(
pipeline_index_file,
rag,
file_path,
track_id,
scheme_name=current_framework,
)
else:
background_tasks.add_task(
rag_anything.process_document_complete_lightrag_api,
file_path=str(file_path),
output_dir="./output",
parse_method="auto",
scheme_name=current_framework,
parser=current_extractor,
source=current_modelSource,
)
await rag.doc_status.upsert(
{
doc_pre_id: {
"status": DocStatus.READY,
"content": "",
"content_summary": "",
"multimodal_content": [],
"scheme_name": current_framework,
"content_length": 0,
"created_at": "",
"updated_at": "",
"file_path": safe_filename,
}
}
)
return InsertResponse( return InsertResponse(
status="success", status="success",
@ -1854,6 +2356,42 @@ def create_document_routes(
f"Successfully dropped all {storage_success_count} storage components" f"Successfully dropped all {storage_success_count} storage components"
) )
# Clean all parse_cache entries after successful storage drops
if storage_success_count > 0:
try:
if "history_messages" in pipeline_status:
pipeline_status["history_messages"].append(
"Cleaning parse_cache entries"
)
parse_cache_result = await rag.aclean_all_parse_cache()
if parse_cache_result.get("error"):
cache_error_msg = f"Warning: Failed to clean parse_cache: {parse_cache_result['error']}"
logger.warning(cache_error_msg)
if "history_messages" in pipeline_status:
pipeline_status["history_messages"].append(cache_error_msg)
else:
deleted_count = parse_cache_result.get("deleted_count", 0)
if deleted_count > 0:
cache_success_msg = f"Successfully cleaned {deleted_count} parse_cache entries"
logger.info(cache_success_msg)
if "history_messages" in pipeline_status:
pipeline_status["history_messages"].append(
cache_success_msg
)
else:
cache_empty_msg = "No parse_cache entries to clean"
logger.info(cache_empty_msg)
if "history_messages" in pipeline_status:
pipeline_status["history_messages"].append(
cache_empty_msg
)
except Exception as cache_error:
cache_error_msg = f"Warning: Exception while cleaning parse_cache: {str(cache_error)}"
logger.warning(cache_error_msg)
if "history_messages" in pipeline_status:
pipeline_status["history_messages"].append(cache_error_msg)
# If all storage operations failed, return error status and don't proceed with file deletion # If all storage operations failed, return error status and don't proceed with file deletion
if storage_success_count == 0 and storage_error_count > 0: if storage_success_count == 0 and storage_error_count > 0:
error_message = "All storage drop operations failed. Aborting document clearing process." error_message = "All storage drop operations failed. Aborting document clearing process."
@ -2025,7 +2563,7 @@ def create_document_routes(
Get the status of all documents in the system. Get the status of all documents in the system.
This endpoint retrieves the current status of all documents, grouped by their This endpoint retrieves the current status of all documents, grouped by their
processing status (PENDING, PROCESSING, PROCESSED, FAILED). processing status (READY, HANDLING, PENDING, PROCESSING, PROCESSED, FAILED).
Returns: Returns:
DocsStatusesResponse: A response object containing a dictionary where keys are DocsStatusesResponse: A response object containing a dictionary where keys are
@ -2037,6 +2575,8 @@ def create_document_routes(
""" """
try: try:
statuses = ( statuses = (
DocStatus.READY,
DocStatus.HANDLING,
DocStatus.PENDING, DocStatus.PENDING,
DocStatus.PROCESSING, DocStatus.PROCESSING,
DocStatus.PROCESSED, DocStatus.PROCESSED,
@ -2057,6 +2597,7 @@ def create_document_routes(
DocStatusResponse( DocStatusResponse(
id=doc_id, id=doc_id,
content_summary=doc_status.content_summary, content_summary=doc_status.content_summary,
multimodal_content=doc_status.multimodal_content,
content_length=doc_status.content_length, content_length=doc_status.content_length,
status=doc_status.status, status=doc_status.status,
created_at=format_datetime(doc_status.created_at), created_at=format_datetime(doc_status.created_at),
@ -2066,6 +2607,7 @@ def create_document_routes(
error_msg=doc_status.error_msg, error_msg=doc_status.error_msg,
metadata=doc_status.metadata, metadata=doc_status.metadata,
file_path=doc_status.file_path, file_path=doc_status.file_path,
scheme_name=doc_status.scheme_name,
) )
) )
return response return response
@ -2402,6 +2944,8 @@ def create_document_routes(
error_msg=doc.error_msg, error_msg=doc.error_msg,
metadata=doc.metadata, metadata=doc.metadata,
file_path=doc.file_path, file_path=doc.file_path,
scheme_name=doc.scheme_name,
multimodal_content=doc.multimodal_content,
) )
) )

View file

@ -0,0 +1 @@
import{e as o,c as l,g as b,k as O,h as P,j as p,l as w,m as c,n as v,t as A,o as N}from"./_baseUniq-BZ_JDEKn.js";import{a_ as g,aw as _,a$ as $,b0 as E,b1 as F,b2 as x,b3 as M,b4 as y,b5 as B,b6 as T}from"./mermaid-vendor-CpW20EHd.js";var S=/\s/;function G(n){for(var r=n.length;r--&&S.test(n.charAt(r)););return r}var H=/^\s+/;function L(n){return n&&n.slice(0,G(n)+1).replace(H,"")}var m=NaN,R=/^[-+]0x[0-9a-f]+$/i,q=/^0b[01]+$/i,z=/^0o[0-7]+$/i,C=parseInt;function K(n){if(typeof n=="number")return n;if(o(n))return m;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=L(n);var t=q.test(n);return t||z.test(n)?C(n.slice(2),t?2:8):R.test(n)?m:+n}var W=1/0,X=17976931348623157e292;function Y(n){if(!n)return n===0?n:0;if(n=K(n),n===W||n===-1/0){var r=n<0?-1:1;return r*X}return n===n?n:0}function D(n){var r=Y(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?l(n):[]}var I=Object.prototype,J=I.hasOwnProperty,dn=_(function(n,r){n=Object(n);var t=-1,e=r.length,i=e>2?r[2]:void 0;for(i&&$(r[0],r[1],i)&&(e=1);++t<e;)for(var f=r[t],a=E(f),s=-1,d=a.length;++s<d;){var u=a[s],h=n[u];(h===void 0||F(h,I[u])&&!J.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function Q(n){return function(r,t,e){var i=Object(r);if(!x(r)){var f=b(t);r=O(r),t=function(s){return f(i[s],s,i)}}var a=n(r,t,e);return a>-1?i[f?r[a]:a]:void 0}}var U=Math.max;function Z(n,r,t){var e=n==null?0:n.length;if(!e)return-1;var i=t==null?0:D(t);return i<0&&(i=U(e+i,0)),P(n,b(r),i)}var hn=Q(Z);function V(n,r){var t=-1,e=x(n)?Array(n.length):[];return p(n,function(i,f,a){e[++t]=r(i,f,a)}),e}function gn(n,r){var t=M(n)?w:V;return t(n,b(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function bn(n,r){return n!=null&&c(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var e=-1,i=n.length;++e<i;){var f=n[e],a=r(f);if(a!=null&&(s===void 0?a===a&&!o(a):t(a,s)))var s=a,d=f}return d}function mn(n){return n&&n.length?tn(n,y,rn):void 0}function an(n,r,t,e){if(!g(n))return n;r=v(r,n);for(var i=-1,f=r.length,a=f-1,s=n;s!=null&&++i<f;){var d=A(r[i]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(i!=a){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:B(r[i+1])?[]:{})}T(s,d,u),s=s[d]}return n}function on(n,r,t){for(var e=-1,i=r.length,f={};++e<i;){var a=r[e],s=N(n,a);t(s,a)&&an(f,v(a,n),s)}return f}export{rn as a,tn as b,V as c,on as d,mn as e,fn as f,hn as g,bn as h,dn as i,D as j,un as l,gn as m,Y as t};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{_ as l}from"./mermaid-vendor-CpW20EHd.js";function m(e,c){var i,t,o;e.accDescr&&((i=c.setAccDescription)==null||i.call(c,e.accDescr)),e.accTitle&&((t=c.setAccTitle)==null||t.call(c,e.accTitle)),e.title&&((o=c.setDiagramTitle)==null||o.call(c,e.title))}l(m,"populateCommonDb");export{m as p};

View file

@ -0,0 +1 @@
import{_ as n,a2 as x,j as l}from"./mermaid-vendor-CpW20EHd.js";var c=n((a,t)=>{const e=a.append("rect");if(e.attr("x",t.x),e.attr("y",t.y),e.attr("fill",t.fill),e.attr("stroke",t.stroke),e.attr("width",t.width),e.attr("height",t.height),t.name&&e.attr("name",t.name),t.rx&&e.attr("rx",t.rx),t.ry&&e.attr("ry",t.ry),t.attrs!==void 0)for(const r in t.attrs)e.attr(r,t.attrs[r]);return t.class&&e.attr("class",t.class),e},"drawRect"),d=n((a,t)=>{const e={x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,stroke:t.stroke,class:"rect"};c(a,e).lower()},"drawBackgroundRect"),g=n((a,t)=>{const e=t.text.replace(x," "),r=a.append("text");r.attr("x",t.x),r.attr("y",t.y),r.attr("class","legend"),r.style("text-anchor",t.anchor),t.class&&r.attr("class",t.class);const s=r.append("tspan");return s.attr("x",t.x+t.textMargin*2),s.text(e),r},"drawText"),h=n((a,t,e,r)=>{const s=a.append("image");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",i)},"drawImage"),m=n((a,t,e,r)=>{const s=a.append("use");s.attr("x",t),s.attr("y",e);const i=l.sanitizeUrl(r);s.attr("xlink:href",`#${i}`)},"drawEmbeddedImage"),y=n(()=>({x:0,y:0,width:100,height:100,fill:"#EDF2AE",stroke:"#666",anchor:"start",rx:0,ry:0}),"getNoteRect"),p=n(()=>({x:0,y:0,width:100,height:100,"text-anchor":"start",style:"#666",textMargin:0,rx:0,ry:0,tspan:!0}),"getTextObj");export{d as a,p as b,m as c,c as d,h as e,g as f,y as g};

View file

@ -0,0 +1 @@
import{_ as s}from"./mermaid-vendor-CpW20EHd.js";var t,e=(t=class{constructor(i){this.init=i,this.records=this.init()}reset(){this.records=this.init()}},s(t,"ImperativeState"),t);export{e as I};

View file

@ -0,0 +1 @@
import{_ as a,d as o}from"./mermaid-vendor-CpW20EHd.js";var d=a((t,e)=>{let n;return e==="sandbox"&&(n=o("#i"+t)),(e==="sandbox"?o(n.nodes()[0].contentDocument.body):o("body")).select(`[id="${t}"]`)},"getDiagramElement");export{d as g};

View file

@ -0,0 +1,15 @@
import{_ as e}from"./mermaid-vendor-CpW20EHd.js";var l=e(()=>`
/* Font Awesome icon styling - consolidated */
.label-icon {
display: inline-block;
height: 1em;
overflow: visible;
vertical-align: -0.125em;
}
.node .label-icon path {
fill: currentColor;
stroke: revert;
stroke-width: revert;
}
`,"getIconStyles");export{l as g};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{_ as a,e as w,l as x}from"./mermaid-vendor-CpW20EHd.js";var d=a((e,t,i,o)=>{e.attr("class",i);const{width:r,height:h,x:n,y:c}=u(e,t);w(e,h,r,o);const s=l(n,c,r,h,t);e.attr("viewBox",s),x.debug(`viewBox configured: ${s} with padding: ${t}`)},"setupViewPortForSVG"),u=a((e,t)=>{var o;const i=((o=e.node())==null?void 0:o.getBBox())||{width:0,height:0,x:0,y:0};return{width:i.width+t*2,height:i.height+t*2,x:i.x,y:i.y}},"calculateDimensionsWithPadding"),l=a((e,t,i,o,r)=>`${e-r} ${t-r} ${i} ${o}`,"createViewBox");export{d as s};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{s as a,c as s,a as e,C as t}from"./chunk-SZ463SBG-CG1c8KxJ.js";import{_ as i}from"./mermaid-vendor-CpW20EHd.js";import"./chunk-E2GYISFI-DgamQYak.js";import"./chunk-BFAMUDN2-CHovHiOg.js";import"./chunk-SKB7J2MH-CqH3ZkpA.js";import"./feature-graph-xUsMo1iK.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var c={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{c as diagram};

View file

@ -0,0 +1 @@
import{s as a,c as s,a as e,C as t}from"./chunk-SZ463SBG-CG1c8KxJ.js";import{_ as i}from"./mermaid-vendor-CpW20EHd.js";import"./chunk-E2GYISFI-DgamQYak.js";import"./chunk-BFAMUDN2-CHovHiOg.js";import"./chunk-SKB7J2MH-CqH3ZkpA.js";import"./feature-graph-xUsMo1iK.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var c={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{c as diagram};

View file

@ -0,0 +1 @@
import{b as r}from"./_baseUniq-BZ_JDEKn.js";var e=4;function a(o){return r(o,e)}export{a as c};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,24 @@
import{p as y}from"./chunk-353BL4L5-0V1KVYyT.js";import{_ as l,s as B,g as S,t as z,q as F,a as P,b as E,F as v,K as W,e as T,z as D,G as _,H as A,l as w}from"./mermaid-vendor-CpW20EHd.js";import{p as N}from"./treemap-75Q7IDZK-CSah7hvo.js";import"./feature-graph-xUsMo1iK.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BZ_JDEKn.js";import"./_basePickBy-C1BlOoDW.js";import"./clone-CDvVvGlj.js";var x={packet:[]},m=structuredClone(x),L=A.packet,Y=l(()=>{const t=v({...L,..._().packet});return t.showBits&&(t.paddingY+=10),t},"getConfig"),G=l(()=>m.packet,"getPacket"),H=l(t=>{t.length>0&&m.packet.push(t)},"pushWord"),I=l(()=>{D(),m=structuredClone(x)},"clear"),u={pushWord:H,getPacket:G,getConfig:Y,clear:I,setAccTitle:E,getAccTitle:P,setDiagramTitle:F,getDiagramTitle:z,getAccDescription:S,setAccDescription:B},K=1e4,M=l(t=>{y(t,u);let e=-1,o=[],n=1;const{bitsPerRow:i}=u.getConfig();for(let{start:a,end:r,bits:c,label:f}of t.blocks){if(a!==void 0&&r!==void 0&&r<a)throw new Error(`Packet block ${a} - ${r} is invalid. End must be greater than start.`);if(a??(a=e+1),a!==e+1)throw new Error(`Packet block ${a} - ${r??a} is not contiguous. It should start from ${e+1}.`);if(c===0)throw new Error(`Packet block ${a} is invalid. Cannot have a zero bit field.`);for(r??(r=a+(c??1)-1),c??(c=r-a+1),e=r,w.debug(`Packet block ${a} - ${e} with label ${f}`);o.length<=i+1&&u.getPacket().length<K;){const[d,p]=O({start:a,end:r,bits:c,label:f},n,i);if(o.push(d),d.end+1===n*i&&(u.pushWord(o),o=[],n++),!p)break;({start:a,end:r,bits:c,label:f}=p)}}u.pushWord(o)},"populate"),O=l((t,e,o)=>{if(t.start===void 0)throw new Error("start should have been set during first phase");if(t.end===void 0)throw new Error("end should have been set during first phase");if(t.start>t.end)throw new Error(`Block start ${t.start} is greater than block end ${t.end}.`);if(t.end+1<=e*o)return[t,void 0];const n=e*o-1,i=e*o;return[{start:t.start,end:n,label:t.label,bits:n-t.start},{start:i,end:t.end,label:t.label,bits:t.end-i}]},"getNextFittingBlock"),q={parse:l(async t=>{const e=await N("packet",t);w.debug(e),M(e)},"parse")},R=l((t,e,o,n)=>{const i=n.db,a=i.getConfig(),{rowHeight:r,paddingY:c,bitWidth:f,bitsPerRow:d}=a,p=i.getPacket(),s=i.getDiagramTitle(),k=r+c,g=k*(p.length+1)-(s?0:r),b=f*d+2,h=W(e);h.attr("viewbox",`0 0 ${b} ${g}`),T(h,g,b,a.useMaxWidth);for(const[C,$]of p.entries())U(h,$,C,a);h.append("text").text(s).attr("x",b/2).attr("y",g-k/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),U=l((t,e,o,{rowHeight:n,paddingX:i,paddingY:a,bitWidth:r,bitsPerRow:c,showBits:f})=>{const d=t.append("g"),p=o*(n+a)+a;for(const s of e){const k=s.start%c*r+1,g=(s.end-s.start+1)*r-i;if(d.append("rect").attr("x",k).attr("y",p).attr("width",g).attr("height",n).attr("class","packetBlock"),d.append("text").attr("x",k+g/2).attr("y",p+n/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(s.label),!f)continue;const b=s.end===s.start,h=p-2;d.append("text").attr("x",k+(b?g/2:0)).attr("y",h).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",b?"middle":"start").text(s.start),b||d.append("text").attr("x",k+g).attr("y",h).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(s.end)}},"drawWord"),X={draw:R},j={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},J=l(({packet:t}={})=>{const e=v(j,t);return`
.packetByte {
font-size: ${e.byteFontSize};
}
.packetByte.start {
fill: ${e.startByteColor};
}
.packetByte.end {
fill: ${e.endByteColor};
}
.packetLabel {
fill: ${e.labelColor};
font-size: ${e.labelFontSize};
}
.packetTitle {
fill: ${e.titleColor};
font-size: ${e.titleFontSize};
}
.packetBlock {
stroke: ${e.blockStrokeColor};
stroke-width: ${e.blockStrokeWidth};
fill: ${e.blockFillColor};
}
`},"styles"),lt={parser:q,db:u,renderer:X,styles:J};export{lt as diagram};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
import{_ as e,l as o,K as i,e as n,L as p}from"./mermaid-vendor-CpW20EHd.js";import{p as m}from"./treemap-75Q7IDZK-CSah7hvo.js";import"./feature-graph-xUsMo1iK.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BZ_JDEKn.js";import"./_basePickBy-C1BlOoDW.js";import"./clone-CDvVvGlj.js";var g={parse:e(async r=>{const a=await m("info",r);o.debug(a)},"parse")},v={version:p.version+""},d=e(()=>v.version,"getVersion"),c={getVersion:d},l=e((r,a,s)=>{o.debug(`rendering info diagram
`+r);const t=i(a);n(t,100,400,!0),t.append("g").append("text").attr("x",100).attr("y",40).attr("class","version").attr("font-size",32).style("text-anchor","middle").text(`v${s}`)},"draw"),f={draw:l},L={parser:g,db:c,renderer:f};export{L as diagram};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,30 @@
import{p as N}from"./chunk-353BL4L5-0V1KVYyT.js";import{_ as i,g as B,s as U,a as q,b as H,t as K,q as V,l as C,c as Z,F as j,K as J,M as Q,N as z,O as X,e as Y,z as tt,P as et,H as at}from"./mermaid-vendor-CpW20EHd.js";import{p as rt}from"./treemap-75Q7IDZK-CSah7hvo.js";import"./feature-graph-xUsMo1iK.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";import"./_baseUniq-BZ_JDEKn.js";import"./_basePickBy-C1BlOoDW.js";import"./clone-CDvVvGlj.js";var it=at.pie,D={sections:new Map,showData:!1},f=D.sections,w=D.showData,st=structuredClone(it),ot=i(()=>structuredClone(st),"getConfig"),nt=i(()=>{f=new Map,w=D.showData,tt()},"clear"),lt=i(({label:t,value:a})=>{f.has(t)||(f.set(t,a),C.debug(`added new section: ${t}, with value: ${a}`))},"addSection"),ct=i(()=>f,"getSections"),pt=i(t=>{w=t},"setShowData"),dt=i(()=>w,"getShowData"),F={getConfig:ot,clear:nt,setDiagramTitle:V,getDiagramTitle:K,setAccTitle:H,getAccTitle:q,setAccDescription:U,getAccDescription:B,addSection:lt,getSections:ct,setShowData:pt,getShowData:dt},gt=i((t,a)=>{N(t,a),a.setShowData(t.showData),t.sections.map(a.addSection)},"populateDb"),ut={parse:i(async t=>{const a=await rt("pie",t);C.debug(a),gt(a,F)},"parse")},mt=i(t=>`
.pieCircle{
stroke: ${t.pieStrokeColor};
stroke-width : ${t.pieStrokeWidth};
opacity : ${t.pieOpacity};
}
.pieOuterCircle{
stroke: ${t.pieOuterStrokeColor};
stroke-width: ${t.pieOuterStrokeWidth};
fill: none;
}
.pieTitleText {
text-anchor: middle;
font-size: ${t.pieTitleTextSize};
fill: ${t.pieTitleTextColor};
font-family: ${t.fontFamily};
}
.slice {
font-family: ${t.fontFamily};
fill: ${t.pieSectionTextColor};
font-size:${t.pieSectionTextSize};
// fill: white;
}
.legend text {
fill: ${t.pieLegendTextColor};
font-family: ${t.fontFamily};
font-size: ${t.pieLegendTextSize};
}
`,"getStyles"),ft=mt,ht=i(t=>{const a=[...t.entries()].map(s=>({label:s[0],value:s[1]})).sort((s,n)=>n.value-s.value);return et().value(s=>s.value)(a)},"createPieArcs"),St=i((t,a,G,s)=>{C.debug(`rendering pie chart
`+t);const n=s.db,y=Z(),T=j(n.getConfig(),y.pie),$=40,o=18,d=4,c=450,h=c,S=J(a),l=S.append("g");l.attr("transform","translate("+h/2+","+c/2+")");const{themeVariables:r}=y;let[A]=Q(r.pieOuterStrokeWidth);A??(A=2);const _=T.textPosition,g=Math.min(h,c)/2-$,M=z().innerRadius(0).outerRadius(g),O=z().innerRadius(g*_).outerRadius(g*_);l.append("circle").attr("cx",0).attr("cy",0).attr("r",g+A/2).attr("class","pieOuterCircle");const b=n.getSections(),v=ht(b),P=[r.pie1,r.pie2,r.pie3,r.pie4,r.pie5,r.pie6,r.pie7,r.pie8,r.pie9,r.pie10,r.pie11,r.pie12],p=X(P);l.selectAll("mySlices").data(v).enter().append("path").attr("d",M).attr("fill",e=>p(e.data.label)).attr("class","pieCircle");let E=0;b.forEach(e=>{E+=e}),l.selectAll("mySlices").data(v).enter().append("text").text(e=>(e.data.value/E*100).toFixed(0)+"%").attr("transform",e=>"translate("+O.centroid(e)+")").style("text-anchor","middle").attr("class","slice"),l.append("text").text(n.getDiagramTitle()).attr("x",0).attr("y",-400/2).attr("class","pieTitleText");const x=l.selectAll(".legend").data(p.domain()).enter().append("g").attr("class","legend").attr("transform",(e,u)=>{const m=o+d,R=m*p.domain().length/2,I=12*o,L=u*m-R;return"translate("+I+","+L+")"});x.append("rect").attr("width",o).attr("height",o).style("fill",p).style("stroke",p),x.data(v).append("text").attr("x",o+d).attr("y",o-d).text(e=>{const{label:u,value:m}=e.data;return n.getShowData()?`${u} [${m}]`:u});const W=Math.max(...x.selectAll("text").nodes().map(e=>(e==null?void 0:e.getBoundingClientRect().width)??0)),k=h+$+o+d+W;S.attr("viewBox",`0 0 ${k} ${c}`),Y(S,c,k,T.useMaxWidth)},"draw"),vt={draw:St},kt={parser:ut,db:F,renderer:vt,styles:ft};export{kt as diagram};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
import{s as r,b as e,a,S as i}from"./chunk-OW32GOEJ-DF1Nd_2F.js";import{_ as s}from"./mermaid-vendor-CpW20EHd.js";import"./chunk-BFAMUDN2-CHovHiOg.js";import"./chunk-SKB7J2MH-CqH3ZkpA.js";import"./feature-graph-xUsMo1iK.js";import"./react-vendor-DEwriMA6.js";import"./graph-vendor-B-X5JegA.js";import"./ui-vendor-CeCm8EER.js";import"./utils-vendor-BysuhMZA.js";var f={parser:a,get db(){return new i(2)},renderer:e,styles:r,init:s(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{f as diagram};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8,20 +8,21 @@
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-Bjp1KQxU.js"></script> <script type="module" crossorigin src="/webui/assets/index-oYZPo1xP.js"></script>
<link rel="modulepreload" crossorigin href="/webui/assets/react-vendor-DEwriMA6.js"> <link rel="modulepreload" crossorigin href="/webui/assets/react-vendor-DEwriMA6.js">
<link rel="modulepreload" crossorigin href="/webui/assets/ui-vendor-CeCm8EER.js"> <link rel="modulepreload" crossorigin href="/webui/assets/ui-vendor-CeCm8EER.js">
<link rel="modulepreload" crossorigin href="/webui/assets/graph-vendor-B-X5JegA.js"> <link rel="modulepreload" crossorigin href="/webui/assets/graph-vendor-B-X5JegA.js">
<link rel="modulepreload" crossorigin href="/webui/assets/utils-vendor-BysuhMZA.js"> <link rel="modulepreload" crossorigin href="/webui/assets/utils-vendor-BysuhMZA.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-graph-bahMe5Gt.js"> <link rel="modulepreload" crossorigin href="/webui/assets/feature-graph-xUsMo1iK.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-cF4-Vta6.js"> <link rel="modulepreload" crossorigin href="/webui/assets/feature-documents-22OwnQq9.js">
<link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-BOzHoVUU.js"> <link rel="modulepreload" crossorigin href="/webui/assets/mermaid-vendor-CpW20EHd.js">
<link rel="modulepreload" crossorigin href="/webui/assets/markdown-vendor-DmIvJdn7.js"> <link rel="modulepreload" crossorigin href="/webui/assets/markdown-vendor-C1oKx5V8.js">
<link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-CnKoygV2.js"> <link rel="modulepreload" crossorigin href="/webui/assets/feature-retrieval-CeceOXFg.js">
<link rel="stylesheet" crossorigin href="/webui/assets/feature-graph-BipNuM18.css"> <link rel="stylesheet" crossorigin href="/webui/assets/feature-graph-BipNuM18.css">
<link rel="stylesheet" crossorigin href="/webui/assets/index-BvrNHAMA.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-CEupRNOQ.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View file

@ -675,6 +675,8 @@ class BaseGraphStorage(StorageNameSpace, ABC):
class DocStatus(str, Enum): class DocStatus(str, Enum):
"""Document processing status""" """Document processing status"""
READY = "ready"
HANDLING = "handling"
PENDING = "pending" PENDING = "pending"
PROCESSING = "processing" PROCESSING = "processing"
PROCESSED = "processed" PROCESSED = "processed"
@ -707,6 +709,12 @@ class DocProcessingStatus:
"""Error message if failed""" """Error message if failed"""
metadata: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict)
"""Additional metadata""" """Additional metadata"""
multimodal_content: list[dict[str, Any]] | None = None
"""raganything: multimodal_content"""
multimodal_processed: bool | None = None
"""raganything: multimodal_processed"""
scheme_name: str | None = None
"""lightrag or raganything"""
@dataclass @dataclass

View file

@ -9,6 +9,7 @@ import warnings
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
from functools import partial from functools import partial
from pathlib import Path
from typing import ( from typing import (
Any, Any,
AsyncIterator, AsyncIterator,
@ -98,6 +99,7 @@ from .utils import (
) )
from .types import KnowledgeGraph from .types import KnowledgeGraph
from dotenv import load_dotenv from dotenv import load_dotenv
from .ragmanager import RAGManager
# use the .env that is inside the current folder # use the .env that is inside the current folder
# allows to use different .env file for each lightrag instance # allows to use different .env file for each lightrag instance
@ -135,6 +137,9 @@ class LightRAG:
doc_status_storage: str = field(default="JsonDocStatusStorage") doc_status_storage: str = field(default="JsonDocStatusStorage")
"""Storage type for tracking document processing statuses.""" """Storage type for tracking document processing statuses."""
input_dir: str = field(default_factory=lambda: os.getenv("INPUT_DIR", "./inputs"))
"""Directory containing input documents"""
# Workspace # Workspace
# --- # ---
@ -863,16 +868,22 @@ class LightRAG:
def insert( def insert(
self, self,
input: str | list[str], input: str | list[str],
multimodal_content: list[dict[str, Any]]
| list[list[dict[str, Any]]]
| None = None,
split_by_character: str | None = None, split_by_character: str | None = None,
split_by_character_only: bool = False, split_by_character_only: bool = False,
ids: str | list[str] | None = None, ids: str | list[str] | None = None,
file_paths: str | list[str] | None = None, file_paths: str | list[str] | None = None,
track_id: str | None = None, track_id: str | None = None,
scheme_name: str | None = None,
) -> str: ) -> str:
"""Sync Insert documents with checkpoint support """Sync Insert documents with checkpoint support
Args: Args:
input: Single document string or list of document strings input: Single document string or list of document strings
multimodal_content (list[dict[str, Any]] | list[list[dict[str, Any]]] | None, optional):
Multimodal content (images, tables, equations) associated with documents
split_by_character: if split_by_character is not None, split the string by character, if chunk longer than split_by_character: if split_by_character is not None, split the string by character, if chunk longer than
chunk_token_size, it will be split again by token size. chunk_token_size, it will be split again by token size.
split_by_character_only: if split_by_character_only is True, split the string by character only, when split_by_character_only: if split_by_character_only is True, split the string by character only, when
@ -880,6 +891,7 @@ class LightRAG:
ids: single string of the document ID or list of unique document IDs, if not provided, MD5 hash IDs will be generated ids: single string of the document ID or list of unique document IDs, if not provided, MD5 hash IDs will be generated
file_paths: single string of the file path or list of file paths, used for citation file_paths: single string of the file path or list of file paths, used for citation
track_id: tracking ID for monitoring processing status, if not provided, will be generated track_id: tracking ID for monitoring processing status, if not provided, will be generated
scheme_name (str | None, optional): Scheme name for categorizing documents
Returns: Returns:
str: tracking ID for monitoring processing status str: tracking ID for monitoring processing status
@ -888,27 +900,35 @@ class LightRAG:
return loop.run_until_complete( return loop.run_until_complete(
self.ainsert( self.ainsert(
input, input,
multimodal_content,
split_by_character, split_by_character,
split_by_character_only, split_by_character_only,
ids, ids,
file_paths, file_paths,
track_id, track_id,
scheme_name,
) )
) )
async def ainsert( async def ainsert(
self, self,
input: str | list[str], input: str | list[str],
multimodal_content: list[dict[str, Any]]
| list[list[dict[str, Any]]]
| None = None,
split_by_character: str | None = None, split_by_character: str | None = None,
split_by_character_only: bool = False, split_by_character_only: bool = False,
ids: str | list[str] | None = None, ids: str | list[str] | None = None,
file_paths: str | list[str] | None = None, file_paths: str | list[str] | None = None,
track_id: str | None = None, track_id: str | None = None,
scheme_name: str | None = None,
) -> str: ) -> str:
"""Async Insert documents with checkpoint support """Async Insert documents with checkpoint support
Args: Args:
input: Single document string or list of document strings input: Single document string or list of document strings
multimodal_content (list[dict[str, Any]] | list[list[dict[str, Any]]] | None, optional):
Multimodal content (images, tables, equations) associated with documents
split_by_character: if split_by_character is not None, split the string by character, if chunk longer than split_by_character: if split_by_character is not None, split the string by character, if chunk longer than
chunk_token_size, it will be split again by token size. chunk_token_size, it will be split again by token size.
split_by_character_only: if split_by_character_only is True, split the string by character only, when split_by_character_only: if split_by_character_only is True, split the string by character only, when
@ -916,6 +936,7 @@ class LightRAG:
ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated
file_paths: list of file paths corresponding to each document, used for citation file_paths: list of file paths corresponding to each document, used for citation
track_id: tracking ID for monitoring processing status, if not provided, will be generated track_id: tracking ID for monitoring processing status, if not provided, will be generated
scheme_name (str | None, optional): Scheme name for categorizing documents
Returns: Returns:
str: tracking ID for monitoring processing status str: tracking ID for monitoring processing status
@ -924,13 +945,83 @@ class LightRAG:
if track_id is None: if track_id is None:
track_id = generate_track_id("insert") track_id = generate_track_id("insert")
await self.apipeline_enqueue_documents(input, ids, file_paths, track_id) paths_to_check = [file_paths] if isinstance(file_paths, str) else file_paths
base_input_dir = Path(self.input_dir)
if self.workspace:
current_input_dir = base_input_dir / self.workspace
else:
current_input_dir = base_input_dir
await self.apipeline_enqueue_documents(
input,
multimodal_content,
ids,
file_paths,
track_id,
scheme_name=scheme_name,
)
for file_path in paths_to_check:
current_file_path = current_input_dir / file_path
if current_file_path.exists():
self.move_file_to_enqueue(current_file_path)
else:
continue
await self.apipeline_process_enqueue_documents( await self.apipeline_process_enqueue_documents(
split_by_character, split_by_character_only split_by_character, split_by_character_only
) )
return track_id return track_id
def move_file_to_enqueue(self, file_path):
try:
enqueued_dir = file_path.parent / "__enqueued__"
enqueued_dir.mkdir(exist_ok=True)
# Generate unique filename to avoid conflicts
unique_filename = self.get_unique_filename_in_enqueued(
enqueued_dir, file_path.name
)
target_path = enqueued_dir / unique_filename
# Move the file
file_path.rename(target_path)
logger.debug(
f"Moved file to enqueued directory: {file_path.name} -> {unique_filename}"
)
except Exception as move_error:
logger.error(
f"Failed to move file {file_path.name} to __enqueued__ directory: {move_error}"
)
# Don't affect the main function's success status
def get_unique_filename_in_enqueued(
self, target_dir: Path, original_name: str
) -> str:
from pathlib import Path
import time
original_path = Path(original_name)
base_name = original_path.stem
extension = original_path.suffix
# Try original name first
if not (target_dir / original_name).exists():
return original_name
# Try with numeric suffixes 001-999
for i in range(1, 1000):
suffix = f"{i:03d}"
new_name = f"{base_name}_{suffix}{extension}"
if not (target_dir / new_name).exists():
return new_name
# Fallback with timestamp if all 999 slots are taken
timestamp = int(time.time())
return f"{base_name}_{timestamp}{extension}"
# TODO: deprecated, use insert instead # TODO: deprecated, use insert instead
def insert_custom_chunks( def insert_custom_chunks(
self, self,
@ -1006,9 +1097,13 @@ class LightRAG:
async def apipeline_enqueue_documents( async def apipeline_enqueue_documents(
self, self,
input: str | list[str], input: str | list[str],
multimodal_content: list[dict[str, Any]]
| list[list[dict[str, Any]]]
| None = None,
ids: list[str] | None = None, ids: list[str] | None = None,
file_paths: str | list[str] | None = None, file_paths: str | list[str] | None = None,
track_id: str | None = None, track_id: str | None = None,
scheme_name: str | None = None,
) -> str: ) -> str:
""" """
Pipeline for Processing Documents Pipeline for Processing Documents
@ -1020,9 +1115,12 @@ class LightRAG:
Args: Args:
input: Single document string or list of document strings input: Single document string or list of document strings
multimodal_content (list[dict[str, Any]] | list[list[dict[str, Any]]] | None, optional):
Multimodal content (images, tables, equations) associated with documents
ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated
file_paths: list of file paths corresponding to each document, used for citation file_paths: list of file paths corresponding to each document, used for citation
track_id: tracking ID for monitoring processing status, if not provided, will be generated with "enqueue" prefix track_id: tracking ID for monitoring processing status, if not provided, will be generated with "enqueue" prefix
scheme_name (str | None, optional): Scheme name for categorizing documents
Returns: Returns:
str: tracking ID for monitoring processing status str: tracking ID for monitoring processing status
@ -1093,6 +1191,7 @@ class LightRAG:
id_: { id_: {
"status": DocStatus.PENDING, "status": DocStatus.PENDING,
"content_summary": get_content_summary(content_data["content"]), "content_summary": get_content_summary(content_data["content"]),
"multimodal_content": multimodal_content,
"content_length": len(content_data["content"]), "content_length": len(content_data["content"]),
"created_at": datetime.now(timezone.utc).isoformat(), "created_at": datetime.now(timezone.utc).isoformat(),
"updated_at": datetime.now(timezone.utc).isoformat(), "updated_at": datetime.now(timezone.utc).isoformat(),
@ -1100,6 +1199,7 @@ class LightRAG:
"file_path" "file_path"
], # Store file path in document status ], # Store file path in document status
"track_id": track_id, # Store track_id in document status "track_id": track_id, # Store track_id in document status
"scheme_name": scheme_name,
} }
for id_, content_data in contents.items() for id_, content_data in contents.items()
} }
@ -1130,6 +1230,12 @@ class LightRAG:
if doc_id in new_docs if doc_id in new_docs
} }
new_docs_idList = [
f"doc-pre-{new_docs[doc_id]['file_path']}"
for doc_id in unique_new_doc_ids
if doc_id in new_docs
]
if not new_docs: if not new_docs:
logger.warning("No new unique documents were found.") logger.warning("No new unique documents were found.")
return return
@ -1147,6 +1253,10 @@ class LightRAG:
# Store document status (without content) # Store document status (without content)
await self.doc_status.upsert(new_docs) await self.doc_status.upsert(new_docs)
logger.debug(f"Stored {len(new_docs)} new unique documents") logger.debug(f"Stored {len(new_docs)} new unique documents")
await self.doc_status.index_done_callback()
await self.doc_status.delete(new_docs_idList)
logger.info(f"Deleted {new_docs_idList} Successful")
return track_id return track_id
@ -1322,6 +1432,7 @@ class LightRAG:
docs_to_reset[doc_id] = { docs_to_reset[doc_id] = {
"status": DocStatus.PENDING, "status": DocStatus.PENDING,
"content_summary": status_doc.content_summary, "content_summary": status_doc.content_summary,
"multimodal_content": status_doc.multimodal_content,
"content_length": status_doc.content_length, "content_length": status_doc.content_length,
"created_at": status_doc.created_at, "created_at": status_doc.created_at,
"updated_at": datetime.now(timezone.utc).isoformat(), "updated_at": datetime.now(timezone.utc).isoformat(),
@ -1330,11 +1441,15 @@ class LightRAG:
# Clear any error messages and processing metadata # Clear any error messages and processing metadata
"error_msg": "", "error_msg": "",
"metadata": {}, "metadata": {},
"scheme_name": status_doc.scheme_name,
} }
# Update the status in to_process_docs as well # Update the status in to_process_docs as well
status_doc.status = DocStatus.PENDING status_doc.status = DocStatus.PENDING
reset_count += 1 reset_count += 1
logger.info(
f"Document {status_doc.file_path} from PROCESSING/FAILED to PENDING status"
)
# Update doc_status storage if there are documents to reset # Update doc_status storage if there are documents to reset
if docs_to_reset: if docs_to_reset:
@ -1557,6 +1672,7 @@ class LightRAG:
chunks.keys() chunks.keys()
), # Save chunks list ), # Save chunks list
"content_summary": status_doc.content_summary, "content_summary": status_doc.content_summary,
"multimodal_content": status_doc.multimodal_content,
"content_length": status_doc.content_length, "content_length": status_doc.content_length,
"created_at": status_doc.created_at, "created_at": status_doc.created_at,
"updated_at": datetime.now( "updated_at": datetime.now(
@ -1567,6 +1683,7 @@ class LightRAG:
"metadata": { "metadata": {
"processing_start_time": processing_start_time "processing_start_time": processing_start_time
}, },
"scheme_name": status_doc.scheme_name,
} }
} }
) )
@ -1632,6 +1749,7 @@ class LightRAG:
"status": DocStatus.FAILED, "status": DocStatus.FAILED,
"error_msg": str(e), "error_msg": str(e),
"content_summary": status_doc.content_summary, "content_summary": status_doc.content_summary,
"multimodal_content": status_doc.multimodal_content,
"content_length": status_doc.content_length, "content_length": status_doc.content_length,
"created_at": status_doc.created_at, "created_at": status_doc.created_at,
"updated_at": datetime.now( "updated_at": datetime.now(
@ -1643,6 +1761,7 @@ class LightRAG:
"processing_start_time": processing_start_time, "processing_start_time": processing_start_time,
"processing_end_time": processing_end_time, "processing_end_time": processing_end_time,
}, },
"scheme_name": status_doc.scheme_name,
} }
} }
) )
@ -1675,10 +1794,11 @@ class LightRAG:
await self.doc_status.upsert( await self.doc_status.upsert(
{ {
doc_id: { doc_id: {
"status": DocStatus.PROCESSED, "status": DocStatus.PROCESSING,
"chunks_count": len(chunks), "chunks_count": len(chunks),
"chunks_list": list(chunks.keys()), "chunks_list": list(chunks.keys()),
"content_summary": status_doc.content_summary, "content_summary": status_doc.content_summary,
"multimodal_content": status_doc.multimodal_content,
"content_length": status_doc.content_length, "content_length": status_doc.content_length,
"created_at": status_doc.created_at, "created_at": status_doc.created_at,
"updated_at": datetime.now( "updated_at": datetime.now(
@ -1690,6 +1810,32 @@ class LightRAG:
"processing_start_time": processing_start_time, "processing_start_time": processing_start_time,
"processing_end_time": processing_end_time, "processing_end_time": processing_end_time,
}, },
"scheme_name": status_doc.scheme_name,
}
}
)
if (
status_doc.multimodal_content
and len(status_doc.multimodal_content) > 0
):
raganything_instance = RAGManager.get_rag()
await raganything_instance._process_multimodal_content(
status_doc.multimodal_content,
status_doc.file_path,
doc_id,
pipeline_status=pipeline_status,
pipeline_status_lock=pipeline_status_lock,
)
current_doc_status = await self.doc_status.get_by_id(
doc_id
)
await self.doc_status.upsert(
{
doc_id: {
**current_doc_status,
"status": DocStatus.PROCESSED,
} }
} }
) )
@ -1733,6 +1879,7 @@ class LightRAG:
"status": DocStatus.FAILED, "status": DocStatus.FAILED,
"error_msg": str(e), "error_msg": str(e),
"content_summary": status_doc.content_summary, "content_summary": status_doc.content_summary,
"multimodal_content": status_doc.multimodal_content,
"content_length": status_doc.content_length, "content_length": status_doc.content_length,
"created_at": status_doc.created_at, "created_at": status_doc.created_at,
"updated_at": datetime.now().isoformat(), "updated_at": datetime.now().isoformat(),
@ -1742,6 +1889,7 @@ class LightRAG:
"processing_start_time": processing_start_time, "processing_start_time": processing_start_time,
"processing_end_time": processing_end_time, "processing_end_time": processing_end_time,
}, },
"scheme_name": status_doc.scheme_name,
} }
} }
) )
@ -2294,6 +2442,156 @@ class LightRAG:
# Return the dictionary containing statuses only for the found document IDs # Return the dictionary containing statuses only for the found document IDs
return found_statuses return found_statuses
async def aclean_parse_cache_by_doc_ids(
self, doc_ids: str | list[str]
) -> dict[str, Any]:
"""Asynchronously clean parse_cache entries for specified document IDs
Args:
doc_ids: Single document ID string or list of document IDs
Returns:
Dictionary containing cleanup results:
- deleted_entries: List of deleted cache entries
- not_found: List of document IDs not found
- error: Error message (if operation fails)
"""
import json
from pathlib import Path
# Normalize input to list
if isinstance(doc_ids, str):
doc_ids = [doc_ids]
result = {"deleted_entries": [], "not_found": [], "error": None}
try:
# Build parse_cache file path using class storage location variables
if self.workspace:
# If workspace exists, use workspace subdirectory
cache_file_path = (
Path(self.working_dir)
/ self.workspace
/ "kv_store_parse_cache.json"
)
else:
# Default to using working_dir
cache_file_path = Path(self.working_dir) / "kv_store_parse_cache.json"
# Check if parse_cache file exists
if not cache_file_path.exists():
logger.warning(f"Parse cache file not found: {cache_file_path}")
result["not_found"] = doc_ids.copy()
return result
# Read current parse_cache data
with open(cache_file_path, "r", encoding="utf-8") as f:
cache_data = json.load(f)
# Find entries to delete and record found doc_ids
entries_to_delete = []
doc_ids_set = set(doc_ids)
found_doc_ids = set()
for cache_key, cache_entry in cache_data.items():
if (
isinstance(cache_entry, dict)
and cache_entry.get("doc_id") in doc_ids_set
):
entries_to_delete.append(cache_key)
result["deleted_entries"].append(cache_key)
found_doc_ids.add(cache_entry.get("doc_id"))
# Delete found entries
for cache_key in entries_to_delete:
del cache_data[cache_key]
# Find doc_ids not found
result["not_found"] = list(doc_ids_set - found_doc_ids)
# Write back updated cache data
with open(cache_file_path, "w", encoding="utf-8") as f:
json.dump(cache_data, f, indent=2, ensure_ascii=False)
logger.info(
f"Deleted {len(entries_to_delete)} parse_cache entries, document IDs: {doc_ids}"
)
except Exception as e:
error_msg = f"Error cleaning parse_cache: {str(e)}"
logger.error(error_msg)
result["error"] = error_msg
return result
def clean_parse_cache_by_doc_ids(self, doc_ids: str | list[str]) -> dict[str, Any]:
"""Synchronously clean parse_cache entries for specified document IDs
Args:
doc_ids: Single document ID string or list of document IDs
Returns:
Dictionary containing cleanup results
"""
loop = always_get_an_event_loop()
return loop.run_until_complete(self.aclean_parse_cache_by_doc_ids(doc_ids))
async def aclean_all_parse_cache(self) -> dict[str, Any]:
"""Asynchronously clean all parse_cache entries
Returns:
Dictionary containing cleanup results:
- deleted_count: Number of deleted entries
- error: Error message (if operation fails)
"""
import json
from pathlib import Path
result = {"deleted_count": 0, "error": None}
try:
# Build parse_cache file path
if self.workspace:
cache_file_path = (
Path(self.working_dir)
/ self.workspace
/ "kv_store_parse_cache.json"
)
else:
cache_file_path = Path(self.working_dir) / "kv_store_parse_cache.json"
if not cache_file_path.exists():
logger.warning(f"Parse cache file not found: {cache_file_path}")
return result
# Read current cache to count entries
with open(cache_file_path, "r", encoding="utf-8") as f:
cache_data = json.load(f)
result["deleted_count"] = len(cache_data)
# Clear all entries
with open(cache_file_path, "w", encoding="utf-8") as f:
json.dump({}, f, indent=2)
logger.info(f"Cleared all {result['deleted_count']} parse_cache entries")
except Exception as e:
error_msg = f"Error clearing parse_cache: {str(e)}"
logger.error(error_msg)
result["error"] = error_msg
return result
def clean_all_parse_cache(self) -> dict[str, Any]:
"""Synchronously clean all parse_cache entries
Returns:
Dictionary containing cleanup results
"""
loop = always_get_an_event_loop()
return loop.run_until_complete(self.aclean_all_parse_cache())
async def adelete_by_doc_id(self, doc_id: str) -> DeletionResult: async def adelete_by_doc_id(self, doc_id: str) -> DeletionResult:
"""Delete a document and all its related data, including chunks, graph elements, and cached entries. """Delete a document and all its related data, including chunks, graph elements, and cached entries.

View file

@ -1936,7 +1936,29 @@ async def merge_nodes_and_edges(
if full_entities_storage and full_relations_storage and doc_id: if full_entities_storage and full_relations_storage and doc_id:
try: try:
# Merge all entities: original entities + entities added during edge processing # Merge all entities: original entities + entities added during edge processing
final_entity_names = set() existing_entites_data = None
existing_relations_data = None
try:
existing_entites_data = await full_entities_storage.get_by_id(doc_id)
existing_relations_data = await full_relations_storage.get_by_id(doc_id)
except Exception as e:
logger.debug(
f"Could not retrieve existing entity/relation data for {doc_id}: {e}"
)
existing_entites_names = set()
if existing_entites_data and existing_entites_data.get("entity_names"):
existing_entites_names.update(existing_entites_data["entity_names"])
existing_relation_pairs = set()
if existing_relations_data and existing_relations_data.get(
"relation_pairs"
):
for pair in existing_relations_data["relation_pairs"]:
existing_relation_pairs.add(tuple(sorted(pair)))
final_entity_names = existing_entites_names.copy()
# Add original processed entities # Add original processed entities
for entity_data in processed_entities: for entity_data in processed_entities:
@ -1949,7 +1971,7 @@ async def merge_nodes_and_edges(
final_entity_names.add(added_entity["entity_name"]) final_entity_names.add(added_entity["entity_name"])
# Collect all relation pairs # Collect all relation pairs
final_relation_pairs = set() final_relation_pairs = existing_relation_pairs.copy()
for edge_data in processed_edges: for edge_data in processed_edges:
if edge_data: if edge_data:
src_id = edge_data.get("src_id") src_id = edge_data.get("src_id")
@ -1959,6 +1981,12 @@ async def merge_nodes_and_edges(
final_relation_pairs.add(relation_pair) final_relation_pairs.add(relation_pair)
log_message = f"Phase 3: Updating final {len(final_entity_names)}({len(processed_entities)}+{len(all_added_entities)}) entities and {len(final_relation_pairs)} relations from {doc_id}" log_message = f"Phase 3: Updating final {len(final_entity_names)}({len(processed_entities)}+{len(all_added_entities)}) entities and {len(final_relation_pairs)} relations from {doc_id}"
new_entities_count = len(final_entity_names) - len(existing_entites_names)
new_relation_count = len(final_relation_pairs) - len(
existing_relation_pairs
)
log_message = f"Phase 3: Merging storage - existing: {len(existing_entites_names)} entitites, {len(existing_relation_pairs)} relations; new: {new_entities_count} entities. {new_relation_count} relations; total: {len(final_entity_names)} entities, {len(final_relation_pairs)} relations"
logger.info(log_message) logger.info(log_message)
async with pipeline_status_lock: async with pipeline_status_lock:
pipeline_status["latest_message"] = log_message pipeline_status["latest_message"] = log_message

View file

@ -10,9 +10,11 @@ PROMPTS["DEFAULT_COMPLETION_DELIMITER"] = "<|COMPLETE|>"
PROMPTS["DEFAULT_USER_PROMPT"] = "n/a" PROMPTS["DEFAULT_USER_PROMPT"] = "n/a"
PROMPTS["entity_extraction_system_prompt"] = """---Role--- PROMPTS["entity_extraction_system_prompt"] = """---Role---
You are a Knowledge Graph Specialist responsible for extracting entities and relationships from the input text. You are a Knowledge Graph Specialist responsible for extracting entities and relationships from the input text.
---Instructions--- ---Instructions---
1. **Entity Extraction & Output:** 1. **Entity Extraction & Output:**
* **Identification:** Identify clearly defined and meaningful entities in the input text. * **Identification:** Identify clearly defined and meaningful entities in the input text.
@ -58,6 +60,7 @@ You are a Knowledge Graph Specialist responsible for extracting entities and rel
8. **Completion Signal:** Output the literal string `{completion_delimiter}` only after all entities and relationships, following all criteria, have been completely extracted and outputted. 8. **Completion Signal:** Output the literal string `{completion_delimiter}` only after all entities and relationships, following all criteria, have been completely extracted and outputted.
---Examples--- ---Examples---
{examples} {examples}

18
lightrag/ragmanager.py Normal file
View file

@ -0,0 +1,18 @@
class RAGManager:
_instance = None
_rag = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def set_rag(cls, rag_instance):
cls._rag = rag_instance
@classmethod
def get_rag(cls):
if cls._rag is None:
raise ValueError("RAG instance not initialized!")
return cls._rag

View file

@ -15,6 +15,7 @@ import GraphViewer from '@/features/GraphViewer'
import DocumentManager from '@/features/DocumentManager' import DocumentManager from '@/features/DocumentManager'
import RetrievalTesting from '@/features/RetrievalTesting' import RetrievalTesting from '@/features/RetrievalTesting'
import ApiSite from '@/features/ApiSite' import ApiSite from '@/features/ApiSite'
import { SchemeProvider } from '@/contexts/SchemeContext';
import { Tabs, TabsContent } from '@/components/ui/Tabs' import { Tabs, TabsContent } from '@/components/ui/Tabs'
@ -204,9 +205,11 @@ function App() {
> >
<SiteHeader /> <SiteHeader />
<div className="relative grow"> <div className="relative grow">
<SchemeProvider>
<TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0 overflow-auto"> <TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0 overflow-auto">
<DocumentManager /> <DocumentManager />
</TabsContent> </TabsContent>
</SchemeProvider>
<TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0 overflow-hidden"> <TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0 overflow-hidden">
<GraphViewer /> <GraphViewer />
</TabsContent> </TabsContent>

View file

@ -161,7 +161,7 @@ export type DeleteDocResponse = {
doc_id: string doc_id: string
} }
export type DocStatus = 'pending' | 'processing' | 'processed' | 'failed' export type DocStatus = 'pending' | 'processing' | 'processed' | 'ready' | 'handling' | 'failed'
export type DocStatusResponse = { export type DocStatusResponse = {
id: string id: string
@ -175,6 +175,7 @@ export type DocStatusResponse = {
error_msg?: string error_msg?: string
metadata?: Record<string, any> metadata?: Record<string, any>
file_path: string file_path: string
scheme_name: string
} }
export type DocsStatusesResponse = { export type DocsStatusesResponse = {
@ -252,6 +253,24 @@ export type LoginResponse = {
webui_description?: string webui_description?: string
} }
export type Scheme = {
id: number;
name: string;
config: {
framework: 'lightrag' | 'raganything';
extractor?: 'mineru' | 'docling' | undefined; // Optional extractor field
modelSource?: 'huggingface' | 'modelscope' | 'local' | undefined; // Optional model source field
};
};
type AddSchemeParams = Omit<Scheme, 'id'>;
export type SchemesResponse = {
status: string;
message: string;
data: Scheme[];
};
export const InvalidApiKeyError = 'Invalid API Key' export const InvalidApiKeyError = 'Invalid API Key'
export const RequireApiKeError = 'API Key required' export const RequireApiKeError = 'API Key required'
@ -305,6 +324,32 @@ axiosInstance.interceptors.response.use(
) )
// API methods // API methods
export const getSchemes = async (): Promise<SchemesResponse> => {
const response = await axiosInstance.get('/documents/schemes');
return response.data;
};
export const saveSchemes = async (schemes: Scheme[]): Promise<{ message: string }> => {
const response = await axiosInstance.post('/documents/schemes', schemes);
return response.data;
};
export const addScheme = async (scheme: AddSchemeParams): Promise<Scheme> => {
try {
const response = await axiosInstance.post('/documents/schemes/add', scheme);
// 验证响应数据是否符合 Scheme 类型(可选,取决于 axios 的配置)
return response.data;
} catch (error) {
console.error('Failed to add scheme:', error);
throw error; // 重新抛出错误,由调用方处理
}
};
export const deleteScheme = async (schemeId: number): Promise<{ message: string }> => {
const response = await axiosInstance.delete(`/documents/schemes/${schemeId}`);
return response.data;
};
export const queryGraphs = async ( export const queryGraphs = async (
label: string, label: string,
maxDepth: number, maxDepth: number,
@ -338,8 +383,10 @@ export const getDocuments = async (): Promise<DocsStatusesResponse> => {
return response.data return response.data
} }
export const scanNewDocuments = async (): Promise<ScanResponse> => { export const scanNewDocuments = async (schemeConfig: any): Promise<ScanResponse> => {
const response = await axiosInstance.post('/documents/scan') const response = await axiosInstance.post('/documents/scan', {
schemeConfig
})
return response.data return response.data
} }
@ -550,10 +597,12 @@ export const insertTexts = async (texts: string[]): Promise<DocActionResponse> =
export const uploadDocument = async ( export const uploadDocument = async (
file: File, file: File,
schemeId: number | '',
onUploadProgress?: (percentCompleted: number) => void onUploadProgress?: (percentCompleted: number) => void
): Promise<DocActionResponse> => { ): Promise<DocActionResponse> => {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
formData.append('schemeId', schemeId.toString())
const response = await axiosInstance.post('/documents/upload', formData, { const response = await axiosInstance.post('/documents/upload', formData, {
headers: { headers: {
@ -573,11 +622,12 @@ export const uploadDocument = async (
export const batchUploadDocuments = async ( export const batchUploadDocuments = async (
files: File[], files: File[],
schemeId: number | '',
onUploadProgress?: (fileName: string, percentCompleted: number) => void onUploadProgress?: (fileName: string, percentCompleted: number) => void
): Promise<DocActionResponse[]> => { ): Promise<DocActionResponse[]> => {
return await Promise.all( return await Promise.all(
files.map(async (file) => { files.map(async (file) => {
return await uploadDocument(file, (percentCompleted) => { return await uploadDocument(file, schemeId, (percentCompleted) => {
onUploadProgress?.(file.name, percentCompleted) onUploadProgress?.(file.name, percentCompleted)
}) })
}) })

View file

@ -0,0 +1,165 @@
.scheme-manager-container {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
}
.toggle-button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-bottom: 20px;
}
.toggle-button:hover {
background-color: #45a049;
}
.scheme-modal {
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.modal-header h2 {
margin: 0;
font-size: 18px;
}
.close-button {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.close-button:hover {
color: #333;
}
.modal-content {
display: flex;
height: 500px;
}
.left-panel {
flex: 0 0 300px;
padding: 16px;
border-right: 1px solid #ddd;
overflow-y: auto;
}
.right-panel {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.add-scheme-form {
display: flex;
margin-bottom: 16px;
gap: 8px;
}
.scheme-name-input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.add-button {
padding: 8px 12px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-button:hover {
background-color: #0b7dda;
}
.scheme-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.scheme-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background-color: #f9f9f9;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.scheme-item:hover {
background-color: #f0f0f0;
}
.scheme-item.active {
background-color: #e3f2fd;
border-left: 3px solid #2196F3;
}
.delete-button {
background: none;
border: none;
color: #f44336;
cursor: pointer;
font-size: 16px;
padding: 0 4px;
}
.delete-button:hover {
color: #d32f2f;
}
.empty-message, .select-message {
color: #666;
text-align: center;
padding: 20px;
}
.config-form {
margin-top: 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}

View file

@ -0,0 +1,299 @@
import React, { useRef,useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/Dialog';
import Button from "@/components/ui/Button";
import { PlusIcon } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/Alert";
import { AlertCircle } from "lucide-react";
import {
getSchemes,
saveSchemes,
addScheme,
deleteScheme,
Scheme
} from '@/api/lightrag';
import { useScheme } from '@/contexts/SchemeContext';
import { useTranslation } from 'react-i18next';
interface SchemeConfig {
framework: 'lightrag' | 'raganything';
extractor?: 'mineru' | 'docling';
modelSource?: 'huggingface' | 'modelscope' | 'local';
}
const SchemeManagerDialog = () => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [schemes, setSchemes] = useState<Scheme[]>([]);
const [newSchemeName, setNewSchemeName] = useState("");
const [error, _setError] = useState<string | undefined>();
const [isLoading, setIsLoading] = useState(true);
const setError = (err?: string) => _setError(err);
const scrollRef = useRef<HTMLDivElement>(null);
const { selectedScheme, setSelectedScheme } = useScheme();
// 加载方案数据
useEffect(() => {
const loadSchemes = async () => {
try {
setIsLoading(true);
const response = await getSchemes();
setSchemes(response.data);
localStorage.getItem('selectedSchemeId') && setSelectedScheme(response.data.find(s => s.id === Number(localStorage.getItem('selectedSchemeId'))) || undefined);
} catch (err) {
setError(err instanceof Error ? err.message : t('schemeManager.errors.loadFailed'));
} finally {
setIsLoading(false);
}
};
loadSchemes();
}, []);
// 自动滚动到底部
useEffect(() => {
handleSelectScheme(selectedScheme?.id!);
if (!scrollRef.current) return;
const scrollToBottom = () => {
const container = scrollRef.current!;
const { scrollHeight } = container;
container.scrollTop = scrollHeight;
};
setTimeout(scrollToBottom, 0);
}, [schemes]);
// 检查方案名是否已存在
const isNameTaken = (name: string): boolean => {
return schemes.some(scheme => scheme.name.trim() === name.trim());
};
// 选中方案(更新 Context
const handleSelectScheme = (schemeId: number) => {
const scheme = schemes.find((s) => s.id === schemeId);
if (scheme) {
setSelectedScheme(scheme);
localStorage.setItem('selectedSchemeId', String(scheme.id));
}
};
// 添加新方案
const handleAddScheme = async () => {
const trimmedName = newSchemeName.trim();
if (!trimmedName) {
setError(t('schemeManager.errors.nameEmpty'));
return;
}
if (isNameTaken(trimmedName)) {
setError(t('schemeManager.errors.nameExists'));
return;
}
try {
const newScheme = await addScheme({
name: trimmedName,
config: { framework: 'lightrag', extractor: undefined, modelSource: undefined },
});
// 更新方案列表
setSchemes((prevSchemes) => [...prevSchemes, newScheme]);
// 选中新方案
setSelectedScheme(newScheme);
// 清空输入和错误
setNewSchemeName("");
setError(undefined);
} catch (err) {
setError(err instanceof Error ? err.message : t('schemeManager.errors.addFailed'));
}
};
// 删除方案
const handleDeleteScheme = async (schemeId: number) => {
try {
await deleteScheme(schemeId);
setSchemes(schemes.filter(s => s.id !== schemeId));
if (selectedScheme?.id === schemeId) {
setSelectedScheme(undefined); // 清除 Context 中的选中状态
}
} catch (err) {
setError(err instanceof Error ? err.message : t('schemeManager.errors.deleteFailed'));
}
};
// 更新方案配置
const handleConfigChange = async (updates: Partial<SchemeConfig>) => {
if (!selectedScheme) return;
const updatedScheme = {
...selectedScheme,
config: {
...selectedScheme.config,
...updates,
framework: updates.framework ?? selectedScheme.config?.framework ?? 'lightrag',
extractor: updates.extractor || selectedScheme.config?.extractor || (updates.framework === 'raganything' ? 'mineru' : undefined),
modelSource: updates.modelSource || selectedScheme.config?.modelSource || (updates.extractor === 'mineru' ? 'huggingface' : undefined),
},
};
setSchemes(schemes.map(s => s.id === selectedScheme.id ? updatedScheme : s));
await saveSchemes([updatedScheme]);
};
if (isLoading) {
return (
<div className="flex justify-center items-center h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
</div>
);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="default" side="bottom" size="sm">
<PlusIcon className="size-4" />
{t('schemeManager.button')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[800px]">
<DialogHeader>
<DialogTitle>{t('schemeManager.title')}</DialogTitle>
<DialogDescription>{t('schemeManager.description')}</DialogDescription>
</DialogHeader>
<div className="flex h-[500px] gap-4">
{/* 左侧:方案列表 */}
<div className="w-1/3 rounded-lg border p-4 bg-gray-50 flex flex-col dark:bg-zinc-800 dark:text-zinc-100">
<h3 className="mb-4 font-semibold">{t('schemeManager.schemeList')}</h3>
{/* 创建新方案输入框 */}
<div className="flex gap-2 mb-4">
<input
type="text"
value={newSchemeName}
onChange={(e) => {
if (e.target.value.length > 50) return;
setNewSchemeName(e.target.value);
setError(undefined);
}}
onKeyPress={(e) => e.key === 'Enter' && handleAddScheme()}
placeholder={t('schemeManager.inputPlaceholder')}
className="w-full px-3 py-1.5 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<Button onClick={handleAddScheme} size="sm">
<PlusIcon className="size-4" />
</Button>
</div>
{/* 错误提示 */}
{error && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="size-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* 方案列表 */}
<div ref={scrollRef} className="flex-1 overflow-y-auto border rounded-md p-1 dark:bg-zinc-800 dark:text-zinc-100">
{schemes.length === 0 ? (
<p className="text-gray-500 text-center py-4">{t('schemeManager.emptySchemes')}</p>
) : (
<div className="space-y-2">
{schemes.map((scheme) => (
<div
key={scheme.id}
className={`flex items-center justify-between p-2 rounded-md cursor-pointer transition-colors truncate ${
selectedScheme?.id === scheme.id
? "bg-blue-100 text-blue-700"
: "hover:bg-gray-100"
}`}
onClick={() => handleSelectScheme(scheme.id)}
>
<div className="flex-1 truncate mr-2" title={scheme.name}>
{scheme.name}
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteScheme(scheme.id);
}}
className="ml-2 text-red-500 hover:text-red-700 hover:bg-red-100 rounded-full p-1 transition-colors"
title={t('schemeManager.deleteTooltip')}
>
</button>
</div>
))}
</div>
)}
</div>
</div>
{/* 右侧:方案配置 */}
<div className="flex-1 rounded-lg border p-4 bg-gray-50 dark:bg-zinc-800 dark:text-zinc-100">
<h3 className="mb-4 font-semibold">{t('schemeManager.schemeConfig')}</h3>
{selectedScheme ? (
<div className="space-y-4">
<div>
<label className="block text-sm mb-1">{t('schemeManager.processingFramework')}</label>
<select
value={selectedScheme.config?.framework || "lightrag"}
onChange={(e) => handleConfigChange({ framework: e.target.value as 'lightrag' | 'raganything' })}
className="w-full px-3 py-1.5 border rounded-md focus:outline-none dark:bg-zinc-800 dark:text-zinc-100"
>
<option value="lightrag">LightRAG</option>
<option value="raganything">RAGAnything</option>
</select>
</div>
{selectedScheme.config?.framework === "raganything" && (
<div>
<label className="block text-sm mb-1">{t('schemeManager.extractionTool')}</label>
<select
value={selectedScheme.config?.extractor || "mineru"}
onChange={(e) => handleConfigChange({ extractor: e.target.value as 'mineru' | 'docling' })}
className="w-full px-3 py-1.5 border rounded-md focus:outline-none dark:bg-zinc-800 dark:text-zinc-100"
>
<option value="mineru">Mineru</option>
<option value="docling">DocLing</option>
</select>
</div>
)}
{selectedScheme.config?.extractor === "mineru" && (
<div>
<label className="block text-sm mb-1">{t('schemeManager.modelSource')}</label>
<select
value={selectedScheme.config?.modelSource || "huggingface"}
onChange={(e) => handleConfigChange({ modelSource: e.target.value as 'huggingface' | 'modelscope' | 'local' })}
className="w-full px-3 py-1.5 border rounded-md focus:outline-none dark:bg-zinc-800 dark:text-zinc-100"
>
<option value="huggingface">huggingface</option>
<option value="modelscope">modelscope</option>
<option value="local">local</option>
</select>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center h-[70%] text-gray-500">
<AlertCircle className="size-12 mb-4 opacity-50" />
<p>{t('schemeManager.selectSchemePrompt')}</p>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
};
export default SchemeManagerDialog;

View file

@ -16,6 +16,7 @@ import { uploadDocument } from '@/api/lightrag'
import { UploadIcon } from 'lucide-react' import { UploadIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useScheme } from '@/contexts/SchemeContext';
interface UploadDocumentsDialogProps { interface UploadDocumentsDialogProps {
onDocumentsUploaded?: () => Promise<void> onDocumentsUploaded?: () => Promise<void>
@ -27,6 +28,7 @@ export default function UploadDocumentsDialog({ onDocumentsUploaded }: UploadDoc
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
const [progresses, setProgresses] = useState<Record<string, number>>({}) const [progresses, setProgresses] = useState<Record<string, number>>({})
const [fileErrors, setFileErrors] = useState<Record<string, string>>({}) const [fileErrors, setFileErrors] = useState<Record<string, string>>({})
const { selectedScheme } = useScheme();
const handleRejectedFiles = useCallback( const handleRejectedFiles = useCallback(
(rejectedFiles: FileRejection[]) => { (rejectedFiles: FileRejection[]) => {
@ -58,6 +60,11 @@ export default function UploadDocumentsDialog({ onDocumentsUploaded }: UploadDoc
const handleDocumentsUpload = useCallback( const handleDocumentsUpload = useCallback(
async (filesToUpload: File[]) => { async (filesToUpload: File[]) => {
if (!selectedScheme) {
toast.error(t('schemeManager.upload.noSchemeSelected'));
return;
}
setIsUploading(true) setIsUploading(true)
let hasSuccessfulUpload = false let hasSuccessfulUpload = false
@ -95,7 +102,7 @@ export default function UploadDocumentsDialog({ onDocumentsUploaded }: UploadDoc
[file.name]: 0 [file.name]: 0
})) }))
const result = await uploadDocument(file, (percentCompleted: number) => { const result = await uploadDocument(file, selectedScheme?.id, (percentCompleted: number) => {
console.debug(t('documentPanel.uploadDocuments.single.uploading', { name: file.name, percent: percentCompleted })) console.debug(t('documentPanel.uploadDocuments.single.uploading', { name: file.name, percent: percentCompleted }))
setProgresses((pre) => ({ setProgresses((pre) => ({
...pre, ...pre,
@ -175,7 +182,7 @@ export default function UploadDocumentsDialog({ onDocumentsUploaded }: UploadDoc
setIsUploading(false) setIsUploading(false)
} }
}, },
[setIsUploading, setProgresses, setFileErrors, t, onDocumentsUploaded] [setIsUploading, setProgresses, setFileErrors, t, onDocumentsUploaded, selectedScheme]
) )
return ( return (
@ -201,7 +208,11 @@ export default function UploadDocumentsDialog({ onDocumentsUploaded }: UploadDoc
<DialogHeader> <DialogHeader>
<DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle> <DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle>
<DialogDescription> <DialogDescription>
{t('documentPanel.uploadDocuments.description')} {selectedScheme ? (
<>{t('schemeManager.upload.currentScheme')}<strong>{selectedScheme.name}</strong></>
) : (
t('schemeManager.upload.noSchemeMessage')
)}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FileUploader <FileUploader

View file

@ -0,0 +1,28 @@
// contexts/SchemeContext.tsx
import React, { createContext, useContext, useState } from 'react';
import { Scheme } from '@/api/lightrag';
interface SchemeContextType {
selectedScheme: Scheme | undefined;
setSelectedScheme: (scheme: Scheme | undefined) => void;
}
const SchemeContext = createContext<SchemeContextType | undefined>(undefined);
export const SchemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [selectedScheme, setSelectedScheme] = useState<Scheme | undefined>();
return (
<SchemeContext.Provider value={{ selectedScheme, setSelectedScheme }}>
{children}
</SchemeContext.Provider>
);
};
export const useScheme = () => {
const context = useContext(SchemeContext);
if (context === undefined) {
throw new Error('useScheme must be used within a SchemeProvider');
}
return context;
};

View file

@ -18,6 +18,8 @@ import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog' import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
import DeleteDocumentsDialog from '@/components/documents/DeleteDocumentsDialog' import DeleteDocumentsDialog from '@/components/documents/DeleteDocumentsDialog'
import PaginationControls from '@/components/ui/PaginationControls' import PaginationControls from '@/components/ui/PaginationControls'
import { SchemeProvider } from '@/contexts/SchemeContext';
import SchemeManager from '@/components/documents/SchemeManager/SchemeManager'
import { import {
scanNewDocuments, scanNewDocuments,
@ -35,6 +37,8 @@ import { useBackendState } from '@/stores/state'
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, RotateCcwIcon, CheckSquareIcon, XIcon, AlertTriangle, Info } from 'lucide-react' import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, RotateCcwIcon, CheckSquareIcon, XIcon, AlertTriangle, Info } from 'lucide-react'
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog' import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
import { useScheme } from '@/contexts/SchemeContext';
type StatusFilter = DocStatus | 'all'; type StatusFilter = DocStatus | 'all';
@ -169,6 +173,8 @@ type SortField = 'created_at' | 'updated_at' | 'id' | 'file_path';
type SortDirection = 'asc' | 'desc'; type SortDirection = 'asc' | 'desc';
export default function DocumentManager() { export default function DocumentManager() {
const { selectedScheme } = useScheme();
// Track component mount status // Track component mount status
const isMountedRef = useRef(true); const isMountedRef = useRef(true);
@ -230,6 +236,8 @@ export default function DocumentManager() {
processing: 1, processing: 1,
pending: 1, pending: 1,
failed: 1, failed: 1,
ready: 1,
handling: 1
}); });
// State for document selection // State for document selection
@ -296,6 +304,8 @@ export default function DocumentManager() {
processing: 1, processing: 1,
pending: 1, pending: 1,
failed: 1, failed: 1,
ready: 1,
handling: 1
}); });
}; };
@ -441,7 +451,9 @@ export default function DocumentManager() {
const prevStatusCounts = useRef({ const prevStatusCounts = useRef({
processed: 0, processed: 0,
processing: 0, processing: 0,
handling: 0,
pending: 0, pending: 0,
ready: 0,
failed: 0 failed: 0
}) })
@ -532,6 +544,8 @@ export default function DocumentManager() {
processed: response.documents.filter((doc: DocStatusResponse) => doc.status === 'processed'), processed: response.documents.filter((doc: DocStatusResponse) => doc.status === 'processed'),
processing: response.documents.filter((doc: DocStatusResponse) => doc.status === 'processing'), processing: response.documents.filter((doc: DocStatusResponse) => doc.status === 'processing'),
pending: response.documents.filter((doc: DocStatusResponse) => doc.status === 'pending'), pending: response.documents.filter((doc: DocStatusResponse) => doc.status === 'pending'),
ready: response.documents.filter((doc: DocStatusResponse) => doc.status === 'ready'),
handling: response.documents.filter((doc: DocStatusResponse) => doc.status === 'handling'),
failed: response.documents.filter((doc: DocStatusResponse) => doc.status === 'failed') failed: response.documents.filter((doc: DocStatusResponse) => doc.status === 'failed')
} }
}; };
@ -794,7 +808,14 @@ export default function DocumentManager() {
// Check if component is still mounted before starting the request // Check if component is still mounted before starting the request
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
const { status, message, track_id: _track_id } = await scanNewDocuments(); // eslint-disable-line @typescript-eslint/no-unused-vars if (!selectedScheme) {
toast.error(t('documentPanel.documentManager.errors.missingSchemeId'));
return;
}
const schemeConfig = selectedScheme.config
const { status, message, track_id: _track_id } = await scanNewDocuments(schemeConfig); // eslint-disable-line @typescript-eslint/no-unused-vars
// Check again if component is still mounted after the request completes // Check again if component is still mounted after the request completes
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
@ -820,10 +841,10 @@ export default function DocumentManager() {
} catch (err) { } catch (err) {
// Only show error if component is still mounted // Only show error if component is still mounted
if (isMountedRef.current) { if (isMountedRef.current) {
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) })); toast.error(t('documentPanel.documentManager.errors.scanFiled', { error: errorMessage(err) }));
} }
} }
}, [t, startPollingInterval, currentTab, health, statusCounts]) }, [t, startPollingInterval, currentTab, health, statusCounts, selectedScheme])
// Handle page size change - update state and save to store // Handle page size change - update state and save to store
const handlePageSizeChange = useCallback((newPageSize: number) => { const handlePageSizeChange = useCallback((newPageSize: number) => {
@ -839,6 +860,8 @@ export default function DocumentManager() {
processing: 1, processing: 1,
pending: 1, pending: 1,
failed: 1, failed: 1,
ready: 1,
handling: 1
}); });
setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize })); setPagination(prev => ({ ...prev, page: 1, page_size: newPageSize }));
@ -878,6 +901,8 @@ export default function DocumentManager() {
processed: response.documents.filter(doc => doc.status === 'processed'), processed: response.documents.filter(doc => doc.status === 'processed'),
processing: response.documents.filter(doc => doc.status === 'processing'), processing: response.documents.filter(doc => doc.status === 'processing'),
pending: response.documents.filter(doc => doc.status === 'pending'), pending: response.documents.filter(doc => doc.status === 'pending'),
ready: response.documents.filter((doc: DocStatusResponse) => doc.status === 'ready'),
handling: response.documents.filter((doc: DocStatusResponse) => doc.status === 'handling'),
failed: response.documents.filter(doc => doc.status === 'failed') failed: response.documents.filter(doc => doc.status === 'failed')
} }
}; };
@ -945,7 +970,9 @@ export default function DocumentManager() {
const newStatusCounts = { const newStatusCounts = {
processed: docs?.statuses?.processed?.length || 0, processed: docs?.statuses?.processed?.length || 0,
processing: docs?.statuses?.processing?.length || 0, processing: docs?.statuses?.processing?.length || 0,
handling: docs?.statuses?.handling?.length || 0,
pending: docs?.statuses?.pending?.length || 0, pending: docs?.statuses?.pending?.length || 0,
ready: docs?.statuses?.ready?.length || 0,
failed: docs?.statuses?.failed?.length || 0 failed: docs?.statuses?.failed?.length || 0
} }
@ -1133,6 +1160,7 @@ export default function DocumentManager() {
<ClearDocumentsDialog onDocumentsCleared={handleDocumentsCleared} /> <ClearDocumentsDialog onDocumentsCleared={handleDocumentsCleared} />
) : null} ) : null}
<UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} /> <UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} />
<SchemeManager />
<PipelineStatusDialog <PipelineStatusDialog
open={showPipelineStatus} open={showPipelineStatus}
onOpenChange={setShowPipelineStatus} onOpenChange={setShowPipelineStatus}
@ -1181,6 +1209,17 @@ export default function DocumentManager() {
> >
{t('documentPanel.documentManager.status.processing')} ({statusCounts.PROCESSING || statusCounts.processing || 0}) {t('documentPanel.documentManager.status.processing')} ({statusCounts.PROCESSING || statusCounts.processing || 0})
</Button> </Button>
<Button
size="sm"
variant={statusFilter === 'handling' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('handling')}
className={cn(
documentCounts.handling > 0 ? 'text-purple-600' : 'text-gray-500',
statusFilter === 'handling' && 'bg-purple-100 dark:bg-purple-900/30 font-medium border border-purple-400 dark:border-purple-600 shadow-sm'
)}
>
{t('documentPanel.documentManager.status.handling')} ({statusCounts.HANDLING || statusCounts.handling || 0})
</Button>
<Button <Button
size="sm" size="sm"
variant={statusFilter === 'pending' ? 'secondary' : 'outline'} variant={statusFilter === 'pending' ? 'secondary' : 'outline'}
@ -1193,6 +1232,17 @@ export default function DocumentManager() {
> >
{t('documentPanel.documentManager.status.pending')} ({statusCounts.PENDING || statusCounts.pending || 0}) {t('documentPanel.documentManager.status.pending')} ({statusCounts.PENDING || statusCounts.pending || 0})
</Button> </Button>
<Button
size="sm"
variant={statusFilter === 'ready' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('ready')}
className={cn(
documentCounts.ready > 0 ? 'text-gray-600' : 'text-gray-500',
statusFilter === 'ready' && 'bg-gray-100 dark:bg-gray-900/30 font-medium border border-gray-400 dark:border-gray-600 shadow-sm'
)}
>
{t('documentPanel.documentManager.status.ready')} ({statusCounts.READY || statusCounts.ready || 0})
</Button>
<Button <Button
size="sm" size="sm"
variant={statusFilter === 'failed' ? 'secondary' : 'outline'} variant={statusFilter === 'failed' ? 'secondary' : 'outline'}
@ -1273,6 +1323,7 @@ export default function DocumentManager() {
</div> </div>
</TableHead> </TableHead>
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead> <TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
<TableHead>{t('documentPanel.documentManager.columns.handler')}</TableHead>
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead> <TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead> <TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead> <TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
@ -1344,6 +1395,9 @@ export default function DocumentManager() {
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell className="truncate max-w-[150px]">
{doc.scheme_name || '-'}
</TableCell>
<TableCell> <TableCell>
<div className="group relative flex items-center overflow-visible tooltip-container"> <div className="group relative flex items-center overflow-visible tooltip-container">
{doc.status === 'processed' && ( {doc.status === 'processed' && (
@ -1358,6 +1412,12 @@ export default function DocumentManager() {
{doc.status === 'failed' && ( {doc.status === 'failed' && (
<span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span> <span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span>
)} )}
{doc.status === 'ready' && (
<span className="text-purple-600">{t('documentPanel.documentManager.status.ready')}</span>
)}
{doc.status === 'handling' && (
<span className="text-gray-600">{t('documentPanel.documentManager.status.handling')}</span>
)}
{/* Icon rendering logic */} {/* Icon rendering logic */}
{doc.error_msg ? ( {doc.error_msg ? (
@ -1382,10 +1442,10 @@ export default function DocumentManager() {
<TableCell>{doc.content_length ?? '-'}</TableCell> <TableCell>{doc.content_length ?? '-'}</TableCell>
<TableCell>{doc.chunks_count ?? '-'}</TableCell> <TableCell>{doc.chunks_count ?? '-'}</TableCell>
<TableCell className="truncate"> <TableCell className="truncate">
{new Date(doc.created_at).toLocaleString()} {doc.created_at ? new Date(doc.created_at).toLocaleString() : '-'}
</TableCell> </TableCell>
<TableCell className="truncate"> <TableCell className="truncate">
{new Date(doc.updated_at).toLocaleString()} {doc.updated_at ? new Date(doc.updated_at).toLocaleString() : '-'}
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
<Checkbox <Checkbox

View file

@ -126,6 +126,7 @@
"id": "المعرف", "id": "المعرف",
"fileName": "اسم الملف", "fileName": "اسم الملف",
"summary": "الملخص", "summary": "الملخص",
"handler": "المعالج",
"status": "الحالة", "status": "الحالة",
"length": "الطول", "length": "الطول",
"chunks": "الأجزاء", "chunks": "الأجزاء",
@ -138,13 +139,16 @@
"all": "الكل", "all": "الكل",
"completed": "مكتمل", "completed": "مكتمل",
"processing": "قيد المعالجة", "processing": "قيد المعالجة",
"handling": "استخراج",
"pending": "معلق", "pending": "معلق",
"ready": "جاهز",
"failed": "فشل" "failed": "فشل"
}, },
"errors": { "errors": {
"loadFailed": "فشل تحميل المستندات\n{{error}}", "loadFailed": "فشل تحميل المستندات\n{{error}}",
"scanFailed": "فشل مسح المستندات\n{{error}}", "scanFailed": "فشل مسح المستندات\n{{error}}",
"scanProgressFailed": "فشل الحصول على تقدم المسح\n{{error}}" "scanProgressFailed": "فشل الحصول على تقدم المسح\n{{error}}",
"missingSchemeId": "الحل هو في عداد المفقودين ، حدد الحل"
}, },
"fileNameLabel": "اسم الملف", "fileNameLabel": "اسم الملف",
"showButton": "عرض", "showButton": "عرض",
@ -422,5 +426,31 @@
"prevPage": "الصفحة السابقة", "prevPage": "الصفحة السابقة",
"nextPage": "الصفحة التالية", "nextPage": "الصفحة التالية",
"lastPage": "الصفحة الأخيرة" "lastPage": "الصفحة الأخيرة"
},
"schemeManager": {
"button": "مخططات معالجة المستندات",
"title": "مدير المخططات",
"description": "إنشاء مخططات جديدة وتكوين الخيارات",
"schemeList": "قائمة المخططات",
"schemeConfig": "تكوين المخطط",
"inputPlaceholder": "أدخل اسم المخطط",
"deleteTooltip": "حذف المخطط",
"emptySchemes": "لا توجد مخططات متاحة",
"selectSchemePrompt": "يرجى تحديد أو إنشاء مخطط أولاً",
"processingFramework": "إطار المعالجة",
"extractionTool": "أداة الاستخراج",
"modelSource": "مصدر النموذج",
"errors": {
"loadFailed": "فشل تحميل المخططات",
"nameEmpty": "لا يمكن أن يكون اسم المخطط فارغًا",
"nameExists": "اسم المخطط موجود بالفعل",
"addFailed": "فشل إضافة المخطط",
"deleteFailed": "فشل حذف المخطط"
},
"upload": {
"noSchemeSelected": "لم يتم تحديد مخطط معالجة، يرجى تحديد مخطط معالجة المستندات أولاً!",
"currentScheme": "المخطط الحالي: ",
"noSchemeMessage": "لم يتم تحديد مخطط معالجة، يرجى إضافة وتحديد واحد أولاً"
}
} }
} }

View file

@ -126,6 +126,7 @@
"id": "ID", "id": "ID",
"fileName": "File Name", "fileName": "File Name",
"summary": "Summary", "summary": "Summary",
"handler": "Handler",
"status": "Status", "status": "Status",
"length": "Length", "length": "Length",
"chunks": "Chunks", "chunks": "Chunks",
@ -138,13 +139,16 @@
"all": "All", "all": "All",
"completed": "Completed", "completed": "Completed",
"processing": "Processing", "processing": "Processing",
"handling": "Handling",
"pending": "Pending", "pending": "Pending",
"ready": "Ready",
"failed": "Failed" "failed": "Failed"
}, },
"errors": { "errors": {
"loadFailed": "Failed to load documents\n{{error}}", "loadFailed": "Failed to load documents\n{{error}}",
"scanFailed": "Failed to scan documents\n{{error}}", "scanFailed": "Failed to scan documents\n{{error}}",
"scanProgressFailed": "Failed to get scan progress\n{{error}}" "scanProgressFailed": "Failed to get scan progress\n{{error}}",
"missingSchemeId": "Lack of solution, please select a solution"
}, },
"fileNameLabel": "File Name", "fileNameLabel": "File Name",
"showButton": "Show", "showButton": "Show",
@ -422,5 +426,31 @@
"prevPage": "Previous Page", "prevPage": "Previous Page",
"nextPage": "Next Page", "nextPage": "Next Page",
"lastPage": "Last Page" "lastPage": "Last Page"
},
"schemeManager": {
"button": "Document Processing Schemes",
"title": "Scheme Manager",
"description": "Create new schemes and configure options",
"schemeList": "Scheme List",
"schemeConfig": "Scheme Configuration",
"inputPlaceholder": "Enter scheme name",
"deleteTooltip": "Delete scheme",
"emptySchemes": "No schemes available",
"selectSchemePrompt": "Please select or create a scheme first",
"processingFramework": "Processing Framework",
"extractionTool": "Extraction Tool",
"modelSource": "Model Source",
"errors": {
"loadFailed": "Failed to load schemes",
"nameEmpty": "Scheme name cannot be empty",
"nameExists": "Scheme name already exists",
"addFailed": "Failed to add scheme",
"deleteFailed": "Failed to delete scheme"
},
"upload": {
"noSchemeSelected": "No processing scheme selected, please select a document processing scheme first!",
"currentScheme": "Current scheme: ",
"noSchemeMessage": "No processing scheme selected, please add and select one"
}
} }
} }

View file

@ -126,6 +126,7 @@
"id": "ID", "id": "ID",
"fileName": "Nom du fichier", "fileName": "Nom du fichier",
"summary": "Résumé", "summary": "Résumé",
"handler": "Gestionnaire",
"status": "Statut", "status": "Statut",
"length": "Longueur", "length": "Longueur",
"chunks": "Fragments", "chunks": "Fragments",
@ -138,13 +139,16 @@
"all": "Tous", "all": "Tous",
"completed": "Terminé", "completed": "Terminé",
"processing": "En traitement", "processing": "En traitement",
"handling": "Extraction",
"pending": "En attente", "pending": "En attente",
"ready": "Prêt",
"failed": "Échoué" "failed": "Échoué"
}, },
"errors": { "errors": {
"loadFailed": "Échec du chargement des documents\n{{error}}", "loadFailed": "Échec du chargement des documents\n{{error}}",
"scanFailed": "Échec de la numérisation des documents\n{{error}}", "scanFailed": "Échec de la numérisation des documents\n{{error}}",
"scanProgressFailed": "Échec de l'obtention de la progression de la numérisation\n{{error}}" "scanProgressFailed": "Échec de l'obtention de la progression de la numérisation\n{{error}}",
"missingSchemeId": "Schéma de traitement manquant, veuillez sélectionner un schéma de traitement"
}, },
"fileNameLabel": "Nom du fichier", "fileNameLabel": "Nom du fichier",
"showButton": "Afficher", "showButton": "Afficher",
@ -422,5 +426,31 @@
"prevPage": "Page précédente", "prevPage": "Page précédente",
"nextPage": "Page suivante", "nextPage": "Page suivante",
"lastPage": "Dernière page" "lastPage": "Dernière page"
},
"schemeManager": {
"button": "Schémas de traitement de documents",
"title": "Gestionnaire de schémas",
"description": "Créer de nouveaux schémas et configurer les options",
"schemeList": "Liste des schémas",
"schemeConfig": "Configuration du schéma",
"inputPlaceholder": "Entrer le nom du schéma",
"deleteTooltip": "Supprimer le schéma",
"emptySchemes": "Aucun schéma disponible",
"selectSchemePrompt": "Veuillez d'abord sélectionner ou créer un schéma",
"processingFramework": "Framework de traitement",
"extractionTool": "Outil d'extraction",
"modelSource": "Source du modèle",
"errors": {
"loadFailed": "Échec du chargement des schémas",
"nameEmpty": "Le nom du schéma ne peut pas être vide",
"nameExists": "Le nom du schéma existe déjà",
"addFailed": "Échec de l'ajout du schéma",
"deleteFailed": "Échec de la suppression du schéma"
},
"upload": {
"noSchemeSelected": "Aucun schéma de traitement sélectionné, veuillez d'abord sélectionner un schéma de traitement de documents !",
"currentScheme": "Schéma actuel : ",
"noSchemeMessage": "Aucun schéma de traitement sélectionné, veuillez d'abord en ajouter et en sélectionner un"
}
} }
} }

View file

@ -126,6 +126,7 @@
"id": "ID", "id": "ID",
"fileName": "文件名", "fileName": "文件名",
"summary": "摘要", "summary": "摘要",
"handler": "处理方案",
"status": "状态", "status": "状态",
"length": "长度", "length": "长度",
"chunks": "分块", "chunks": "分块",
@ -138,13 +139,16 @@
"all": "全部", "all": "全部",
"completed": "已完成", "completed": "已完成",
"processing": "处理中", "processing": "处理中",
"handling": "提取中",
"pending": "等待中", "pending": "等待中",
"ready": "准备中",
"failed": "失败" "failed": "失败"
}, },
"errors": { "errors": {
"loadFailed": "加载文档失败\n{{error}}", "loadFailed": "加载文档失败\n{{error}}",
"scanFailed": "扫描文档失败\n{{error}}", "scanFailed": "扫描文档失败\n{{error}}",
"scanProgressFailed": "获取扫描进度失败\n{{error}}" "scanProgressFailed": "获取扫描进度失败\n{{error}}",
"missingSchemeId": "缺少处理方案,请选择处理方案"
}, },
"fileNameLabel": "文件名", "fileNameLabel": "文件名",
"showButton": "显示", "showButton": "显示",
@ -422,5 +426,31 @@
"prevPage": "上一页", "prevPage": "上一页",
"nextPage": "下一页", "nextPage": "下一页",
"lastPage": "末页" "lastPage": "末页"
},
"schemeManager": {
"button": "文档处理方案",
"title": "方案管理器",
"description": "创建新方案并配置选项",
"schemeList": "方案列表",
"schemeConfig": "方案配置",
"inputPlaceholder": "输入方案名称",
"deleteTooltip": "删除方案",
"emptySchemes": "暂无方案",
"selectSchemePrompt": "请先选择或创建一个方案",
"processingFramework": "处理框架",
"extractionTool": "提取工具",
"modelSource": "模型源",
"errors": {
"loadFailed": "加载方案失败",
"nameEmpty": "方案名称不能为空",
"nameExists": "方案名称已存在",
"addFailed": "添加方案失败",
"deleteFailed": "删除方案失败"
},
"upload": {
"noSchemeSelected": "未选择处理方案,请先选择文档处理方案!",
"currentScheme": "当前方案:",
"noSchemeMessage": "未选择处理方案,请先添加选择"
}
} }
} }

View file

@ -126,6 +126,7 @@
"id": "ID", "id": "ID",
"fileName": "檔案名稱", "fileName": "檔案名稱",
"summary": "摘要", "summary": "摘要",
"handler": "處理方案",
"status": "狀態", "status": "狀態",
"length": "長度", "length": "長度",
"chunks": "分塊", "chunks": "分塊",
@ -138,13 +139,16 @@
"all": "全部", "all": "全部",
"completed": "已完成", "completed": "已完成",
"processing": "處理中", "processing": "處理中",
"handling": "提取中",
"pending": "等待中", "pending": "等待中",
"ready": "準備中",
"failed": "失敗" "failed": "失敗"
}, },
"errors": { "errors": {
"loadFailed": "載入文件失敗\n{{error}}", "loadFailed": "載入文件失敗\n{{error}}",
"scanFailed": "掃描文件失敗\n{{error}}", "scanFailed": "掃描文件失敗\n{{error}}",
"scanProgressFailed": "取得掃描進度失敗\n{{error}}" "scanProgressFailed": "取得掃描進度失敗\n{{error}}",
"missingSchemeId": "缺少處理方案,請選擇處理方案"
}, },
"fileNameLabel": "檔案名稱", "fileNameLabel": "檔案名稱",
"showButton": "顯示", "showButton": "顯示",
@ -422,5 +426,31 @@
"prevPage": "上一頁", "prevPage": "上一頁",
"nextPage": "下一頁", "nextPage": "下一頁",
"lastPage": "最後一頁" "lastPage": "最後一頁"
},
"schemeManager": {
"button": "文件處理方案",
"title": "方案管理器",
"description": "建立新方案並設定選項",
"schemeList": "方案清單",
"schemeConfig": "方案設定",
"inputPlaceholder": "輸入方案名稱",
"deleteTooltip": "刪除方案",
"emptySchemes": "暫無方案",
"selectSchemePrompt": "請先選擇或建立一個方案",
"processingFramework": "處理框架",
"extractionTool": "提取工具",
"modelSource": "模型源",
"errors": {
"loadFailed": "載入方案失敗",
"nameEmpty": "方案名稱不能為空",
"nameExists": "方案名稱已存在",
"addFailed": "新增方案失敗",
"deleteFailed": "刪除方案失敗"
},
"upload": {
"noSchemeSelected": "未選擇處理方案,請先選擇文件處理方案!",
"currentScheme": "目前方案:",
"noSchemeMessage": "未選擇處理方案,請先新增選擇"
}
} }
} }

View file

@ -1,4 +1,5 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: ['class'], darkMode: ['class'],
content: [ content: [