Merge branch 'main' of github.com:lcjqyml/LightRAG
This commit is contained in:
24
README.md
24
README.md
@@ -77,7 +77,9 @@ This repository hosts the code of LightRAG. The structure of this code is based
|
||||
|
||||
</details>
|
||||
|
||||
## Install
|
||||
## Installation
|
||||
|
||||
### Install LightRAG Core
|
||||
|
||||
* Install from source (Recommend)
|
||||
|
||||
@@ -92,6 +94,26 @@ pip install -e .
|
||||
pip install lightrag-hku
|
||||
```
|
||||
|
||||
### Install LightRAG Server
|
||||
|
||||
The LightRAG Server is designed to provide Web UI and API support. The Web UI facilitates document indexing, knowledge graph exploration, and a simple RAG query interface. LightRAG Server also provide an Ollama compatible interfaces, aiming to emulate LightRAG as an Ollama chat model. This allows AI chat bot, such as Open WebUI, to access LightRAG easily.
|
||||
|
||||
* Install from PyPI
|
||||
|
||||
```bash
|
||||
pip install "lightrag-hku[api]"
|
||||
```
|
||||
|
||||
* Installation from Source
|
||||
|
||||
```bash
|
||||
# create a Python virtual enviroment if neccesary
|
||||
# Install in editable mode with API support
|
||||
pip install -e ".[api]"
|
||||
```
|
||||
|
||||
**For more information about LightRAG Server, please refer to [LightRAG Server](./lightrag/api/README.md).**
|
||||
|
||||
## Quick Start
|
||||
|
||||
* [Video demo](https://www.youtube.com/watch?v=g21royNJ4fw) of running LightRAG locally.
|
||||
|
22
env.example
22
env.example
@@ -30,11 +30,6 @@
|
||||
# LOG_MAX_BYTES=10485760 # Log file max size in bytes, defaults to 10MB
|
||||
# LOG_BACKUP_COUNT=5 # Number of backup files to keep, defaults to 5
|
||||
|
||||
### Max async calls for LLM
|
||||
# MAX_ASYNC=4
|
||||
### Optional Timeout for LLM
|
||||
# TIMEOUT=150 # Time out in seconds, None for infinite timeout
|
||||
|
||||
### Settings for RAG query
|
||||
# HISTORY_TURNS=3
|
||||
# COSINE_THRESHOLD=0.2
|
||||
@@ -44,16 +39,21 @@
|
||||
# MAX_TOKEN_ENTITY_DESC=4000
|
||||
|
||||
### Settings for document indexing
|
||||
# SUMMARY_LANGUAGE=English
|
||||
# CHUNK_SIZE=1200
|
||||
# CHUNK_OVERLAP_SIZE=100
|
||||
# MAX_TOKENS=32768 # Max tokens send to LLM for summarization
|
||||
# MAX_TOKEN_SUMMARY=500 # Max tokens for entity or relations summary
|
||||
# SUMMARY_LANGUAGE=English
|
||||
# MAX_TOKEN_SUMMARY=500 # Max tokens for entity or relations summary
|
||||
# MAX_PARALLEL_INSERT=2 # Number of parallel processing documents in one patch
|
||||
# MAX_ASYNC=4 # Max concurrency requests of LLM
|
||||
# ENABLE_LLM_CACHE_FOR_EXTRACT=true # Enable LLM cache for entity extraction
|
||||
|
||||
# EMBEDDING_BATCH_NUM=32 # num of chunks send to Embedding in one request
|
||||
# EMBEDDING_FUNC_MAX_ASYNC=16 # Max concurrency requests for Embedding
|
||||
# MAX_EMBED_TOKENS=8192
|
||||
# ENABLE_LLM_CACHE_FOR_EXTRACT=true # Enable LLM cache for entity extraction
|
||||
# MAX_PARALLEL_INSERT=2 # Maximum number of parallel processing documents in pipeline
|
||||
|
||||
### LLM Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
|
||||
# MAX_TOKENS=32768 # Max tokens send to LLM (less than context size of the model)
|
||||
# TIMEOUT=150 # Time out in seconds for LLM, None for infinite timeout
|
||||
LLM_BINDING=ollama
|
||||
LLM_MODEL=mistral-nemo:latest
|
||||
LLM_BINDING_API_KEY=your_api_key
|
||||
@@ -73,8 +73,6 @@ LLM_BINDING_HOST=http://localhost:11434
|
||||
### Embedding Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
|
||||
EMBEDDING_MODEL=bge-m3:latest
|
||||
EMBEDDING_DIM=1024
|
||||
EMBEDDING_BATCH_NUM=32
|
||||
EMBEDDING_FUNC_MAX_ASYNC=16
|
||||
# EMBEDDING_BINDING_API_KEY=your_api_key
|
||||
### ollama example
|
||||
EMBEDDING_BINDING=ollama
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
|
||||
|
||||
__version__ = "1.2.7"
|
||||
__version__ = "1.2.8"
|
||||
__author__ = "Zirui Guo"
|
||||
__url__ = "https://github.com/HKUDS/LightRAG"
|
||||
|
BIN
lightrag/api/README.assets/image-20250323122538997.png
Normal file
BIN
lightrag/api/README.assets/image-20250323122538997.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 374 KiB |
BIN
lightrag/api/README.assets/image-20250323122754387.png
Normal file
BIN
lightrag/api/README.assets/image-20250323122754387.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 357 KiB |
BIN
lightrag/api/README.assets/image-20250323123011220.png
Normal file
BIN
lightrag/api/README.assets/image-20250323123011220.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 530 KiB |
BIN
lightrag/api/README.assets/image-20250323194750379.png
Normal file
BIN
lightrag/api/README.assets/image-20250323194750379.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 381 KiB |
@@ -1,14 +1,24 @@
|
||||
## Install LightRAG as an API Server
|
||||
# LightRAG Server and WebUI
|
||||
|
||||
LightRAG provides optional API support through FastAPI servers that add RAG capabilities to existing LLM services. You can install LightRAG API Server in two ways:
|
||||
The LightRAG Server is designed to provide Web UI and API support. The Web UI facilitates document indexing, knowledge graph exploration, and a simple RAG query interface. LightRAG Server also provide an Ollama compatible interfaces, aiming to emulate LightRAG as an Ollama chat model. This allows AI chat bot, such as Open WebUI, to access LightRAG easily.
|
||||
|
||||
### Installation from PyPI
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Getting Start
|
||||
|
||||
### Installation
|
||||
|
||||
* Install from PyPI
|
||||
|
||||
```bash
|
||||
pip install "lightrag-hku[api]"
|
||||
```
|
||||
|
||||
### Installation from Source (Development)
|
||||
* Installation from Source
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
@@ -22,143 +32,94 @@ cd lightrag
|
||||
pip install -e ".[api]"
|
||||
```
|
||||
|
||||
### Starting API Server with Default Settings
|
||||
### Before Starting LightRAG Server
|
||||
|
||||
After installing LightRAG with API support, you can start LightRAG by this command: `lightrag-server`
|
||||
|
||||
LightRAG requires both LLM and Embedding Model to work together to complete document indexing and querying tasks. LightRAG supports binding to various LLM/Embedding backends:
|
||||
LightRAG necessitates the integration of both an LLM (Large Language Model) and an Embedding Model to effectively execute document indexing and querying operations. Prior to the initial deployment of the LightRAG server, it is essential to configure the settings for both the LLM and the Embedding Model. LightRAG supports binding to various LLM/Embedding backends:
|
||||
|
||||
* ollama
|
||||
* lollms
|
||||
* openai & openai compatible
|
||||
* openai or openai compatible
|
||||
* azure_openai
|
||||
|
||||
Before running any of the servers, ensure you have the corresponding backend service running for both llm and embedding.
|
||||
The LightRAG API Server provides default parameters for LLM and Embedding, allowing users to easily start the service through command line. These default configurations are:
|
||||
It is recommended to use environment variables to configure the LightRAG Server. There is an example environment variable file named `env.example` in the root directory of the project. Please copy this file to the startup directory and rename it to `.env`. After that, you can modify the parameters related to the LLM and Embedding models in the `.env` file. It is important to note that the LightRAG Server will load the environment variables from `.env` into the system environment variables each time it starts. Since the LightRAG Server will prioritize the settings in the system environment variables, if you modify the `.env` file after starting the LightRAG Server via the command line, you need to execute `source .env` to make the new settings take effect.
|
||||
|
||||
* Default endpoint of LLM/Embeding backend(LLM_BINDING_HOST or EMBEDDING_BINDING_HOST)
|
||||
Here are some examples of common settings for LLM and Embedding models:
|
||||
|
||||
* OpenAI LLM + Ollama Embedding
|
||||
|
||||
```
|
||||
# for lollms backend
|
||||
LLM_BINDING_HOST=http://localhost:11434
|
||||
EMBEDDING_BINDING_HOST=http://localhost:11434
|
||||
|
||||
# for lollms backend
|
||||
LLM_BINDING_HOST=http://localhost:9600
|
||||
EMBEDDING_BINDING_HOST=http://localhost:9600
|
||||
|
||||
# for openai, openai compatible or azure openai backend
|
||||
LLM_BINDING=openai
|
||||
LLM_MODEL=gpt-4o
|
||||
LLM_BINDING_HOST=https://api.openai.com/v1
|
||||
EMBEDDING_BINDING_HOST=http://localhost:9600
|
||||
```
|
||||
|
||||
* Default model config
|
||||
|
||||
```
|
||||
LLM_MODEL=mistral-nemo:latest
|
||||
LLM_BINDING_API_KEY=your_api_key
|
||||
MAX_TOKENS=32768 # Max tokens send to LLM (less than model context size)
|
||||
|
||||
EMBEDDING_BINDING=ollama
|
||||
EMBEDDING_BINDING_HOST=http://localhost:11434
|
||||
EMBEDDING_MODEL=bge-m3:latest
|
||||
EMBEDDING_DIM=1024
|
||||
MAX_EMBED_TOKENS=8192
|
||||
# EMBEDDING_BINDING_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
* API keys for LLM/Embedding backend
|
||||
|
||||
When connecting to backend require API KEY, corresponding environment variables must be provided:
|
||||
* Ollama LLM + Ollama Embedding
|
||||
|
||||
```
|
||||
LLM_BINDING_API_KEY=your_api_key
|
||||
EMBEDDING_BINDING_API_KEY=your_api_key
|
||||
LLM_BINDING=ollama
|
||||
LLM_MODEL=mistral-nemo:latest
|
||||
LLM_BINDING_HOST=http://localhost:11434
|
||||
# LLM_BINDING_API_KEY=your_api_key
|
||||
MAX_TOKENS=8192 # Max tokens send to LLM (base on your Ollama Server capacity)
|
||||
|
||||
EMBEDDING_BINDING=ollama
|
||||
EMBEDDING_BINDING_HOST=http://localhost:11434
|
||||
EMBEDDING_MODEL=bge-m3:latest
|
||||
EMBEDDING_DIM=1024
|
||||
# EMBEDDING_BINDING_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
* Use command line arguments to choose LLM/Embeding backend
|
||||
### Starting LightRAG Server
|
||||
|
||||
Use `--llm-binding` to select LLM backend type, and use `--embedding-binding` to select the embedding backend type. All the supported backend types are:
|
||||
The LightRAG Server supports two operational modes:
|
||||
* The simple and efficient Uvicorn mode
|
||||
|
||||
```
|
||||
openai: LLM default type
|
||||
ollama: Embedding defult type
|
||||
lollms:
|
||||
azure_openai:
|
||||
openai-ollama: select openai for LLM and ollama for embedding(only valid for --llm-binding)
|
||||
lightrag-server
|
||||
```
|
||||
|
||||
The LightRAG API Server allows you to mix different bindings for llm/embeddings. For example, you have the possibility to use ollama for the embedding and openai for the llm.With the above default parameters, you can start API Server with simple CLI arguments like these:
|
||||
* The multiprocess Gunicorn + Uvicorn mode (production mode, not supported on Windows environments)
|
||||
|
||||
```
|
||||
# start with openai llm and ollama embedding
|
||||
LLM_BINDING_API_KEY=your_api_key Light_server
|
||||
LLM_BINDING_API_KEY=your_api_key Light_server --llm-binding openai-ollama
|
||||
|
||||
# start with openai llm and openai embedding
|
||||
LLM_BINDING_API_KEY=your_api_key Light_server --llm-binding openai --embedding-binding openai
|
||||
|
||||
# start with ollama llm and ollama embedding (no apikey is needed)
|
||||
light-server --llm-binding ollama --embedding-binding ollama
|
||||
```
|
||||
|
||||
### Starting API Server with Gunicorn (Production)
|
||||
|
||||
For production deployments, it's recommended to use Gunicorn as the WSGI server to handle concurrent requests efficiently. LightRAG provides a dedicated Gunicorn startup script that handles shared data initialization, process management, and other critical functionalities.
|
||||
|
||||
```bash
|
||||
# Start with lightrag-gunicorn command
|
||||
lightrag-gunicorn --workers 4
|
||||
|
||||
# Alternatively, you can use the module directly
|
||||
python -m lightrag.api.run_with_gunicorn --workers 4
|
||||
```
|
||||
The `.env` file must be placed in the startup directory. Upon launching, the LightRAG Server will create a documents directory (default is `./inputs`) and a data directory (default is `./rag_storage`). This allows you to initiate multiple instances of LightRAG Server from different directories, with each instance configured to listen on a distinct network port.
|
||||
|
||||
The `--workers` parameter is crucial for performance:
|
||||
|
||||
- Determines how many worker processes Gunicorn will spawn to handle requests
|
||||
- Each worker can handle concurrent requests using asyncio
|
||||
- Recommended value is (2 x number_of_cores) + 1
|
||||
- For example, on a 4-core machine, use 9 workers: (2 x 4) + 1 = 9
|
||||
- Consider your server's memory when setting this value, as each worker consumes memory
|
||||
|
||||
Other important startup parameters:
|
||||
Here are some common used startup parameters:
|
||||
|
||||
- `--host`: Server listening address (default: 0.0.0.0)
|
||||
- `--port`: Server listening port (default: 9621)
|
||||
- `--timeout`: Request handling timeout (default: 150 seconds)
|
||||
- `--timeout`: LLM request timeout (default: 150 seconds)
|
||||
- `--log-level`: Logging level (default: INFO)
|
||||
- `--ssl`: Enable HTTPS
|
||||
- `--ssl-certfile`: Path to SSL certificate file
|
||||
- `--ssl-keyfile`: Path to SSL private key file
|
||||
- --input-dir: specifying the directory to scan for documents (default: ./input)
|
||||
|
||||
The command line parameters and enviroment variable run_with_gunicorn.py is exactly the same as `light-server`.
|
||||
### Auto scan on startup
|
||||
|
||||
### For Azure OpenAI Backend
|
||||
When starting any of the servers with the `--auto-scan-at-startup` parameter, the system will automatically:
|
||||
|
||||
Azure OpenAI API can be created using the following commands in Azure CLI (you need to install Azure CLI first from [https://docs.microsoft.com/en-us/cli/azure/install-azure-cli](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)):
|
||||
```bash
|
||||
# Change the resource group name, location and OpenAI resource name as needed
|
||||
RESOURCE_GROUP_NAME=LightRAG
|
||||
LOCATION=swedencentral
|
||||
RESOURCE_NAME=LightRAG-OpenAI
|
||||
1. Scan for new files in the input directory
|
||||
2. Indexing new documents that aren't already in the database
|
||||
3. Make all content immediately available for RAG queries
|
||||
|
||||
az login
|
||||
az group create --name $RESOURCE_GROUP_NAME --location $LOCATION
|
||||
az cognitiveservices account create --name $RESOURCE_NAME --resource-group $RESOURCE_GROUP_NAME --kind OpenAI --sku S0 --location swedencentral
|
||||
az cognitiveservices account deployment create --resource-group $RESOURCE_GROUP_NAME --model-format OpenAI --name $RESOURCE_NAME --deployment-name gpt-4o --model-name gpt-4o --model-version "2024-08-06" --sku-capacity 100 --sku-name "Standard"
|
||||
az cognitiveservices account deployment create --resource-group $RESOURCE_GROUP_NAME --model-format OpenAI --name $RESOURCE_NAME --deployment-name text-embedding-3-large --model-name text-embedding-3-large --model-version "1" --sku-capacity 80 --sku-name "Standard"
|
||||
az cognitiveservices account show --name $RESOURCE_NAME --resource-group $RESOURCE_GROUP_NAME --query "properties.endpoint"
|
||||
az cognitiveservices account keys list --name $RESOURCE_NAME -g $RESOURCE_GROUP_NAME
|
||||
> The `--input-dir` parameter specify the input directory to scan for. You can trigger input diretory scan from webui.
|
||||
|
||||
### Multiple workers for Gunicorn + Uvicorn
|
||||
|
||||
The LightRAG Server can operate in the `Gunicorn + Uvicorn` preload mode. Gunicorn's Multiple Worker (multiprocess) capability prevents document indexing tasks from blocking RAG queries. Using CPU-exhaustive document extraction tools, such as docling, can lead to the entire system being blocked in pure Uvicorn mode.
|
||||
|
||||
Though LightRAG Server uses one workers to process the document indexing pipeline, with aysnc task supporting of Uvicorn, multiple files can be processed in parallell. The bottleneck of document indexing speed mainly lies with the LLM. If your LLM supports high concurrency, you can accelerate document indexing by increasing the concurrency level of the LLM. Below are several environment variables related to concurrent processing, along with their default values:
|
||||
|
||||
```
|
||||
The output of the last command will give you the endpoint and the key for the OpenAI API. You can use these values to set the environment variables in the `.env` file.
|
||||
|
||||
```
|
||||
# Azure OpenAI Configuration in .env
|
||||
LLM_BINDING=azure_openai
|
||||
LLM_BINDING_HOST=your-azure-endpoint
|
||||
LLM_MODEL=your-model-deployment-name
|
||||
LLM_BINDING_API_KEY=your-azure-api-key
|
||||
AZURE_OPENAI_API_VERSION=2024-08-01-preview # optional, defaults to latest version
|
||||
EMBEDDING_BINDING=azure_openai # if using Azure OpenAI for embeddings
|
||||
EMBEDDING_MODEL=your-embedding-deployment-name
|
||||
|
||||
WORKERS=2 # Num of worker processes, not greater then (2 x number_of_cores) + 1
|
||||
MAX_PARALLEL_INSERT=2 # Num of parallel files to process in one batch
|
||||
MAX_ASYNC=4 # Max concurrency requests of LLM
|
||||
```
|
||||
|
||||
### Install Lightrag as a Linux Service
|
||||
@@ -192,17 +153,106 @@ sudo systemctl status lightrag.service
|
||||
sudo systemctl enable lightrag.service
|
||||
```
|
||||
|
||||
### Automatic Document Indexing
|
||||
|
||||
When starting any of the servers with the `--auto-scan-at-startup` parameter, the system will automatically:
|
||||
|
||||
1. Scan for new files in the input directory
|
||||
2. Indexing new documents that aren't already in the database
|
||||
3. Make all content immediately available for RAG queries
|
||||
|
||||
> The `--input-dir` parameter specify the input directory to scan for.
|
||||
|
||||
## API Server Configuration
|
||||
## Ollama Emulation
|
||||
|
||||
We provide an Ollama-compatible interfaces for LightRAG, aiming to emulate LightRAG as an Ollama chat model. This allows AI chat frontends supporting Ollama, such as Open WebUI, to access LightRAG easily.
|
||||
|
||||
### Connect Open WebUI to LightRAG
|
||||
|
||||
After starting the lightrag-server, you can add an Ollama-type connection in the Open WebUI admin pannel. And then a model named lightrag:latest will appear in Open WebUI's model management interface. Users can then send queries to LightRAG through the chat interface. You'd better install LightRAG as service for this use case.
|
||||
|
||||
Open WebUI's use LLM to do the session title and session keyword generation task. So the Ollama chat chat completion API detects and forwards OpenWebUI session-related requests directly to underlying LLM. Screen shot from Open WebUI:
|
||||
|
||||

|
||||
|
||||
### Choose Query mode in chat
|
||||
|
||||
A query prefix in the query string can determines which LightRAG query mode is used to generate the respond for the query. The supported prefixes include:
|
||||
|
||||
```
|
||||
/local
|
||||
/global
|
||||
/hybrid
|
||||
/naive
|
||||
/mix
|
||||
/bypass
|
||||
```
|
||||
|
||||
For example, chat message "/mix 唐僧有几个徒弟" will trigger a mix mode query for LighRAG. A chat message without query prefix will trigger a hybrid mode query by default。
|
||||
|
||||
"/bypass" is not a LightRAG query mode, it will tell API Server to pass the query directly to the underlying LLM with chat history. So user can use LLM to answer question base on the chat history. If you are using Open WebUI as front end, you can just switch the model to a normal LLM instead of using /bypass prefix.
|
||||
|
||||
|
||||
|
||||
## API-Key and Authentication
|
||||
|
||||
By default, the LightRAG Server can be accessed without any authentication. We can configure the server with an API-Key or account credentials to secure it.
|
||||
|
||||
* API-KEY
|
||||
|
||||
```
|
||||
LIGHTRAG_API_KEY=your-secure-api-key-here
|
||||
```
|
||||
|
||||
* Account credentials (the web UI requires login before access)
|
||||
|
||||
LightRAG API Server implements JWT-based authentication using HS256 algorithm. To enable secure access control, the following environment variables are required:
|
||||
|
||||
```bash
|
||||
# For jwt auth
|
||||
AUTH_USERNAME=admin # login name
|
||||
AUTH_PASSWORD=admin123 # password
|
||||
TOKEN_SECRET=your-key # JWT key
|
||||
TOKEN_EXPIRE_HOURS=4 # expire duration
|
||||
```
|
||||
|
||||
> Currently, only the configuration of an administrator account and password is supported. A comprehensive account system is yet to be developed and implemented.
|
||||
|
||||
If Account credentials are not configured, the web UI will access the system as a Guest. Therefore, even if only API-KEY is configured, all API can still be accessed through the Guest account, which remains insecure. Hence, to safeguard the API, it is necessary to configure both authentication methods simultaneously.
|
||||
|
||||
|
||||
|
||||
## For Azure OpenAI Backend
|
||||
|
||||
Azure OpenAI API can be created using the following commands in Azure CLI (you need to install Azure CLI first from [https://docs.microsoft.com/en-us/cli/azure/install-azure-cli](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)):
|
||||
|
||||
```bash
|
||||
# Change the resource group name, location and OpenAI resource name as needed
|
||||
RESOURCE_GROUP_NAME=LightRAG
|
||||
LOCATION=swedencentral
|
||||
RESOURCE_NAME=LightRAG-OpenAI
|
||||
|
||||
az login
|
||||
az group create --name $RESOURCE_GROUP_NAME --location $LOCATION
|
||||
az cognitiveservices account create --name $RESOURCE_NAME --resource-group $RESOURCE_GROUP_NAME --kind OpenAI --sku S0 --location swedencentral
|
||||
az cognitiveservices account deployment create --resource-group $RESOURCE_GROUP_NAME --model-format OpenAI --name $RESOURCE_NAME --deployment-name gpt-4o --model-name gpt-4o --model-version "2024-08-06" --sku-capacity 100 --sku-name "Standard"
|
||||
az cognitiveservices account deployment create --resource-group $RESOURCE_GROUP_NAME --model-format OpenAI --name $RESOURCE_NAME --deployment-name text-embedding-3-large --model-name text-embedding-3-large --model-version "1" --sku-capacity 80 --sku-name "Standard"
|
||||
az cognitiveservices account show --name $RESOURCE_NAME --resource-group $RESOURCE_GROUP_NAME --query "properties.endpoint"
|
||||
az cognitiveservices account keys list --name $RESOURCE_NAME -g $RESOURCE_GROUP_NAME
|
||||
|
||||
```
|
||||
|
||||
The output of the last command will give you the endpoint and the key for the OpenAI API. You can use these values to set the environment variables in the `.env` file.
|
||||
|
||||
```
|
||||
# Azure OpenAI Configuration in .env
|
||||
LLM_BINDING=azure_openai
|
||||
LLM_BINDING_HOST=your-azure-endpoint
|
||||
LLM_MODEL=your-model-deployment-name
|
||||
LLM_BINDING_API_KEY=your-azure-api-key
|
||||
AZURE_OPENAI_API_VERSION=2024-08-01-preview # optional, defaults to latest version
|
||||
EMBEDDING_BINDING=azure_openai # if using Azure OpenAI for embeddings
|
||||
EMBEDDING_MODEL=your-embedding-deployment-name
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
## LightRAG Server Configuration in Detail
|
||||
|
||||
API Server can be config in three way (highest priority first):
|
||||
|
||||
@@ -392,19 +442,6 @@ Note: If you don't need the API functionality, you can install the base package
|
||||
pip install lightrag-hku
|
||||
```
|
||||
|
||||
## Authentication Endpoints
|
||||
|
||||
### JWT Authentication Mechanism
|
||||
LightRAG API Server implements JWT-based authentication using HS256 algorithm. To enable secure access control, the following environment variables are required:
|
||||
```bash
|
||||
# For jwt auth
|
||||
AUTH_USERNAME=admin # login name
|
||||
AUTH_PASSWORD=admin123 # password
|
||||
TOKEN_SECRET=your-key # JWT key
|
||||
TOKEN_EXPIRE_HOURS=4 # expire duration
|
||||
WHITELIST_PATHS=/api1,/api2 # white list. /login,/health,/docs,/redoc,/openapi.json are whitelisted by default.
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All servers (LoLLMs, Ollama, OpenAI and Azure OpenAI) provide the same REST API endpoints for RAG functionality. When API Server is running, visit:
|
||||
@@ -528,30 +565,3 @@ Check server health and configuration.
|
||||
```bash
|
||||
curl "http://localhost:9621/health"
|
||||
```
|
||||
|
||||
## Ollama Emulation
|
||||
|
||||
We provide an Ollama-compatible interfaces for LightRAG, aiming to emulate LightRAG as an Ollama chat model. This allows AI chat frontends supporting Ollama, such as Open WebUI, to access LightRAG easily.
|
||||
|
||||
### Connect Open WebUI to LightRAG
|
||||
|
||||
After starting the lightrag-server, you can add an Ollama-type connection in the Open WebUI admin pannel. And then a model named lightrag:latest will appear in Open WebUI's model management interface. Users can then send queries to LightRAG through the chat interface. You'd better install LightRAG as service for this use case.
|
||||
|
||||
Open WebUI's use LLM to do the session title and session keyword generation task. So the Ollama chat chat completion API detects and forwards OpenWebUI session-related requests directly to underlying LLM.
|
||||
|
||||
### Choose Query mode in chat
|
||||
|
||||
A query prefix in the query string can determines which LightRAG query mode is used to generate the respond for the query. The supported prefixes include:
|
||||
|
||||
```
|
||||
/local
|
||||
/global
|
||||
/hybrid
|
||||
/naive
|
||||
/mix
|
||||
/bypass
|
||||
```
|
||||
|
||||
For example, chat message "/mix 唐僧有几个徒弟" will trigger a mix mode query for LighRAG. A chat message without query prefix will trigger a hybrid mode query by default。
|
||||
|
||||
"/bypass" is not a LightRAG query mode, it will tell API Server to pass the query directly to the underlying LLM with chat history. So user can use LLM to answer question base on the chat history. If you are using Open WebUI as front end, you can just switch the model to a normal LLM instead of using /bypass prefix.
|
||||
|
@@ -1 +1 @@
|
||||
__api_version__ = "1.0.5"
|
||||
__api_version__ = "1.2.2"
|
||||
|
@@ -3,6 +3,9 @@ from datetime import datetime, timedelta
|
||||
import jwt
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
|
@@ -29,7 +29,9 @@ preload_app = True
|
||||
worker_class = "uvicorn.workers.UvicornWorker"
|
||||
|
||||
# Other Gunicorn configurations
|
||||
timeout = int(os.getenv("TIMEOUT", 150)) # Default 150s to match run_with_gunicorn.py
|
||||
timeout = int(
|
||||
os.getenv("TIMEOUT", 150 * 2)
|
||||
) # Default 150s *2 to match run_with_gunicorn.py
|
||||
keepalive = int(os.getenv("KEEPALIVE", 5)) # Default 5s
|
||||
|
||||
# Logging configuration
|
||||
|
@@ -23,9 +23,9 @@ from lightrag.api.utils_api import (
|
||||
get_default_host,
|
||||
display_splash_screen,
|
||||
)
|
||||
from lightrag import LightRAG
|
||||
from lightrag.types import GPTKeywordExtractionFormat
|
||||
from lightrag import LightRAG, __version__ as core_version
|
||||
from lightrag.api import __api_version__
|
||||
from lightrag.types import GPTKeywordExtractionFormat
|
||||
from lightrag.utils import EmbeddingFunc
|
||||
from lightrag.api.routers.document_routes import (
|
||||
DocumentManager,
|
||||
@@ -49,7 +49,7 @@ from .auth import auth_handler
|
||||
# Load environment variables
|
||||
# Updated to use the .env that is inside the current folder
|
||||
# This update allows the user to put a different.env file for each lightrag folder
|
||||
load_dotenv(".env", override=True)
|
||||
load_dotenv()
|
||||
|
||||
# Initialize config parser
|
||||
config = configparser.ConfigParser()
|
||||
@@ -364,9 +364,16 @@ def create_app(args):
|
||||
"token_type": "bearer",
|
||||
"auth_mode": "disabled",
|
||||
"message": "Authentication is disabled. Using guest access.",
|
||||
"core_version": core_version,
|
||||
"api_version": __api_version__,
|
||||
}
|
||||
|
||||
return {"auth_configured": True, "auth_mode": "enabled"}
|
||||
return {
|
||||
"auth_configured": True,
|
||||
"auth_mode": "enabled",
|
||||
"core_version": core_version,
|
||||
"api_version": __api_version__,
|
||||
}
|
||||
|
||||
@app.post("/login", dependencies=[Depends(optional_api_key)])
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
@@ -383,6 +390,8 @@ def create_app(args):
|
||||
"token_type": "bearer",
|
||||
"auth_mode": "disabled",
|
||||
"message": "Authentication is disabled. Using guest access.",
|
||||
"core_version": core_version,
|
||||
"api_version": __api_version__,
|
||||
}
|
||||
|
||||
if form_data.username != username or form_data.password != password:
|
||||
@@ -398,6 +407,8 @@ def create_app(args):
|
||||
"access_token": user_token,
|
||||
"token_type": "bearer",
|
||||
"auth_mode": "enabled",
|
||||
"core_version": core_version,
|
||||
"api_version": __api_version__,
|
||||
}
|
||||
|
||||
@app.get("/health", dependencies=[Depends(optional_api_key)])
|
||||
@@ -406,6 +417,13 @@ def create_app(args):
|
||||
# Get update flags status for all namespaces
|
||||
update_status = await get_all_update_flags_status()
|
||||
|
||||
username = os.getenv("AUTH_USERNAME")
|
||||
password = os.getenv("AUTH_PASSWORD")
|
||||
if not (username and password):
|
||||
auth_mode = "disabled"
|
||||
else:
|
||||
auth_mode = "enabled"
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"working_directory": str(args.working_dir),
|
||||
@@ -427,6 +445,9 @@ def create_app(args):
|
||||
"enable_llm_cache_for_extract": args.enable_llm_cache_for_extract,
|
||||
},
|
||||
"update_status": update_status,
|
||||
"core_version": core_version,
|
||||
"api_version": __api_version__,
|
||||
"auth_mode": auth_mode,
|
||||
}
|
||||
|
||||
# Custom StaticFiles class to prevent caching of HTML files
|
||||
|
@@ -405,7 +405,7 @@ async def pipeline_index_file(rag: LightRAG, file_path: Path):
|
||||
|
||||
|
||||
async def pipeline_index_files(rag: LightRAG, file_paths: List[Path]):
|
||||
"""Index multiple files concurrently
|
||||
"""Index multiple files sequentially to avoid high CPU load
|
||||
|
||||
Args:
|
||||
rag: LightRAG instance
|
||||
@@ -416,12 +416,12 @@ async def pipeline_index_files(rag: LightRAG, file_paths: List[Path]):
|
||||
try:
|
||||
enqueued = False
|
||||
|
||||
if len(file_paths) == 1:
|
||||
enqueued = await pipeline_enqueue_file(rag, file_paths[0])
|
||||
else:
|
||||
tasks = [pipeline_enqueue_file(rag, path) for path in file_paths]
|
||||
enqueued = any(await asyncio.gather(*tasks))
|
||||
# Process files sequentially
|
||||
for file_path in file_paths:
|
||||
if await pipeline_enqueue_file(rag, file_path):
|
||||
enqueued = True
|
||||
|
||||
# Process the queue only if at least one file was successfully enqueued
|
||||
if enqueued:
|
||||
await rag.apipeline_process_enqueue_documents()
|
||||
except Exception as e:
|
||||
@@ -472,14 +472,34 @@ async def run_scanning_process(rag: LightRAG, doc_manager: DocumentManager):
|
||||
total_files = len(new_files)
|
||||
logger.info(f"Found {total_files} new files to index.")
|
||||
|
||||
for idx, file_path in enumerate(new_files):
|
||||
try:
|
||||
await pipeline_index_file(rag, file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Error indexing file {file_path}: {str(e)}")
|
||||
if not new_files:
|
||||
return
|
||||
|
||||
# Get MAX_PARALLEL_INSERT from global_args
|
||||
max_parallel = global_args["max_parallel_insert"]
|
||||
# Calculate batch size as 2 * MAX_PARALLEL_INSERT
|
||||
batch_size = 2 * max_parallel
|
||||
|
||||
# Process files in batches
|
||||
for i in range(0, total_files, batch_size):
|
||||
batch_files = new_files[i : i + batch_size]
|
||||
batch_num = i // batch_size + 1
|
||||
total_batches = (total_files + batch_size - 1) // batch_size
|
||||
|
||||
logger.info(
|
||||
f"Processing batch {batch_num}/{total_batches} with {len(batch_files)} files"
|
||||
)
|
||||
await pipeline_index_files(rag, batch_files)
|
||||
|
||||
# Log progress
|
||||
processed = min(i + batch_size, total_files)
|
||||
logger.info(
|
||||
f"Processed {processed}/{total_files} files ({processed/total_files*100:.1f}%)"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during scanning process: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
def create_document_routes(
|
||||
|
@@ -13,7 +13,7 @@ from dotenv import load_dotenv
|
||||
|
||||
# Updated to use the .env that is inside the current folder
|
||||
# This update allows the user to put a different.env file for each lightrag folder
|
||||
load_dotenv(".env")
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def check_and_install_dependencies():
|
||||
@@ -140,7 +140,7 @@ def main():
|
||||
|
||||
# Timeout configuration prioritizes command line arguments
|
||||
gunicorn_config.timeout = (
|
||||
args.timeout if args.timeout else int(os.getenv("TIMEOUT", 150))
|
||||
args.timeout if args.timeout * 2 else int(os.getenv("TIMEOUT", 150 * 2))
|
||||
)
|
||||
|
||||
# Keepalive configuration
|
||||
|
@@ -16,7 +16,7 @@ from starlette.status import HTTP_403_FORBIDDEN
|
||||
from .auth import auth_handler
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv(override=True)
|
||||
load_dotenv()
|
||||
|
||||
global_args = {"main_args": None}
|
||||
|
||||
@@ -365,6 +365,9 @@ def parse_args(is_uvicorn_mode: bool = False) -> argparse.Namespace:
|
||||
"LIGHTRAG_VECTOR_STORAGE", DefaultRAGStorageConfig.VECTOR_STORAGE
|
||||
)
|
||||
|
||||
# Get MAX_PARALLEL_INSERT from environment
|
||||
global_args["max_parallel_insert"] = get_env_value("MAX_PARALLEL_INSERT", 2, int)
|
||||
|
||||
# Handle openai-ollama special case
|
||||
if args.llm_binding == "openai-ollama":
|
||||
args.llm_binding = "openai"
|
||||
@@ -441,8 +444,8 @@ def display_splash_screen(args: argparse.Namespace) -> None:
|
||||
ASCIIColors.yellow(f"{args.log_level}")
|
||||
ASCIIColors.white(" ├─ Verbose Debug: ", end="")
|
||||
ASCIIColors.yellow(f"{args.verbose}")
|
||||
ASCIIColors.white(" ├─ Timeout: ", end="")
|
||||
ASCIIColors.yellow(f"{args.timeout if args.timeout else 'None (infinite)'}")
|
||||
ASCIIColors.white(" ├─ History Turns: ", end="")
|
||||
ASCIIColors.yellow(f"{args.history_turns}")
|
||||
ASCIIColors.white(" └─ API Key: ", end="")
|
||||
ASCIIColors.yellow("Set" if args.key else "Not Set")
|
||||
|
||||
@@ -459,8 +462,10 @@ def display_splash_screen(args: argparse.Namespace) -> None:
|
||||
ASCIIColors.yellow(f"{args.llm_binding}")
|
||||
ASCIIColors.white(" ├─ Host: ", end="")
|
||||
ASCIIColors.yellow(f"{args.llm_binding_host}")
|
||||
ASCIIColors.white(" └─ Model: ", end="")
|
||||
ASCIIColors.white(" ├─ Model: ", end="")
|
||||
ASCIIColors.yellow(f"{args.llm_model}")
|
||||
ASCIIColors.white(" └─ Timeout: ", end="")
|
||||
ASCIIColors.yellow(f"{args.timeout if args.timeout else 'None (infinite)'}")
|
||||
|
||||
# Embedding Configuration
|
||||
ASCIIColors.magenta("\n📊 Embedding Configuration:")
|
||||
@@ -475,8 +480,10 @@ def display_splash_screen(args: argparse.Namespace) -> None:
|
||||
|
||||
# RAG Configuration
|
||||
ASCIIColors.magenta("\n⚙️ RAG Configuration:")
|
||||
ASCIIColors.white(" ├─ Max Async Operations: ", end="")
|
||||
ASCIIColors.white(" ├─ Max Async for LLM: ", end="")
|
||||
ASCIIColors.yellow(f"{args.max_async}")
|
||||
ASCIIColors.white(" ├─ Max Parallel Insert: ", end="")
|
||||
ASCIIColors.yellow(f"{global_args['max_parallel_insert']}")
|
||||
ASCIIColors.white(" ├─ Max Tokens: ", end="")
|
||||
ASCIIColors.yellow(f"{args.max_tokens}")
|
||||
ASCIIColors.white(" ├─ Max Embed Tokens: ", end="")
|
||||
@@ -485,8 +492,6 @@ def display_splash_screen(args: argparse.Namespace) -> None:
|
||||
ASCIIColors.yellow(f"{args.chunk_size}")
|
||||
ASCIIColors.white(" ├─ Chunk Overlap Size: ", end="")
|
||||
ASCIIColors.yellow(f"{args.chunk_overlap_size}")
|
||||
ASCIIColors.white(" ├─ History Turns: ", end="")
|
||||
ASCIIColors.yellow(f"{args.history_turns}")
|
||||
ASCIIColors.white(" ├─ Cosine Threshold: ", end="")
|
||||
ASCIIColors.yellow(f"{args.cosine_threshold}")
|
||||
ASCIIColors.white(" ├─ Top-K: ", end="")
|
||||
|
1178
lightrag/api/webui/assets/index-4I5HV9Fr.js
generated
1178
lightrag/api/webui/assets/index-4I5HV9Fr.js
generated
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-BSOt8Nur.css
generated
1
lightrag/api/webui/assets/index-BSOt8Nur.css
generated
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-Cq65VeVX.css
generated
Normal file
1
lightrag/api/webui/assets/index-Cq65VeVX.css
generated
Normal file
File diff suppressed because one or more lines are too long
1208
lightrag/api/webui/assets/index-DlScqWrq.js
generated
Normal file
1208
lightrag/api/webui/assets/index-DlScqWrq.js
generated
Normal file
File diff suppressed because one or more lines are too long
4
lightrag/api/webui/index.html
generated
4
lightrag/api/webui/index.html
generated
@@ -8,8 +8,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lightrag</title>
|
||||
<script type="module" crossorigin src="/webui/assets/index-4I5HV9Fr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-BSOt8Nur.css">
|
||||
<script type="module" crossorigin src="/webui/assets/index-DlScqWrq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-Cq65VeVX.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
@@ -41,6 +41,9 @@ _pipeline_status_lock: Optional[LockType] = None
|
||||
_graph_db_lock: Optional[LockType] = None
|
||||
_data_init_lock: Optional[LockType] = None
|
||||
|
||||
# async locks for coroutine synchronization in multiprocess mode
|
||||
_async_locks: Optional[Dict[str, asyncio.Lock]] = None
|
||||
|
||||
|
||||
class UnifiedLock(Generic[T]):
|
||||
"""Provide a unified lock interface type for asyncio.Lock and multiprocessing.Lock"""
|
||||
@@ -51,12 +54,14 @@ class UnifiedLock(Generic[T]):
|
||||
is_async: bool,
|
||||
name: str = "unnamed",
|
||||
enable_logging: bool = True,
|
||||
async_lock: Optional[asyncio.Lock] = None,
|
||||
):
|
||||
self._lock = lock
|
||||
self._is_async = is_async
|
||||
self._pid = os.getpid() # for debug only
|
||||
self._name = name # for debug only
|
||||
self._enable_logging = enable_logging # for debug only
|
||||
self._async_lock = async_lock # auxiliary lock for coroutine synchronization
|
||||
|
||||
async def __aenter__(self) -> "UnifiedLock[T]":
|
||||
try:
|
||||
@@ -64,16 +69,39 @@ class UnifiedLock(Generic[T]):
|
||||
f"== Lock == Process {self._pid}: Acquiring lock '{self._name}' (async={self._is_async})",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
|
||||
# If in multiprocess mode and async lock exists, acquire it first
|
||||
if not self._is_async and self._async_lock is not None:
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Acquiring async lock for '{self._name}'",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
await self._async_lock.acquire()
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Async lock for '{self._name}' acquired",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
|
||||
# Then acquire the main lock
|
||||
if self._is_async:
|
||||
await self._lock.acquire()
|
||||
else:
|
||||
self._lock.acquire()
|
||||
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Lock '{self._name}' acquired (async={self._is_async})",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
return self
|
||||
except Exception as e:
|
||||
# If main lock acquisition fails, release the async lock if it was acquired
|
||||
if (
|
||||
not self._is_async
|
||||
and self._async_lock is not None
|
||||
and self._async_lock.locked()
|
||||
):
|
||||
self._async_lock.release()
|
||||
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Failed to acquire lock '{self._name}': {e}",
|
||||
level="ERROR",
|
||||
@@ -82,15 +110,29 @@ class UnifiedLock(Generic[T]):
|
||||
raise
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
main_lock_released = False
|
||||
try:
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Releasing lock '{self._name}' (async={self._is_async})",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
|
||||
# Release main lock first
|
||||
if self._is_async:
|
||||
self._lock.release()
|
||||
else:
|
||||
self._lock.release()
|
||||
|
||||
main_lock_released = True
|
||||
|
||||
# Then release async lock if in multiprocess mode
|
||||
if not self._is_async and self._async_lock is not None:
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Releasing async lock for '{self._name}'",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
self._async_lock.release()
|
||||
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Lock '{self._name}' released (async={self._is_async})",
|
||||
enable_output=self._enable_logging,
|
||||
@@ -101,6 +143,31 @@ class UnifiedLock(Generic[T]):
|
||||
level="ERROR",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
|
||||
# If main lock release failed but async lock hasn't been released, try to release it
|
||||
if (
|
||||
not main_lock_released
|
||||
and not self._is_async
|
||||
and self._async_lock is not None
|
||||
):
|
||||
try:
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Attempting to release async lock after main lock failure",
|
||||
level="WARNING",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
self._async_lock.release()
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Successfully released async lock after main lock failure",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
except Exception as inner_e:
|
||||
direct_log(
|
||||
f"== Lock == Process {self._pid}: Failed to release async lock after main lock failure: {inner_e}",
|
||||
level="ERROR",
|
||||
enable_output=self._enable_logging,
|
||||
)
|
||||
|
||||
raise
|
||||
|
||||
def __enter__(self) -> "UnifiedLock[T]":
|
||||
@@ -151,51 +218,61 @@ class UnifiedLock(Generic[T]):
|
||||
|
||||
def get_internal_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||
"""return unified storage lock for data consistency"""
|
||||
async_lock = _async_locks.get("internal_lock") if is_multiprocess else None
|
||||
return UnifiedLock(
|
||||
lock=_internal_lock,
|
||||
is_async=not is_multiprocess,
|
||||
name="internal_lock",
|
||||
enable_logging=enable_logging,
|
||||
async_lock=async_lock,
|
||||
)
|
||||
|
||||
|
||||
def get_storage_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||
"""return unified storage lock for data consistency"""
|
||||
async_lock = _async_locks.get("storage_lock") if is_multiprocess else None
|
||||
return UnifiedLock(
|
||||
lock=_storage_lock,
|
||||
is_async=not is_multiprocess,
|
||||
name="storage_lock",
|
||||
enable_logging=enable_logging,
|
||||
async_lock=async_lock,
|
||||
)
|
||||
|
||||
|
||||
def get_pipeline_status_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||
"""return unified storage lock for data consistency"""
|
||||
async_lock = _async_locks.get("pipeline_status_lock") if is_multiprocess else None
|
||||
return UnifiedLock(
|
||||
lock=_pipeline_status_lock,
|
||||
is_async=not is_multiprocess,
|
||||
name="pipeline_status_lock",
|
||||
enable_logging=enable_logging,
|
||||
async_lock=async_lock,
|
||||
)
|
||||
|
||||
|
||||
def get_graph_db_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||
"""return unified graph database lock for ensuring atomic operations"""
|
||||
async_lock = _async_locks.get("graph_db_lock") if is_multiprocess else None
|
||||
return UnifiedLock(
|
||||
lock=_graph_db_lock,
|
||||
is_async=not is_multiprocess,
|
||||
name="graph_db_lock",
|
||||
enable_logging=enable_logging,
|
||||
async_lock=async_lock,
|
||||
)
|
||||
|
||||
|
||||
def get_data_init_lock(enable_logging: bool = False) -> UnifiedLock:
|
||||
"""return unified data initialization lock for ensuring atomic data initialization"""
|
||||
async_lock = _async_locks.get("data_init_lock") if is_multiprocess else None
|
||||
return UnifiedLock(
|
||||
lock=_data_init_lock,
|
||||
is_async=not is_multiprocess,
|
||||
name="data_init_lock",
|
||||
enable_logging=enable_logging,
|
||||
async_lock=async_lock,
|
||||
)
|
||||
|
||||
|
||||
@@ -229,7 +306,8 @@ def initialize_share_data(workers: int = 1):
|
||||
_shared_dicts, \
|
||||
_init_flags, \
|
||||
_initialized, \
|
||||
_update_flags
|
||||
_update_flags, \
|
||||
_async_locks
|
||||
|
||||
# Check if already initialized
|
||||
if _initialized:
|
||||
@@ -251,6 +329,16 @@ def initialize_share_data(workers: int = 1):
|
||||
_shared_dicts = _manager.dict()
|
||||
_init_flags = _manager.dict()
|
||||
_update_flags = _manager.dict()
|
||||
|
||||
# Initialize async locks for multiprocess mode
|
||||
_async_locks = {
|
||||
"internal_lock": asyncio.Lock(),
|
||||
"storage_lock": asyncio.Lock(),
|
||||
"pipeline_status_lock": asyncio.Lock(),
|
||||
"graph_db_lock": asyncio.Lock(),
|
||||
"data_init_lock": asyncio.Lock(),
|
||||
}
|
||||
|
||||
direct_log(
|
||||
f"Process {os.getpid()} Shared-Data created for Multiple Process (workers={workers})"
|
||||
)
|
||||
@@ -264,6 +352,7 @@ def initialize_share_data(workers: int = 1):
|
||||
_shared_dicts = {}
|
||||
_init_flags = {}
|
||||
_update_flags = {}
|
||||
_async_locks = None # No need for async locks in single process mode
|
||||
direct_log(f"Process {os.getpid()} Shared-Data created for Single Process")
|
||||
|
||||
# Mark as initialized
|
||||
@@ -458,7 +547,8 @@ def finalize_share_data():
|
||||
_shared_dicts, \
|
||||
_init_flags, \
|
||||
_initialized, \
|
||||
_update_flags
|
||||
_update_flags, \
|
||||
_async_locks
|
||||
|
||||
# Check if already initialized
|
||||
if not _initialized:
|
||||
@@ -523,5 +613,6 @@ def finalize_share_data():
|
||||
_graph_db_lock = None
|
||||
_data_init_lock = None
|
||||
_update_flags = None
|
||||
_async_locks = None
|
||||
|
||||
direct_log(f"Process {os.getpid()} storage data finalization complete")
|
||||
|
@@ -186,7 +186,9 @@ class LightRAG:
|
||||
embedding_batch_num: int = field(default=int(os.getenv("EMBEDDING_BATCH_NUM", 32)))
|
||||
"""Batch size for embedding computations."""
|
||||
|
||||
embedding_func_max_async: int = field(default=int(os.getenv("EMBEDDING_FUNC_MAX_ASYNC", 16)))
|
||||
embedding_func_max_async: int = field(
|
||||
default=int(os.getenv("EMBEDDING_FUNC_MAX_ASYNC", 16))
|
||||
)
|
||||
"""Maximum number of concurrent embedding function calls."""
|
||||
|
||||
embedding_cache_config: dict[str, Any] = field(
|
||||
|
311
lightrag/llm/anthropic.py
Normal file
311
lightrag/llm/anthropic.py
Normal file
@@ -0,0 +1,311 @@
|
||||
from ..utils import verbose_debug, VERBOSE_DEBUG
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
import numpy as np
|
||||
from typing import Any, Union, AsyncIterator
|
||||
import pipmaster as pm # Pipmaster for dynamic library install
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing import AsyncIterator
|
||||
else:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
# Install Anthropic SDK if not present
|
||||
if not pm.is_installed("anthropic"):
|
||||
pm.install("anthropic")
|
||||
|
||||
# Add Voyage AI import
|
||||
if not pm.is_installed("voyageai"):
|
||||
pm.install("voyageai")
|
||||
import voyageai
|
||||
|
||||
from anthropic import (
|
||||
AsyncAnthropic,
|
||||
APIConnectionError,
|
||||
RateLimitError,
|
||||
APITimeoutError,
|
||||
)
|
||||
from tenacity import (
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
retry_if_exception_type,
|
||||
)
|
||||
from lightrag.utils import (
|
||||
safe_unicode_decode,
|
||||
logger,
|
||||
)
|
||||
from lightrag.api import __api_version__
|
||||
|
||||
|
||||
# Custom exception for retry mechanism
|
||||
class InvalidResponseError(Exception):
|
||||
"""Custom exception class for triggering retry mechanism"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Core Anthropic completion function with retry
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=10),
|
||||
retry=retry_if_exception_type(
|
||||
(RateLimitError, APIConnectionError, APITimeoutError, InvalidResponseError)
|
||||
),
|
||||
)
|
||||
async def anthropic_complete_if_cache(
|
||||
model: str,
|
||||
prompt: str,
|
||||
system_prompt: str | None = None,
|
||||
history_messages: list[dict[str, Any]] | None = None,
|
||||
base_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[str, AsyncIterator[str]]:
|
||||
if history_messages is None:
|
||||
history_messages = []
|
||||
if not api_key:
|
||||
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||
|
||||
default_headers = {
|
||||
"User-Agent": f"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_8) LightRAG/{__api_version__}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# Set logger level to INFO when VERBOSE_DEBUG is off
|
||||
if not VERBOSE_DEBUG and logger.level == logging.DEBUG:
|
||||
logging.getLogger("anthropic").setLevel(logging.INFO)
|
||||
|
||||
anthropic_async_client = (
|
||||
AsyncAnthropic(default_headers=default_headers, api_key=api_key)
|
||||
if base_url is None
|
||||
else AsyncAnthropic(
|
||||
base_url=base_url, default_headers=default_headers, api_key=api_key
|
||||
)
|
||||
)
|
||||
kwargs.pop("hashing_kv", None)
|
||||
messages: list[dict[str, Any]] = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.extend(history_messages)
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
logger.debug("===== Sending Query to Anthropic LLM =====")
|
||||
logger.debug(f"Model: {model} Base URL: {base_url}")
|
||||
logger.debug(f"Additional kwargs: {kwargs}")
|
||||
verbose_debug(f"Query: {prompt}")
|
||||
verbose_debug(f"System prompt: {system_prompt}")
|
||||
|
||||
try:
|
||||
response = await anthropic_async_client.messages.create(
|
||||
model=model, messages=messages, stream=True, **kwargs
|
||||
)
|
||||
except APIConnectionError as e:
|
||||
logger.error(f"Anthropic API Connection Error: {e}")
|
||||
raise
|
||||
except RateLimitError as e:
|
||||
logger.error(f"Anthropic API Rate Limit Error: {e}")
|
||||
raise
|
||||
except APITimeoutError as e:
|
||||
logger.error(f"Anthropic API Timeout Error: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Anthropic API Call Failed,\nModel: {model},\nParams: {kwargs}, Got: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
async def stream_response():
|
||||
try:
|
||||
async for event in response:
|
||||
content = (
|
||||
event.delta.text
|
||||
if hasattr(event, "delta") and event.delta.text
|
||||
else None
|
||||
)
|
||||
if content is None:
|
||||
continue
|
||||
if r"\u" in content:
|
||||
content = safe_unicode_decode(content.encode("utf-8"))
|
||||
yield content
|
||||
except Exception as e:
|
||||
logger.error(f"Error in stream response: {str(e)}")
|
||||
raise
|
||||
|
||||
return stream_response()
|
||||
|
||||
|
||||
# Generic Anthropic completion function
|
||||
async def anthropic_complete(
|
||||
prompt: str,
|
||||
system_prompt: str | None = None,
|
||||
history_messages: list[dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[str, AsyncIterator[str]]:
|
||||
if history_messages is None:
|
||||
history_messages = []
|
||||
model_name = kwargs["hashing_kv"].global_config["llm_model_name"]
|
||||
return await anthropic_complete_if_cache(
|
||||
model_name,
|
||||
prompt,
|
||||
system_prompt=system_prompt,
|
||||
history_messages=history_messages,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# Claude 3 Opus specific completion
|
||||
async def claude_3_opus_complete(
|
||||
prompt: str,
|
||||
system_prompt: str | None = None,
|
||||
history_messages: list[dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[str, AsyncIterator[str]]:
|
||||
if history_messages is None:
|
||||
history_messages = []
|
||||
return await anthropic_complete_if_cache(
|
||||
"claude-3-opus-20240229",
|
||||
prompt,
|
||||
system_prompt=system_prompt,
|
||||
history_messages=history_messages,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# Claude 3 Sonnet specific completion
|
||||
async def claude_3_sonnet_complete(
|
||||
prompt: str,
|
||||
system_prompt: str | None = None,
|
||||
history_messages: list[dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[str, AsyncIterator[str]]:
|
||||
if history_messages is None:
|
||||
history_messages = []
|
||||
return await anthropic_complete_if_cache(
|
||||
"claude-3-sonnet-20240229",
|
||||
prompt,
|
||||
system_prompt=system_prompt,
|
||||
history_messages=history_messages,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# Claude 3 Haiku specific completion
|
||||
async def claude_3_haiku_complete(
|
||||
prompt: str,
|
||||
system_prompt: str | None = None,
|
||||
history_messages: list[dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[str, AsyncIterator[str]]:
|
||||
if history_messages is None:
|
||||
history_messages = []
|
||||
return await anthropic_complete_if_cache(
|
||||
"claude-3-haiku-20240307",
|
||||
prompt,
|
||||
system_prompt=system_prompt,
|
||||
history_messages=history_messages,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# Embedding function (placeholder, as Anthropic does not provide embeddings)
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=60),
|
||||
retry=retry_if_exception_type(
|
||||
(RateLimitError, APIConnectionError, APITimeoutError)
|
||||
),
|
||||
)
|
||||
async def anthropic_embed(
|
||||
texts: list[str],
|
||||
model: str = "voyage-3", # Default to voyage-3 as a good general-purpose model
|
||||
base_url: str = None,
|
||||
api_key: str = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Generate embeddings using Voyage AI since Anthropic doesn't provide native embedding support.
|
||||
|
||||
Args:
|
||||
texts: List of text strings to embed
|
||||
model: Voyage AI model name (e.g., "voyage-3", "voyage-3-large", "voyage-code-3")
|
||||
base_url: Optional custom base URL (not used for Voyage AI)
|
||||
api_key: API key for Voyage AI (defaults to VOYAGE_API_KEY environment variable)
|
||||
|
||||
Returns:
|
||||
numpy array of shape (len(texts), embedding_dimension) containing the embeddings
|
||||
"""
|
||||
if not api_key:
|
||||
api_key = os.environ.get("VOYAGE_API_KEY")
|
||||
if not api_key:
|
||||
logger.error("VOYAGE_API_KEY environment variable not set")
|
||||
raise ValueError(
|
||||
"VOYAGE_API_KEY environment variable is required for embeddings"
|
||||
)
|
||||
|
||||
try:
|
||||
# Initialize Voyage AI client
|
||||
voyage_client = voyageai.Client(api_key=api_key)
|
||||
|
||||
# Get embeddings
|
||||
result = voyage_client.embed(
|
||||
texts,
|
||||
model=model,
|
||||
input_type="document", # Assuming document context; could be made configurable
|
||||
)
|
||||
|
||||
# Convert list of embeddings to numpy array
|
||||
embeddings = np.array(result.embeddings, dtype=np.float32)
|
||||
|
||||
logger.debug(f"Generated embeddings for {len(texts)} texts using {model}")
|
||||
verbose_debug(f"Embedding shape: {embeddings.shape}")
|
||||
|
||||
return embeddings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Voyage AI embedding failed: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
# Optional: a helper function to get available embedding models
|
||||
def get_available_embedding_models() -> dict[str, dict]:
|
||||
"""
|
||||
Returns a dictionary of available Voyage AI embedding models and their properties.
|
||||
"""
|
||||
return {
|
||||
"voyage-3-large": {
|
||||
"context_length": 32000,
|
||||
"dimension": 1024,
|
||||
"description": "Best general-purpose and multilingual",
|
||||
},
|
||||
"voyage-3": {
|
||||
"context_length": 32000,
|
||||
"dimension": 1024,
|
||||
"description": "General-purpose and multilingual",
|
||||
},
|
||||
"voyage-3-lite": {
|
||||
"context_length": 32000,
|
||||
"dimension": 512,
|
||||
"description": "Optimized for latency and cost",
|
||||
},
|
||||
"voyage-code-3": {
|
||||
"context_length": 32000,
|
||||
"dimension": 1024,
|
||||
"description": "Optimized for code",
|
||||
},
|
||||
"voyage-finance-2": {
|
||||
"context_length": 32000,
|
||||
"dimension": 1024,
|
||||
"description": "Optimized for finance",
|
||||
},
|
||||
"voyage-law-2": {
|
||||
"context_length": 16000,
|
||||
"dimension": 1024,
|
||||
"description": "Optimized for legal",
|
||||
},
|
||||
"voyage-multimodal-3": {
|
||||
"context_length": 32000,
|
||||
"dimension": 1024,
|
||||
"description": "Multimodal text and images",
|
||||
},
|
||||
}
|
@@ -1,13 +1,13 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import ThemeProvider from '@/components/ThemeProvider'
|
||||
import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
|
||||
import MessageAlert from '@/components/MessageAlert'
|
||||
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
||||
import StatusIndicator from '@/components/graph/StatusIndicator'
|
||||
import { healthCheckInterval } from '@/lib/constants'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
import { useBackendState, useAuthStore } from '@/stores/state'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useEffect } from 'react'
|
||||
import { getAuthStatus } from '@/api/lightrag'
|
||||
import SiteHeader from '@/features/SiteHeader'
|
||||
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
|
||||
|
||||
@@ -23,17 +23,64 @@ function App() {
|
||||
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
||||
const currentTab = useSettingsStore.use.currentTab()
|
||||
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
||||
const versionCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode
|
||||
|
||||
// Health check
|
||||
// Health check - can be disabled
|
||||
useEffect(() => {
|
||||
// Check immediately
|
||||
useBackendState.getState().check()
|
||||
// Only execute if health check is enabled
|
||||
if (!enableHealthCheck) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
await useBackendState.getState().check()
|
||||
}, healthCheckInterval * 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [enableHealthCheck])
|
||||
// Health check function
|
||||
const performHealthCheck = async () => {
|
||||
await useBackendState.getState().check();
|
||||
};
|
||||
|
||||
// Execute immediately
|
||||
performHealthCheck();
|
||||
|
||||
// Set interval for periodic execution
|
||||
const interval = setInterval(performHealthCheck, healthCheckInterval * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [enableHealthCheck]);
|
||||
|
||||
// Version check - independent and executed only once
|
||||
useEffect(() => {
|
||||
const checkVersion = async () => {
|
||||
// Prevent duplicate calls in Vite dev mode
|
||||
if (versionCheckRef.current) return;
|
||||
versionCheckRef.current = true;
|
||||
|
||||
// Check if version info was already obtained in login page
|
||||
const versionCheckedFromLogin = sessionStorage.getItem('VERSION_CHECKED_FROM_LOGIN') === 'true';
|
||||
if (versionCheckedFromLogin) return;
|
||||
|
||||
// Get version info
|
||||
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const status = await getAuthStatus();
|
||||
if (status.core_version || status.api_version) {
|
||||
const isGuestMode = status.auth_mode === 'disabled' || useAuthStore.getState().isGuestMode;
|
||||
// Update version info while maintaining login state
|
||||
useAuthStore.getState().login(
|
||||
token,
|
||||
isGuestMode,
|
||||
status.core_version,
|
||||
status.api_version
|
||||
);
|
||||
|
||||
// Set flag to indicate version info has been checked
|
||||
sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get version info:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute version check
|
||||
checkVersion();
|
||||
}, []); // Empty dependency array ensures it only runs once on mount
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
|
||||
|
@@ -2,98 +2,11 @@ import { HashRouter as Router, Routes, Route, useNavigate } from 'react-router-d
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAuthStore } from '@/stores/state'
|
||||
import { navigationService } from '@/services/navigation'
|
||||
import { getAuthStatus } from '@/api/lightrag'
|
||||
import { toast } from 'sonner'
|
||||
import { Toaster } from 'sonner'
|
||||
import App from './App'
|
||||
import LoginPage from '@/features/LoginPage'
|
||||
import ThemeProvider from '@/components/ThemeProvider'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||
const { isAuthenticated } = useAuthStore()
|
||||
const [isChecking, setIsChecking] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
|
||||
// Set navigate function for navigation service
|
||||
useEffect(() => {
|
||||
navigationService.setNavigate(navigate)
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true; // Flag to prevent state updates after unmount
|
||||
|
||||
// This effect will run when the component mounts
|
||||
// and will check if authentication is required
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
// Skip check if already authenticated
|
||||
if (isAuthenticated) {
|
||||
if (isMounted) setIsChecking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await getAuthStatus()
|
||||
|
||||
// Only proceed if component is still mounted
|
||||
if (!isMounted) return;
|
||||
|
||||
if (!status.auth_configured && status.access_token) {
|
||||
// If auth is not configured, use the guest token
|
||||
useAuthStore.getState().login(status.access_token, true)
|
||||
if (status.message) {
|
||||
toast.info(status.message)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check auth status:', error)
|
||||
} finally {
|
||||
// Only update state if component is still mounted
|
||||
if (isMounted) {
|
||||
setIsChecking(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute immediately
|
||||
checkAuthStatus()
|
||||
|
||||
// Cleanup function to prevent state updates after unmount
|
||||
return () => {
|
||||
isMounted = false;
|
||||
}
|
||||
}, [isAuthenticated])
|
||||
|
||||
// Handle navigation when authentication status changes
|
||||
useEffect(() => {
|
||||
if (!isChecking && !isAuthenticated) {
|
||||
const currentPath = window.location.hash.slice(1); // Remove the '#' from hash
|
||||
const isLoginPage = currentPath === '/login';
|
||||
|
||||
if (!isLoginPage) {
|
||||
// Use navigation service for redirection
|
||||
console.log('Not authenticated, redirecting to login');
|
||||
navigationService.navigateToLogin();
|
||||
}
|
||||
}
|
||||
}, [isChecking, isAuthenticated]);
|
||||
|
||||
// Show nothing while checking auth status or when not authenticated on login page
|
||||
if (isChecking || (!isAuthenticated && window.location.hash.slice(1) === '/login')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show children only when authenticated
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const AppContent = () => {
|
||||
const [initializing, setInitializing] = useState(true)
|
||||
const { isAuthenticated } = useAuthStore()
|
||||
@@ -104,58 +17,48 @@ const AppContent = () => {
|
||||
navigationService.setNavigate(navigate)
|
||||
}, [navigate])
|
||||
|
||||
// Check token validity and auth configuration on app initialization
|
||||
// Token validity check
|
||||
useEffect(() => {
|
||||
let isMounted = true; // Flag to prevent state updates after unmount
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('LIGHTRAG-API-TOKEN')
|
||||
|
||||
// If we have a token, we're already authenticated
|
||||
if (token && isAuthenticated) {
|
||||
if (isMounted) setInitializing(false);
|
||||
setInitializing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no token or not authenticated, check if auth is configured
|
||||
const status = await getAuthStatus()
|
||||
|
||||
// Only proceed if component is still mounted
|
||||
if (!isMounted) return;
|
||||
|
||||
if (!status.auth_configured && status.access_token) {
|
||||
// If auth is not configured, use the guest token
|
||||
useAuthStore.getState().login(status.access_token, true)
|
||||
if (status.message) {
|
||||
toast.info(status.message)
|
||||
}
|
||||
} else if (!token) {
|
||||
// Only logout if we don't have a token
|
||||
if (!token) {
|
||||
useAuthStore.getState().logout()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth initialization error:', error)
|
||||
if (isMounted && !isAuthenticated) {
|
||||
if (!isAuthenticated) {
|
||||
useAuthStore.getState().logout()
|
||||
}
|
||||
} finally {
|
||||
// Only update state if component is still mounted
|
||||
if (isMounted) {
|
||||
setInitializing(false)
|
||||
}
|
||||
setInitializing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute immediately
|
||||
checkAuth()
|
||||
|
||||
// Cleanup function to prevent state updates after unmount
|
||||
return () => {
|
||||
isMounted = false;
|
||||
}
|
||||
}, [isAuthenticated])
|
||||
|
||||
// Redirect effect for protected routes
|
||||
useEffect(() => {
|
||||
if (!initializing && !isAuthenticated) {
|
||||
const currentPath = window.location.hash.slice(1);
|
||||
if (currentPath !== '/login') {
|
||||
console.log('Not authenticated, redirecting to login');
|
||||
navigate('/login');
|
||||
}
|
||||
}
|
||||
}, [initializing, isAuthenticated, navigate]);
|
||||
|
||||
// Show nothing while initializing
|
||||
if (initializing) {
|
||||
return null
|
||||
@@ -166,11 +69,7 @@ const AppContent = () => {
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<App />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
element={isAuthenticated ? <App /> : null}
|
||||
/>
|
||||
</Routes>
|
||||
)
|
||||
|
@@ -41,6 +41,10 @@ export type LightragStatus = {
|
||||
graph_storage: string
|
||||
vector_storage: string
|
||||
}
|
||||
update_status?: Record<string, any>
|
||||
core_version?: string
|
||||
api_version?: string
|
||||
auth_mode?: 'enabled' | 'disabled'
|
||||
}
|
||||
|
||||
export type LightragDocumentsScanProgress = {
|
||||
@@ -132,6 +136,8 @@ export type AuthStatusResponse = {
|
||||
token_type?: string
|
||||
auth_mode?: 'enabled' | 'disabled'
|
||||
message?: string
|
||||
core_version?: string
|
||||
api_version?: string
|
||||
}
|
||||
|
||||
export type LoginResponse = {
|
||||
@@ -139,6 +145,8 @@ export type LoginResponse = {
|
||||
token_type: string
|
||||
auth_mode?: 'enabled' | 'disabled' // Authentication mode identifier
|
||||
message?: string // Optional message
|
||||
core_version?: string
|
||||
api_version?: string
|
||||
}
|
||||
|
||||
export const InvalidApiKeyError = 'Invalid API Key'
|
||||
@@ -179,8 +187,9 @@ axiosInstance.interceptors.response.use(
|
||||
}
|
||||
// For other APIs, navigate to login page
|
||||
navigationService.navigateToLogin();
|
||||
// Return a never-resolving promise to prevent further execution
|
||||
return new Promise(() => {});
|
||||
|
||||
// return a reject Promise
|
||||
return Promise.reject(new Error('Authentication required'));
|
||||
}
|
||||
throw new Error(
|
||||
`${error.response.status} ${error.response.statusText}\n${JSON.stringify(
|
||||
|
@@ -22,7 +22,7 @@ export default function AppSettings({ className }: AppSettingsProps) {
|
||||
const setTheme = useSettingsStore.use.setTheme()
|
||||
|
||||
const handleLanguageChange = useCallback((value: string) => {
|
||||
setLanguage(value as 'en' | 'zh')
|
||||
setLanguage(value as 'en' | 'zh' | 'fr' | 'ar')
|
||||
}, [setLanguage])
|
||||
|
||||
const handleThemeChange = useCallback((value: string) => {
|
||||
@@ -47,6 +47,8 @@ export default function AppSettings({ className }: AppSettingsProps) {
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="zh">中文</SelectItem>
|
||||
<SelectItem value="fr">Français</SelectItem>
|
||||
<SelectItem value="ar">العربية</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { AsyncSelect } from '@/components/ui/AsyncSelect'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
@@ -12,44 +12,8 @@ const GraphLabels = () => {
|
||||
const { t } = useTranslation()
|
||||
const label = useSettingsStore.use.queryLabel()
|
||||
const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
|
||||
const rawGraph = useGraphStore.use.rawGraph()
|
||||
const labelsLoadedRef = useRef(false)
|
||||
|
||||
// Track if a fetch is in progress to prevent multiple simultaneous fetches
|
||||
const fetchInProgressRef = useRef(false)
|
||||
|
||||
// Fetch labels and trigger initial data load
|
||||
useEffect(() => {
|
||||
// Check if we've already attempted to fetch labels in this session
|
||||
const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
|
||||
|
||||
// Only fetch if we haven't attempted in this session and no fetch is in progress
|
||||
if (!labelsFetchAttempted && !fetchInProgressRef.current) {
|
||||
fetchInProgressRef.current = true
|
||||
// Set global flag to indicate we've attempted to fetch in this session
|
||||
useGraphStore.getState().setLabelsFetchAttempted(true)
|
||||
|
||||
useGraphStore.getState().fetchAllDatabaseLabels()
|
||||
.then(() => {
|
||||
labelsLoadedRef.current = true
|
||||
fetchInProgressRef.current = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch labels:', error)
|
||||
fetchInProgressRef.current = false
|
||||
// Reset global flag to allow retry
|
||||
useGraphStore.getState().setLabelsFetchAttempted(false)
|
||||
})
|
||||
}
|
||||
}, []) // Empty dependency array ensures this only runs once on mount
|
||||
|
||||
// Trigger data load when labels are loaded
|
||||
useEffect(() => {
|
||||
if (labelsLoadedRef.current) {
|
||||
// Reset the fetch attempted flag to force a new data fetch
|
||||
useGraphStore.getState().setGraphDataFetchAttempted(false)
|
||||
}
|
||||
}, [label])
|
||||
// Remove initial label fetch effect as it's now handled by fetchGraph based on lastSuccessfulQueryLabel
|
||||
|
||||
const getSearchEngine = useCallback(() => {
|
||||
// Create search engine
|
||||
@@ -93,40 +57,40 @@ const GraphLabels = () => {
|
||||
)
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
// Reset labels fetch status to allow fetching labels again
|
||||
// Reset fetch status flags
|
||||
useGraphStore.getState().setLabelsFetchAttempted(false)
|
||||
|
||||
// Reset graph data fetch status directly, not depending on allDatabaseLabels changes
|
||||
useGraphStore.getState().setGraphDataFetchAttempted(false)
|
||||
|
||||
// Fetch all labels again
|
||||
useGraphStore.getState().fetchAllDatabaseLabels()
|
||||
.then(() => {
|
||||
// Trigger a graph data reload by changing the query label back and forth
|
||||
const currentLabel = useSettingsStore.getState().queryLabel
|
||||
useSettingsStore.getState().setQueryLabel('')
|
||||
setTimeout(() => {
|
||||
useSettingsStore.getState().setQueryLabel(currentLabel)
|
||||
}, 0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to refresh labels:', error)
|
||||
})
|
||||
}, [])
|
||||
// Clear last successful query label to ensure labels are fetched
|
||||
useGraphStore.getState().setLastSuccessfulQueryLabel('')
|
||||
|
||||
// Get current label
|
||||
const currentLabel = useSettingsStore.getState().queryLabel
|
||||
|
||||
// If current label is empty, use default label '*'
|
||||
if (!currentLabel) {
|
||||
useSettingsStore.getState().setQueryLabel('*')
|
||||
} else {
|
||||
// Trigger data reload
|
||||
useSettingsStore.getState().setQueryLabel('')
|
||||
setTimeout(() => {
|
||||
useSettingsStore.getState().setQueryLabel(currentLabel)
|
||||
}, 0)
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{rawGraph && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant={controlButtonVariant}
|
||||
onClick={handleRefresh}
|
||||
tooltip={t('graphPanel.graphLabels.refreshTooltip')}
|
||||
className="mr-1"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{/* Always show refresh button */}
|
||||
<Button
|
||||
size="icon"
|
||||
variant={controlButtonVariant}
|
||||
onClick={handleRefresh}
|
||||
tooltip={t('graphPanel.graphLabels.refreshTooltip')}
|
||||
className="mr-1"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<AsyncSelect<string>
|
||||
className="ml-2"
|
||||
triggerClassName="max-h-8"
|
||||
@@ -141,20 +105,23 @@ const GraphLabels = () => {
|
||||
placeholder={t('graphPanel.graphLabels.placeholder')}
|
||||
value={label !== null ? label : '*'}
|
||||
onChange={(newLabel) => {
|
||||
const currentLabel = useSettingsStore.getState().queryLabel
|
||||
const currentLabel = useSettingsStore.getState().queryLabel;
|
||||
|
||||
// select the last item means query all
|
||||
if (newLabel === '...') {
|
||||
newLabel = '*'
|
||||
newLabel = '*';
|
||||
}
|
||||
|
||||
// Handle reselecting the same label
|
||||
if (newLabel === currentLabel && newLabel !== '*') {
|
||||
newLabel = '*'
|
||||
newLabel = '*';
|
||||
}
|
||||
|
||||
// Update the label, which will trigger the useEffect to handle data loading
|
||||
useSettingsStore.getState().setQueryLabel(newLabel)
|
||||
// Reset graphDataFetchAttempted flag to ensure data fetch is triggered
|
||||
useGraphStore.getState().setGraphDataFetchAttempted(false);
|
||||
|
||||
// Update the label to trigger data loading
|
||||
useSettingsStore.getState().setQueryLabel(newLabel);
|
||||
}}
|
||||
clearable={false} // Prevent clearing value on reselect
|
||||
/>
|
||||
|
@@ -218,8 +218,8 @@ const LayoutsControl = () => {
|
||||
maxIterations: maxIterations,
|
||||
settings: {
|
||||
attraction: 0.0003, // Lower attraction force to reduce oscillation
|
||||
repulsion: 0.05, // Lower repulsion force to reduce oscillation
|
||||
gravity: 0.01, // Increase gravity to make nodes converge to center faster
|
||||
repulsion: 0.02, // Lower repulsion force to reduce oscillation
|
||||
gravity: 0.02, // Increase gravity to make nodes converge to center faster
|
||||
inertia: 0.4, // Lower inertia to add damping effect
|
||||
maxMove: 100 // Limit maximum movement per step to prevent large jumps
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/stores/state'
|
||||
import { loginToServer, getAuthStatus } from '@/api/lightrag'
|
||||
@@ -18,6 +18,7 @@ const LoginPage = () => {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [checkingAuth, setCheckingAuth] = useState(true)
|
||||
const authCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode
|
||||
|
||||
useEffect(() => {
|
||||
console.log('LoginPage mounted')
|
||||
@@ -25,9 +26,14 @@ const LoginPage = () => {
|
||||
|
||||
// Check if authentication is configured, skip login if not
|
||||
useEffect(() => {
|
||||
let isMounted = true; // Flag to prevent state updates after unmount
|
||||
|
||||
const checkAuthConfig = async () => {
|
||||
// Prevent duplicate calls in Vite dev mode
|
||||
if (authCheckRef.current) {
|
||||
return;
|
||||
}
|
||||
authCheckRef.current = true;
|
||||
|
||||
try {
|
||||
// If already authenticated, redirect to home
|
||||
if (isAuthenticated) {
|
||||
@@ -38,26 +44,30 @@ const LoginPage = () => {
|
||||
// Check auth status
|
||||
const status = await getAuthStatus()
|
||||
|
||||
// Only proceed if component is still mounted
|
||||
if (!isMounted) return;
|
||||
// Set session flag for version check to avoid duplicate checks in App component
|
||||
if (status.core_version || status.api_version) {
|
||||
sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true');
|
||||
}
|
||||
|
||||
if (!status.auth_configured && status.access_token) {
|
||||
// If auth is not configured, use the guest token and redirect
|
||||
login(status.access_token, true)
|
||||
login(status.access_token, true, status.core_version, status.api_version)
|
||||
if (status.message) {
|
||||
toast.info(status.message)
|
||||
}
|
||||
navigate('/')
|
||||
return // Exit early, no need to set checkingAuth to false
|
||||
return
|
||||
}
|
||||
|
||||
// Only set checkingAuth to false if we need to show the login page
|
||||
setCheckingAuth(false);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to check auth configuration:', error)
|
||||
} finally {
|
||||
// Only update state if component is still mounted
|
||||
if (isMounted) {
|
||||
setCheckingAuth(false)
|
||||
}
|
||||
// Also set checkingAuth to false in case of error
|
||||
setCheckingAuth(false);
|
||||
}
|
||||
// Removed finally block as we're setting checkingAuth earlier
|
||||
}
|
||||
|
||||
// Execute immediately
|
||||
@@ -65,7 +75,6 @@ const LoginPage = () => {
|
||||
|
||||
// Cleanup function to prevent state updates after unmount
|
||||
return () => {
|
||||
isMounted = false;
|
||||
}
|
||||
}, [isAuthenticated, login, navigate])
|
||||
|
||||
@@ -87,7 +96,12 @@ const LoginPage = () => {
|
||||
|
||||
// Check authentication mode
|
||||
const isGuestMode = response.auth_mode === 'disabled'
|
||||
login(response.access_token, isGuestMode)
|
||||
login(response.access_token, isGuestMode, response.core_version, response.api_version)
|
||||
|
||||
// Set session flag for version check
|
||||
if (response.core_version || response.api_version) {
|
||||
sessionStorage.setItem('VERSION_CHECKED_FROM_LOGIN', 'true');
|
||||
}
|
||||
|
||||
if (isGuestMode) {
|
||||
// Show authentication disabled notification
|
||||
|
@@ -55,7 +55,11 @@ function TabsNavigation() {
|
||||
|
||||
export default function SiteHeader() {
|
||||
const { t } = useTranslation()
|
||||
const { isGuestMode } = useAuthStore()
|
||||
const { isGuestMode, coreVersion, apiVersion } = useAuthStore()
|
||||
|
||||
const versionDisplay = (coreVersion && apiVersion)
|
||||
? `${coreVersion}/${apiVersion}`
|
||||
: null;
|
||||
|
||||
const handleLogout = () => {
|
||||
navigationService.navigateToLogin();
|
||||
@@ -67,6 +71,11 @@ export default function SiteHeader() {
|
||||
<ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
|
||||
{/* <img src='/logo.png' className="size-4" /> */}
|
||||
<span className="font-bold md:inline-block">{SiteInfo.name}</span>
|
||||
{versionDisplay && (
|
||||
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
v{versionDisplay}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
|
||||
<div className="flex h-10 flex-1 justify-center">
|
||||
@@ -86,9 +95,11 @@ export default function SiteHeader() {
|
||||
</a>
|
||||
</Button>
|
||||
<AppSettings />
|
||||
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.logout')} onClick={handleLogout}>
|
||||
<LogOutIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
{!isGuestMode && (
|
||||
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.logout')} onClick={handleLogout}>
|
||||
<LogOutIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
@@ -12,34 +12,52 @@ import { useSettingsStore } from '@/stores/settings'
|
||||
import seedrandom from 'seedrandom'
|
||||
|
||||
const validateGraph = (graph: RawGraph) => {
|
||||
// Check if graph exists
|
||||
if (!graph) {
|
||||
return false
|
||||
}
|
||||
if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
|
||||
return false
|
||||
console.log('Graph validation failed: graph is null');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if nodes and edges are arrays
|
||||
if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
|
||||
console.log('Graph validation failed: nodes or edges is not an array');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if nodes array is empty
|
||||
if (graph.nodes.length === 0) {
|
||||
console.log('Graph validation failed: nodes array is empty');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate each node
|
||||
for (const node of graph.nodes) {
|
||||
if (!node.id || !node.labels || !node.properties) {
|
||||
return false
|
||||
console.log('Graph validation failed: invalid node structure');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate each edge
|
||||
for (const edge of graph.edges) {
|
||||
if (!edge.id || !edge.source || !edge.target) {
|
||||
return false
|
||||
console.log('Graph validation failed: invalid edge structure');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate edge connections
|
||||
for (const edge of graph.edges) {
|
||||
const source = graph.getNode(edge.source)
|
||||
const target = graph.getNode(edge.target)
|
||||
const source = graph.getNode(edge.source);
|
||||
const target = graph.getNode(edge.target);
|
||||
if (source == undefined || target == undefined) {
|
||||
return false
|
||||
console.log('Graph validation failed: edge references non-existent node');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
console.log('Graph validation passed');
|
||||
return true;
|
||||
}
|
||||
|
||||
export type NodeType = {
|
||||
@@ -53,16 +71,32 @@ export type NodeType = {
|
||||
export type EdgeType = { label: string }
|
||||
|
||||
const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => {
|
||||
let rawData: any = null
|
||||
let rawData: any = null;
|
||||
|
||||
try {
|
||||
rawData = await queryGraphs(label, maxDepth, minDegree)
|
||||
} catch (e) {
|
||||
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
|
||||
return null
|
||||
// Check if we need to fetch all database labels first
|
||||
const lastSuccessfulQueryLabel = useGraphStore.getState().lastSuccessfulQueryLabel;
|
||||
if (!lastSuccessfulQueryLabel) {
|
||||
console.log('Last successful queryLabel is empty');
|
||||
try {
|
||||
await useGraphStore.getState().fetchAllDatabaseLabels();
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch all database labels:', e);
|
||||
// Continue with graph fetch even if labels fetch fails
|
||||
}
|
||||
}
|
||||
|
||||
let rawGraph = null
|
||||
// If label is empty, use default label '*'
|
||||
const queryLabel = label || '*';
|
||||
|
||||
try {
|
||||
console.log(`Fetching graph label: ${queryLabel}, depth: ${maxDepth}, deg: ${minDegree}`);
|
||||
rawData = await queryGraphs(queryLabel, maxDepth, minDegree);
|
||||
} catch (e) {
|
||||
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!');
|
||||
return null;
|
||||
}
|
||||
|
||||
let rawGraph = null;
|
||||
|
||||
if (rawData) {
|
||||
const nodeIdMap: Record<string, number> = {}
|
||||
@@ -129,7 +163,7 @@ const fetchGraph = async (label: string, maxDepth: number, minDegree: number) =>
|
||||
|
||||
if (!validateGraph(rawGraph)) {
|
||||
rawGraph = null
|
||||
console.error('Invalid graph data')
|
||||
console.warn('Invalid graph data')
|
||||
}
|
||||
console.log('Graph data loaded')
|
||||
}
|
||||
@@ -192,6 +226,8 @@ const useLightrangeGraph = () => {
|
||||
// Use ref to track if data has been loaded and initial load
|
||||
const dataLoadedRef = useRef(false)
|
||||
const initialLoadRef = useRef(false)
|
||||
// Use ref to track if empty data has been handled
|
||||
const emptyDataHandledRef = useRef(false)
|
||||
|
||||
const getNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
@@ -224,11 +260,16 @@ const useLightrangeGraph = () => {
|
||||
|
||||
// Data fetching logic
|
||||
useEffect(() => {
|
||||
// Skip if fetch is already in progress or no query label
|
||||
if (fetchInProgressRef.current || !queryLabel) {
|
||||
// Skip if fetch is already in progress
|
||||
if (fetchInProgressRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
// Empty queryLabel should be only handle once(avoid infinite loop)
|
||||
if (!queryLabel && emptyDataHandledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only fetch data when graphDataFetchAttempted is false (avoids re-fetching on vite dev mode)
|
||||
if (!isFetching && !useGraphStore.getState().graphDataFetchAttempted) {
|
||||
// Set flags
|
||||
@@ -246,49 +287,104 @@ const useLightrangeGraph = () => {
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Fetching graph data...')
|
||||
console.log('Preparing graph data...')
|
||||
|
||||
// Use a local copy of the parameters
|
||||
const currentQueryLabel = queryLabel
|
||||
const currentMaxQueryDepth = maxQueryDepth
|
||||
const currentMinDegree = minDegree
|
||||
|
||||
// Fetch graph data
|
||||
fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
|
||||
// Declare a variable to store data promise
|
||||
let dataPromise;
|
||||
|
||||
// 1. If query label is not empty, use fetchGraph
|
||||
if (currentQueryLabel) {
|
||||
dataPromise = fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree);
|
||||
} else {
|
||||
// 2. If query label is empty, set data to null
|
||||
console.log('Query label is empty, show empty graph')
|
||||
dataPromise = Promise.resolve(null);
|
||||
}
|
||||
|
||||
// 3. Process data
|
||||
dataPromise.then((data) => {
|
||||
const state = useGraphStore.getState()
|
||||
|
||||
// Reset state
|
||||
state.reset()
|
||||
|
||||
// Create and set new graph directly
|
||||
const newSigmaGraph = createSigmaGraph(data)
|
||||
data?.buildDynamicMap()
|
||||
// Check if data is empty or invalid
|
||||
if (!data || !data.nodes || data.nodes.length === 0) {
|
||||
// Create a graph with a single "Graph Is Empty" node
|
||||
const emptyGraph = new DirectedGraph();
|
||||
|
||||
// Set new graph data
|
||||
state.setSigmaGraph(newSigmaGraph)
|
||||
state.setRawGraph(data)
|
||||
// Add a single node with "Graph Is Empty" label
|
||||
emptyGraph.addNode('empty-graph-node', {
|
||||
label: t('graphPanel.emptyGraph'),
|
||||
color: '#cccccc', // gray color
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
size: 15,
|
||||
borderColor: Constants.nodeBorderColor,
|
||||
borderSize: 0.2
|
||||
});
|
||||
|
||||
// Set graph to store
|
||||
state.setSigmaGraph(emptyGraph);
|
||||
state.setRawGraph(null);
|
||||
|
||||
// Still mark graph as empty for other logic
|
||||
state.setGraphIsEmpty(true);
|
||||
|
||||
// Only clear current label if it's not already empty
|
||||
if (currentQueryLabel) {
|
||||
useSettingsStore.getState().setQueryLabel('');
|
||||
}
|
||||
|
||||
// Clear last successful query label to ensure labels are fetched next time
|
||||
state.setLastSuccessfulQueryLabel('');
|
||||
|
||||
console.log('Graph data is empty, created graph with empty graph node');
|
||||
} else {
|
||||
// Create and set new graph
|
||||
const newSigmaGraph = createSigmaGraph(data);
|
||||
data.buildDynamicMap();
|
||||
|
||||
// Set new graph data
|
||||
state.setSigmaGraph(newSigmaGraph);
|
||||
state.setRawGraph(data);
|
||||
state.setGraphIsEmpty(false);
|
||||
|
||||
// Update last successful query label
|
||||
state.setLastSuccessfulQueryLabel(currentQueryLabel);
|
||||
|
||||
// Reset camera view
|
||||
state.setMoveToSelectedNode(true);
|
||||
}
|
||||
|
||||
// Update flags
|
||||
dataLoadedRef.current = true
|
||||
initialLoadRef.current = true
|
||||
fetchInProgressRef.current = false
|
||||
|
||||
// Reset camera view
|
||||
state.setMoveToSelectedNode(true)
|
||||
|
||||
state.setIsFetching(false)
|
||||
|
||||
// Mark empty data as handled if data is empty and query label is empty
|
||||
if ((!data || !data.nodes || data.nodes.length === 0) && !currentQueryLabel) {
|
||||
emptyDataHandledRef.current = true;
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Error fetching graph data:', error)
|
||||
|
||||
// Reset state on error
|
||||
const state = useGraphStore.getState()
|
||||
state.setIsFetching(false)
|
||||
dataLoadedRef.current = false
|
||||
dataLoadedRef.current = false;
|
||||
fetchInProgressRef.current = false
|
||||
state.setGraphDataFetchAttempted(false)
|
||||
state.setLastSuccessfulQueryLabel('') // Clear last successful query label on error
|
||||
})
|
||||
}
|
||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
|
||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching, t])
|
||||
|
||||
// Handle node expansion
|
||||
useEffect(() => {
|
||||
@@ -368,7 +464,7 @@ const useLightrangeGraph = () => {
|
||||
const nodesToAdd = new Set<string>();
|
||||
const edgesToAdd = new Set<string>();
|
||||
|
||||
// Get degree range from existing graph for size calculations
|
||||
// Get degree maxDegree from existing graph for size calculations
|
||||
const minDegree = 1;
|
||||
let maxDegree = 0;
|
||||
sigmaGraph.forEachNode(node => {
|
||||
@@ -376,10 +472,6 @@ const useLightrangeGraph = () => {
|
||||
maxDegree = Math.max(maxDegree, degree);
|
||||
});
|
||||
|
||||
// Calculate size formula parameters
|
||||
const range = maxDegree - minDegree || 1; // Avoid division by zero
|
||||
const scale = Constants.maxNodeSize - Constants.minNodeSize;
|
||||
|
||||
// First identify connectable nodes (nodes connected to the expanded node)
|
||||
for (const node of processedNodes) {
|
||||
// Skip if node already exists
|
||||
@@ -400,6 +492,7 @@ const useLightrangeGraph = () => {
|
||||
|
||||
// Calculate node degrees and track discarded edges in one pass
|
||||
const nodeDegrees = new Map<string, number>();
|
||||
const existingNodeDegreeIncrements = new Map<string, number>(); // Track degree increments for existing nodes
|
||||
const nodesWithDiscardedEdges = new Set<string>();
|
||||
|
||||
for (const edge of processedEdges) {
|
||||
@@ -408,12 +501,19 @@ const useLightrangeGraph = () => {
|
||||
|
||||
if (sourceExists && targetExists) {
|
||||
edgesToAdd.add(edge.id);
|
||||
// Add degrees for valid edges
|
||||
// Add degrees for both new and existing nodes
|
||||
if (nodesToAdd.has(edge.source)) {
|
||||
nodeDegrees.set(edge.source, (nodeDegrees.get(edge.source) || 0) + 1);
|
||||
} else if (existingNodeIds.has(edge.source)) {
|
||||
// Track degree increments for existing nodes
|
||||
existingNodeDegreeIncrements.set(edge.source, (existingNodeDegreeIncrements.get(edge.source) || 0) + 1);
|
||||
}
|
||||
|
||||
if (nodesToAdd.has(edge.target)) {
|
||||
nodeDegrees.set(edge.target, (nodeDegrees.get(edge.target) || 0) + 1);
|
||||
} else if (existingNodeIds.has(edge.target)) {
|
||||
// Track degree increments for existing nodes
|
||||
existingNodeDegreeIncrements.set(edge.target, (existingNodeDegreeIncrements.get(edge.target) || 0) + 1);
|
||||
}
|
||||
} else {
|
||||
// Track discarded edges for both new and existing nodes
|
||||
@@ -437,16 +537,21 @@ const useLightrangeGraph = () => {
|
||||
sigmaGraph: DirectedGraph,
|
||||
nodesWithDiscardedEdges: Set<string>,
|
||||
minDegree: number,
|
||||
range: number,
|
||||
scale: number
|
||||
maxDegree: number
|
||||
) => {
|
||||
// Calculate derived values inside the function
|
||||
const range = maxDegree - minDegree || 1; // Avoid division by zero
|
||||
const scale = Constants.maxNodeSize - Constants.minNodeSize;
|
||||
|
||||
for (const nodeId of nodesWithDiscardedEdges) {
|
||||
if (sigmaGraph.hasNode(nodeId)) {
|
||||
let newDegree = sigmaGraph.degree(nodeId);
|
||||
newDegree += 1; // Add +1 for discarded edges
|
||||
// Limit newDegree to maxDegree + 1 to prevent nodes from being too large
|
||||
const limitedDegree = Math.min(newDegree, maxDegree + 1);
|
||||
|
||||
const newSize = Math.round(
|
||||
Constants.minNodeSize + scale * Math.pow((newDegree - minDegree) / range, 0.5)
|
||||
Constants.minNodeSize + scale * Math.pow((limitedDegree - minDegree) / range, 0.5)
|
||||
);
|
||||
|
||||
const currentSize = sigmaGraph.getNodeAttribute(nodeId, 'size');
|
||||
@@ -460,16 +565,27 @@ const useLightrangeGraph = () => {
|
||||
|
||||
// If no new connectable nodes found, show toast and return
|
||||
if (nodesToAdd.size === 0) {
|
||||
updateNodeSizes(sigmaGraph, nodesWithDiscardedEdges, minDegree, range, scale);
|
||||
updateNodeSizes(sigmaGraph, nodesWithDiscardedEdges, minDegree, maxDegree);
|
||||
toast.info(t('graphPanel.propertiesView.node.noNewNodes'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Update maxDegree with new node degrees
|
||||
// Update maxDegree considering all nodes (both new and existing)
|
||||
// 1. Consider degrees of new nodes
|
||||
for (const [, degree] of nodeDegrees.entries()) {
|
||||
maxDegree = Math.max(maxDegree, degree);
|
||||
}
|
||||
|
||||
// 2. Consider degree increments for existing nodes
|
||||
for (const [nodeId, increment] of existingNodeDegreeIncrements.entries()) {
|
||||
const currentDegree = sigmaGraph.degree(nodeId);
|
||||
const projectedDegree = currentDegree + increment;
|
||||
maxDegree = Math.max(maxDegree, projectedDegree);
|
||||
}
|
||||
|
||||
const range = maxDegree - minDegree || 1; // Avoid division by zero
|
||||
const scale = Constants.maxNodeSize - Constants.minNodeSize;
|
||||
|
||||
// SAdd nodes and edges to the graph
|
||||
// Calculate camera ratio and spread factor once before the loop
|
||||
const cameraRatio = useGraphStore.getState().sigmaInstance?.getCamera().ratio || 1;
|
||||
@@ -489,8 +605,10 @@ const useLightrangeGraph = () => {
|
||||
const nodeDegree = nodeDegrees.get(nodeId) || 0;
|
||||
|
||||
// Calculate node size
|
||||
// Limit nodeDegree to maxDegree + 1 to prevent new nodes from being too large
|
||||
const limitedDegree = Math.min(nodeDegree, maxDegree + 1);
|
||||
const nodeSize = Math.round(
|
||||
Constants.minNodeSize + scale * Math.pow((nodeDegree - minDegree) / range, 0.5)
|
||||
Constants.minNodeSize + scale * Math.pow((limitedDegree - minDegree) / range, 0.5)
|
||||
);
|
||||
|
||||
// Calculate angle for polar coordinates
|
||||
@@ -565,7 +683,18 @@ const useLightrangeGraph = () => {
|
||||
useGraphStore.getState().resetSearchEngine();
|
||||
|
||||
// Update sizes for all nodes with discarded edges
|
||||
updateNodeSizes(sigmaGraph, nodesWithDiscardedEdges, minDegree, range, scale);
|
||||
updateNodeSizes(sigmaGraph, nodesWithDiscardedEdges, minDegree, maxDegree);
|
||||
|
||||
if (sigmaGraph.hasNode(nodeId)) {
|
||||
const finalDegree = sigmaGraph.degree(nodeId);
|
||||
const limitedDegree = Math.min(finalDegree, maxDegree + 1);
|
||||
const newSize = Math.round(
|
||||
Constants.minNodeSize + scale * Math.pow((limitedDegree - minDegree) / range, 0.5)
|
||||
);
|
||||
sigmaGraph.setNodeAttribute(nodeId, 'size', newSize);
|
||||
nodeToExpand.size = newSize;
|
||||
nodeToExpand.degree = finalDegree;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error expanding node:', error);
|
||||
|
@@ -1,35 +0,0 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { useSettingsStore } from "./stores/settings";
|
||||
|
||||
import en from "./locales/en.json";
|
||||
import zh from "./locales/zh.json";
|
||||
|
||||
const getStoredLanguage = () => {
|
||||
try {
|
||||
const settingsString = localStorage.getItem('settings-storage');
|
||||
if (settingsString) {
|
||||
const settings = JSON.parse(settingsString);
|
||||
return settings.state?.language || 'en';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get stored language:', e);
|
||||
}
|
||||
return 'en';
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
zh: { translation: zh }
|
||||
},
|
||||
lng: getStoredLanguage(), // 使用存储的语言设置
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
@@ -4,34 +4,44 @@ import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
import en from './locales/en.json'
|
||||
import zh from './locales/zh.json'
|
||||
import fr from './locales/fr.json'
|
||||
import ar from './locales/ar.json'
|
||||
|
||||
// Function to sync i18n with store state
|
||||
export const initializeI18n = async (): Promise<typeof i18n> => {
|
||||
// Get initial language from store
|
||||
const initialLanguage = useSettingsStore.getState().language
|
||||
const getStoredLanguage = () => {
|
||||
try {
|
||||
const settingsString = localStorage.getItem('settings-storage')
|
||||
if (settingsString) {
|
||||
const settings = JSON.parse(settingsString)
|
||||
return settings.state?.language || 'en'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get stored language:', e)
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
|
||||
// Initialize with store language
|
||||
await i18n.use(initReactI18next).init({
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
zh: { translation: zh }
|
||||
zh: { translation: zh },
|
||||
fr: { translation: fr },
|
||||
ar: { translation: ar }
|
||||
},
|
||||
lng: initialLanguage,
|
||||
lng: getStoredLanguage(), // 使用存储的语言设置
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
})
|
||||
|
||||
// Subscribe to language changes
|
||||
useSettingsStore.subscribe((state) => {
|
||||
const currentLanguage = state.language
|
||||
if (i18n.language !== currentLanguage) {
|
||||
i18n.changeLanguage(currentLanguage)
|
||||
}
|
||||
})
|
||||
|
||||
return i18n
|
||||
}
|
||||
// Subscribe to language changes
|
||||
useSettingsStore.subscribe((state) => {
|
||||
const currentLanguage = state.language
|
||||
if (i18n.language !== currentLanguage) {
|
||||
i18n.changeLanguage(currentLanguage)
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
|
263
lightrag_webui/src/locales/ar.json
Normal file
263
lightrag_webui/src/locales/ar.json
Normal file
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"settings": {
|
||||
"language": "اللغة",
|
||||
"theme": "السمة",
|
||||
"light": "فاتح",
|
||||
"dark": "داكن",
|
||||
"system": "النظام"
|
||||
},
|
||||
"header": {
|
||||
"documents": "المستندات",
|
||||
"knowledgeGraph": "شبكة المعرفة",
|
||||
"retrieval": "الاسترجاع",
|
||||
"api": "واجهة برمجة التطبيقات",
|
||||
"projectRepository": "مستودع المشروع",
|
||||
"logout": "تسجيل الخروج",
|
||||
"themeToggle": {
|
||||
"switchToLight": "التحويل إلى السمة الفاتحة",
|
||||
"switchToDark": "التحويل إلى السمة الداكنة"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"description": "الرجاء إدخال حسابك وكلمة المرور لتسجيل الدخول إلى النظام",
|
||||
"username": "اسم المستخدم",
|
||||
"usernamePlaceholder": "الرجاء إدخال اسم المستخدم",
|
||||
"password": "كلمة المرور",
|
||||
"passwordPlaceholder": "الرجاء إدخال كلمة المرور",
|
||||
"loginButton": "تسجيل الدخول",
|
||||
"loggingIn": "جاري تسجيل الدخول...",
|
||||
"successMessage": "تم تسجيل الدخول بنجاح",
|
||||
"errorEmptyFields": "الرجاء إدخال اسم المستخدم وكلمة المرور",
|
||||
"errorInvalidCredentials": "فشل تسجيل الدخول، يرجى التحقق من اسم المستخدم وكلمة المرور",
|
||||
"authDisabled": "تم تعطيل المصادقة. استخدام وضع بدون تسجيل دخول.",
|
||||
"guestMode": "وضع بدون تسجيل دخول"
|
||||
},
|
||||
"documentPanel": {
|
||||
"clearDocuments": {
|
||||
"button": "مسح",
|
||||
"tooltip": "مسح المستندات",
|
||||
"title": "مسح المستندات",
|
||||
"confirm": "هل تريد حقًا مسح جميع المستندات؟",
|
||||
"confirmButton": "نعم",
|
||||
"success": "تم مسح المستندات بنجاح",
|
||||
"failed": "فشل مسح المستندات:\n{{message}}",
|
||||
"error": "فشل مسح المستندات:\n{{error}}"
|
||||
},
|
||||
"uploadDocuments": {
|
||||
"button": "رفع",
|
||||
"tooltip": "رفع المستندات",
|
||||
"title": "رفع المستندات",
|
||||
"description": "اسحب وأفلت مستنداتك هنا أو انقر للتصفح.",
|
||||
"uploading": "جارٍ الرفع {{name}}: {{percent}}%",
|
||||
"success": "نجاح الرفع:\nتم رفع {{name}} بنجاح",
|
||||
"failed": "فشل الرفع:\n{{name}}\n{{message}}",
|
||||
"error": "فشل الرفع:\n{{name}}\n{{error}}",
|
||||
"generalError": "فشل الرفع\n{{error}}",
|
||||
"fileTypes": "الأنواع المدعومة: TXT، MD، DOCX، PDF، PPTX، RTF، ODT، EPUB، HTML، HTM، TEX، JSON، XML، YAML، YML، CSV، LOG، CONF، INI، PROPERTIES، SQL، BAT، SH، C، CPP، PY، JAVA، JS، TS، SWIFT، GO، RB، PHP، CSS، SCSS، LESS"
|
||||
},
|
||||
"documentManager": {
|
||||
"title": "إدارة المستندات",
|
||||
"scanButton": "مسح ضوئي",
|
||||
"scanTooltip": "مسح المستندات ضوئيًا",
|
||||
"uploadedTitle": "المستندات المرفوعة",
|
||||
"uploadedDescription": "قائمة المستندات المرفوعة وحالاتها.",
|
||||
"emptyTitle": "لا توجد مستندات",
|
||||
"emptyDescription": "لا توجد مستندات مرفوعة بعد.",
|
||||
"columns": {
|
||||
"id": "المعرف",
|
||||
"summary": "الملخص",
|
||||
"status": "الحالة",
|
||||
"length": "الطول",
|
||||
"chunks": "الأجزاء",
|
||||
"created": "تم الإنشاء",
|
||||
"updated": "تم التحديث",
|
||||
"metadata": "البيانات الوصفية"
|
||||
},
|
||||
"status": {
|
||||
"completed": "مكتمل",
|
||||
"processing": "قيد المعالجة",
|
||||
"pending": "معلق",
|
||||
"failed": "فشل"
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "فشل تحميل المستندات\n{{error}}",
|
||||
"scanFailed": "فشل المسح الضوئي للمستندات\n{{error}}",
|
||||
"scanProgressFailed": "فشل الحصول على تقدم المسح الضوئي\n{{error}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphPanel": {
|
||||
"sideBar": {
|
||||
"settings": {
|
||||
"settings": "الإعدادات",
|
||||
"healthCheck": "فحص الحالة",
|
||||
"showPropertyPanel": "إظهار لوحة الخصائص",
|
||||
"showSearchBar": "إظهار شريط البحث",
|
||||
"showNodeLabel": "إظهار تسمية العقدة",
|
||||
"nodeDraggable": "العقدة قابلة للسحب",
|
||||
"showEdgeLabel": "إظهار تسمية الحافة",
|
||||
"hideUnselectedEdges": "إخفاء الحواف غير المحددة",
|
||||
"edgeEvents": "أحداث الحافة",
|
||||
"maxQueryDepth": "أقصى عمق للاستعلام",
|
||||
"minDegree": "الدرجة الدنيا",
|
||||
"maxLayoutIterations": "أقصى تكرارات التخطيط",
|
||||
"depth": "العمق",
|
||||
"degree": "الدرجة",
|
||||
"apiKey": "مفتاح واجهة برمجة التطبيقات",
|
||||
"enterYourAPIkey": "أدخل مفتاح واجهة برمجة التطبيقات الخاص بك",
|
||||
"save": "حفظ",
|
||||
"refreshLayout": "تحديث التخطيط"
|
||||
},
|
||||
"zoomControl": {
|
||||
"zoomIn": "تكبير",
|
||||
"zoomOut": "تصغير",
|
||||
"resetZoom": "إعادة تعيين التكبير",
|
||||
"rotateCamera": "تدوير في اتجاه عقارب الساعة",
|
||||
"rotateCameraCounterClockwise": "تدوير عكس اتجاه عقارب الساعة"
|
||||
},
|
||||
"layoutsControl": {
|
||||
"startAnimation": "بدء حركة التخطيط",
|
||||
"stopAnimation": "إيقاف حركة التخطيط",
|
||||
"layoutGraph": "تخطيط الرسم البياني",
|
||||
"layouts": {
|
||||
"Circular": "دائري",
|
||||
"Circlepack": "حزمة دائرية",
|
||||
"Random": "عشوائي",
|
||||
"Noverlaps": "بدون تداخل",
|
||||
"Force Directed": "موجه بالقوة",
|
||||
"Force Atlas": "أطلس القوة"
|
||||
}
|
||||
},
|
||||
"fullScreenControl": {
|
||||
"fullScreen": "شاشة كاملة",
|
||||
"windowed": "نوافذ"
|
||||
}
|
||||
},
|
||||
"statusIndicator": {
|
||||
"connected": "متصل",
|
||||
"disconnected": "غير متصل"
|
||||
},
|
||||
"statusCard": {
|
||||
"unavailable": "معلومات الحالة غير متوفرة",
|
||||
"storageInfo": "معلومات التخزين",
|
||||
"workingDirectory": "دليل العمل",
|
||||
"inputDirectory": "دليل الإدخال",
|
||||
"llmConfig": "تكوين نموذج اللغة الكبير",
|
||||
"llmBinding": "ربط نموذج اللغة الكبير",
|
||||
"llmBindingHost": "مضيف ربط نموذج اللغة الكبير",
|
||||
"llmModel": "نموذج اللغة الكبير",
|
||||
"maxTokens": "أقصى عدد من الرموز",
|
||||
"embeddingConfig": "تكوين التضمين",
|
||||
"embeddingBinding": "ربط التضمين",
|
||||
"embeddingBindingHost": "مضيف ربط التضمين",
|
||||
"embeddingModel": "نموذج التضمين",
|
||||
"storageConfig": "تكوين التخزين",
|
||||
"kvStorage": "تخزين المفتاح-القيمة",
|
||||
"docStatusStorage": "تخزين حالة المستند",
|
||||
"graphStorage": "تخزين الرسم البياني",
|
||||
"vectorStorage": "تخزين المتجهات"
|
||||
},
|
||||
"propertiesView": {
|
||||
"node": {
|
||||
"title": "عقدة",
|
||||
"id": "المعرف",
|
||||
"labels": "التسميات",
|
||||
"degree": "الدرجة",
|
||||
"properties": "الخصائص",
|
||||
"relationships": "العلاقات (داخل الرسم الفرعي)",
|
||||
"expandNode": "توسيع العقدة",
|
||||
"pruneNode": "تقليم العقدة",
|
||||
"deleteAllNodesError": "رفض حذف جميع العقد في الرسم البياني",
|
||||
"nodesRemoved": "تم إزالة {{count}} عقدة، بما في ذلك العقد اليتيمة",
|
||||
"noNewNodes": "لم يتم العثور على عقد قابلة للتوسيع",
|
||||
"propertyNames": {
|
||||
"description": "الوصف",
|
||||
"entity_id": "الاسم",
|
||||
"entity_type": "النوع",
|
||||
"source_id": "معرف المصدر",
|
||||
"Neighbour": "الجار"
|
||||
}
|
||||
},
|
||||
"edge": {
|
||||
"title": "علاقة",
|
||||
"id": "المعرف",
|
||||
"type": "النوع",
|
||||
"source": "المصدر",
|
||||
"target": "الهدف",
|
||||
"properties": "الخصائص"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "ابحث في العقد...",
|
||||
"message": "و {{count}} آخرون"
|
||||
},
|
||||
"graphLabels": {
|
||||
"selectTooltip": "حدد تسمية الاستعلام",
|
||||
"noLabels": "لم يتم العثور على تسميات",
|
||||
"label": "التسمية",
|
||||
"placeholder": "ابحث في التسميات...",
|
||||
"andOthers": "و {{count}} آخرون",
|
||||
"refreshTooltip": "إعادة تحميل بيانات الرسم البياني"
|
||||
},
|
||||
"emptyGraph": "الرسم البياني فارغ"
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
"copyTooltip": "نسخ إلى الحافظة",
|
||||
"copyError": "فشل نسخ النص إلى الحافظة"
|
||||
},
|
||||
"retrieval": {
|
||||
"startPrompt": "ابدأ الاسترجاع بكتابة استفسارك أدناه",
|
||||
"clear": "مسح",
|
||||
"send": "إرسال",
|
||||
"placeholder": "اكتب استفسارك...",
|
||||
"error": "خطأ: فشل الحصول على الرد"
|
||||
},
|
||||
"querySettings": {
|
||||
"parametersTitle": "المعلمات",
|
||||
"parametersDescription": "تكوين معلمات الاستعلام الخاص بك",
|
||||
"queryMode": "وضع الاستعلام",
|
||||
"queryModeTooltip": "حدد استراتيجية الاسترجاع:\n• ساذج: بحث أساسي بدون تقنيات متقدمة\n• محلي: استرجاع معلومات يعتمد على السياق\n• عالمي: يستخدم قاعدة المعرفة العالمية\n• مختلط: يجمع بين الاسترجاع المحلي والعالمي\n• مزيج: يدمج شبكة المعرفة مع الاسترجاع المتجهي",
|
||||
"queryModeOptions": {
|
||||
"naive": "ساذج",
|
||||
"local": "محلي",
|
||||
"global": "عالمي",
|
||||
"hybrid": "مختلط",
|
||||
"mix": "مزيج"
|
||||
},
|
||||
"responseFormat": "تنسيق الرد",
|
||||
"responseFormatTooltip": "يحدد تنسيق الرد. أمثلة:\n• فقرات متعددة\n• فقرة واحدة\n• نقاط نقطية",
|
||||
"responseFormatOptions": {
|
||||
"multipleParagraphs": "فقرات متعددة",
|
||||
"singleParagraph": "فقرة واحدة",
|
||||
"bulletPoints": "نقاط نقطية"
|
||||
},
|
||||
"topK": "أعلى K نتائج",
|
||||
"topKTooltip": "عدد العناصر العلوية للاسترجاع. يمثل الكيانات في وضع 'محلي' والعلاقات في وضع 'عالمي'",
|
||||
"topKPlaceholder": "عدد النتائج",
|
||||
"maxTokensTextUnit": "أقصى عدد من الرموز لوحدة النص",
|
||||
"maxTokensTextUnitTooltip": "الحد الأقصى لعدد الرموز المسموح به لكل جزء نصي مسترجع",
|
||||
"maxTokensGlobalContext": "أقصى عدد من الرموز للسياق العالمي",
|
||||
"maxTokensGlobalContextTooltip": "الحد الأقصى لعدد الرموز المخصص لأوصاف العلاقات في الاسترجاع العالمي",
|
||||
"maxTokensLocalContext": "أقصى عدد من الرموز للسياق المحلي",
|
||||
"maxTokensLocalContextTooltip": "الحد الأقصى لعدد الرموز المخصص لأوصاف الكيانات في الاسترجاع المحلي",
|
||||
"historyTurns": "دورات التاريخ",
|
||||
"historyTurnsTooltip": "عدد الدورات الكاملة للمحادثة (أزواج المستخدم-المساعد) التي يجب مراعاتها في سياق الرد",
|
||||
"historyTurnsPlaceholder": "عدد دورات التاريخ",
|
||||
"hlKeywords": "الكلمات المفتاحية عالية المستوى",
|
||||
"hlKeywordsTooltip": "قائمة الكلمات المفتاحية عالية المستوى لإعطائها الأولوية في الاسترجاع. افصل بينها بفواصل",
|
||||
"hlkeywordsPlaceHolder": "أدخل الكلمات المفتاحية",
|
||||
"llKeywords": "الكلمات المفتاحية منخفضة المستوى",
|
||||
"llKeywordsTooltip": "قائمة الكلمات المفتاحية منخفضة المستوى لتحسين تركيز الاسترجاع. افصل بينها بفواصل",
|
||||
"onlyNeedContext": "تحتاج فقط إلى السياق",
|
||||
"onlyNeedContextTooltip": "إذا كان صحيحًا، يتم إرجاع السياق المسترجع فقط دون إنشاء رد",
|
||||
"onlyNeedPrompt": "تحتاج فقط إلى المطالبة",
|
||||
"onlyNeedPromptTooltip": "إذا كان صحيحًا، يتم إرجاع المطالبة المولدة فقط دون إنتاج رد",
|
||||
"streamResponse": "تدفق الرد",
|
||||
"streamResponseTooltip": "إذا كان صحيحًا، يتيح إخراج التدفق للردود في الوقت الفعلي"
|
||||
}
|
||||
},
|
||||
"apiSite": {
|
||||
"loading": "جارٍ تحميل وثائق واجهة برمجة التطبيقات..."
|
||||
}
|
||||
}
|
@@ -167,7 +167,7 @@
|
||||
"labels": "Labels",
|
||||
"degree": "Degree",
|
||||
"properties": "Properties",
|
||||
"relationships": "Relationships",
|
||||
"relationships": "Relations(within subgraph)",
|
||||
"expandNode": "Expand Node",
|
||||
"pruneNode": "Prune Node",
|
||||
"deleteAllNodesError": "Refuse to delete all nodes in the graph",
|
||||
@@ -201,7 +201,8 @@
|
||||
"placeholder": "Search labels...",
|
||||
"andOthers": "And {count} others",
|
||||
"refreshTooltip": "Reload graph data"
|
||||
}
|
||||
},
|
||||
"emptyGraph": "Graph Is Empty"
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
|
263
lightrag_webui/src/locales/fr.json
Normal file
263
lightrag_webui/src/locales/fr.json
Normal file
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"settings": {
|
||||
"language": "Langue",
|
||||
"theme": "Thème",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"system": "Système"
|
||||
},
|
||||
"header": {
|
||||
"documents": "Documents",
|
||||
"knowledgeGraph": "Graphe de connaissances",
|
||||
"retrieval": "Récupération",
|
||||
"api": "API",
|
||||
"projectRepository": "Référentiel du projet",
|
||||
"logout": "Déconnexion",
|
||||
"themeToggle": {
|
||||
"switchToLight": "Passer au thème clair",
|
||||
"switchToDark": "Passer au thème sombre"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"description": "Veuillez entrer votre compte et mot de passe pour vous connecter au système",
|
||||
"username": "Nom d'utilisateur",
|
||||
"usernamePlaceholder": "Veuillez saisir un nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"passwordPlaceholder": "Veuillez saisir un mot de passe",
|
||||
"loginButton": "Connexion",
|
||||
"loggingIn": "Connexion en cours...",
|
||||
"successMessage": "Connexion réussie",
|
||||
"errorEmptyFields": "Veuillez saisir votre nom d'utilisateur et mot de passe",
|
||||
"errorInvalidCredentials": "Échec de la connexion, veuillez vérifier le nom d'utilisateur et le mot de passe",
|
||||
"authDisabled": "L'authentification est désactivée. Utilisation du mode sans connexion.",
|
||||
"guestMode": "Mode sans connexion"
|
||||
},
|
||||
"documentPanel": {
|
||||
"clearDocuments": {
|
||||
"button": "Effacer",
|
||||
"tooltip": "Effacer les documents",
|
||||
"title": "Effacer les documents",
|
||||
"confirm": "Voulez-vous vraiment effacer tous les documents ?",
|
||||
"confirmButton": "OUI",
|
||||
"success": "Documents effacés avec succès",
|
||||
"failed": "Échec de l'effacement des documents :\n{{message}}",
|
||||
"error": "Échec de l'effacement des documents :\n{{error}}"
|
||||
},
|
||||
"uploadDocuments": {
|
||||
"button": "Télécharger",
|
||||
"tooltip": "Télécharger des documents",
|
||||
"title": "Télécharger des documents",
|
||||
"description": "Glissez-déposez vos documents ici ou cliquez pour parcourir.",
|
||||
"uploading": "Téléchargement de {{name}} : {{percent}}%",
|
||||
"success": "Succès du téléchargement :\n{{name}} téléchargé avec succès",
|
||||
"failed": "Échec du téléchargement :\n{{name}}\n{{message}}",
|
||||
"error": "Échec du téléchargement :\n{{name}}\n{{error}}",
|
||||
"generalError": "Échec du téléchargement\n{{error}}",
|
||||
"fileTypes": "Types pris en charge : TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
||||
},
|
||||
"documentManager": {
|
||||
"title": "Gestion des documents",
|
||||
"scanButton": "Scanner",
|
||||
"scanTooltip": "Scanner les documents",
|
||||
"uploadedTitle": "Documents téléchargés",
|
||||
"uploadedDescription": "Liste des documents téléchargés et leurs statuts.",
|
||||
"emptyTitle": "Aucun document",
|
||||
"emptyDescription": "Il n'y a pas encore de documents téléchargés.",
|
||||
"columns": {
|
||||
"id": "ID",
|
||||
"summary": "Résumé",
|
||||
"status": "Statut",
|
||||
"length": "Longueur",
|
||||
"chunks": "Fragments",
|
||||
"created": "Créé",
|
||||
"updated": "Mis à jour",
|
||||
"metadata": "Métadonnées"
|
||||
},
|
||||
"status": {
|
||||
"completed": "Terminé",
|
||||
"processing": "En traitement",
|
||||
"pending": "En attente",
|
||||
"failed": "Échoué"
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Échec du chargement 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}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphPanel": {
|
||||
"sideBar": {
|
||||
"settings": {
|
||||
"settings": "Paramètres",
|
||||
"healthCheck": "Vérification de l'état",
|
||||
"showPropertyPanel": "Afficher le panneau des propriétés",
|
||||
"showSearchBar": "Afficher la barre de recherche",
|
||||
"showNodeLabel": "Afficher l'étiquette du nœud",
|
||||
"nodeDraggable": "Nœud déplaçable",
|
||||
"showEdgeLabel": "Afficher l'étiquette de l'arête",
|
||||
"hideUnselectedEdges": "Masquer les arêtes non sélectionnées",
|
||||
"edgeEvents": "Événements des arêtes",
|
||||
"maxQueryDepth": "Profondeur maximale de la requête",
|
||||
"minDegree": "Degré minimum",
|
||||
"maxLayoutIterations": "Itérations maximales de mise en page",
|
||||
"depth": "Profondeur",
|
||||
"degree": "Degré",
|
||||
"apiKey": "Clé API",
|
||||
"enterYourAPIkey": "Entrez votre clé API",
|
||||
"save": "Sauvegarder",
|
||||
"refreshLayout": "Actualiser la mise en page"
|
||||
},
|
||||
"zoomControl": {
|
||||
"zoomIn": "Zoom avant",
|
||||
"zoomOut": "Zoom arrière",
|
||||
"resetZoom": "Réinitialiser le zoom",
|
||||
"rotateCamera": "Rotation horaire",
|
||||
"rotateCameraCounterClockwise": "Rotation antihoraire"
|
||||
},
|
||||
"layoutsControl": {
|
||||
"startAnimation": "Démarrer l'animation de mise en page",
|
||||
"stopAnimation": "Arrêter l'animation de mise en page",
|
||||
"layoutGraph": "Mettre en page le graphe",
|
||||
"layouts": {
|
||||
"Circular": "Circulaire",
|
||||
"Circlepack": "Paquet circulaire",
|
||||
"Random": "Aléatoire",
|
||||
"Noverlaps": "Sans chevauchement",
|
||||
"Force Directed": "Dirigé par la force",
|
||||
"Force Atlas": "Atlas de force"
|
||||
}
|
||||
},
|
||||
"fullScreenControl": {
|
||||
"fullScreen": "Plein écran",
|
||||
"windowed": "Fenêtré"
|
||||
}
|
||||
},
|
||||
"statusIndicator": {
|
||||
"connected": "Connecté",
|
||||
"disconnected": "Déconnecté"
|
||||
},
|
||||
"statusCard": {
|
||||
"unavailable": "Informations sur l'état indisponibles",
|
||||
"storageInfo": "Informations de stockage",
|
||||
"workingDirectory": "Répertoire de travail",
|
||||
"inputDirectory": "Répertoire d'entrée",
|
||||
"llmConfig": "Configuration du modèle de langage",
|
||||
"llmBinding": "Liaison du modèle de langage",
|
||||
"llmBindingHost": "Hôte de liaison du modèle de langage",
|
||||
"llmModel": "Modèle de langage",
|
||||
"maxTokens": "Nombre maximum de jetons",
|
||||
"embeddingConfig": "Configuration d'incorporation",
|
||||
"embeddingBinding": "Liaison d'incorporation",
|
||||
"embeddingBindingHost": "Hôte de liaison d'incorporation",
|
||||
"embeddingModel": "Modèle d'incorporation",
|
||||
"storageConfig": "Configuration de stockage",
|
||||
"kvStorage": "Stockage clé-valeur",
|
||||
"docStatusStorage": "Stockage de l'état des documents",
|
||||
"graphStorage": "Stockage du graphe",
|
||||
"vectorStorage": "Stockage vectoriel"
|
||||
},
|
||||
"propertiesView": {
|
||||
"node": {
|
||||
"title": "Nœud",
|
||||
"id": "ID",
|
||||
"labels": "Étiquettes",
|
||||
"degree": "Degré",
|
||||
"properties": "Propriétés",
|
||||
"relationships": "Relations(dans le sous-graphe)",
|
||||
"expandNode": "Développer le nœud",
|
||||
"pruneNode": "Élaguer le nœud",
|
||||
"deleteAllNodesError": "Refus de supprimer tous les nœuds du graphe",
|
||||
"nodesRemoved": "{{count}} nœuds supprimés, y compris les nœuds orphelins",
|
||||
"noNewNodes": "Aucun nœud développable trouvé",
|
||||
"propertyNames": {
|
||||
"description": "Description",
|
||||
"entity_id": "Nom",
|
||||
"entity_type": "Type",
|
||||
"source_id": "ID source",
|
||||
"Neighbour": "Voisin"
|
||||
}
|
||||
},
|
||||
"edge": {
|
||||
"title": "Relation",
|
||||
"id": "ID",
|
||||
"type": "Type",
|
||||
"source": "Source",
|
||||
"target": "Cible",
|
||||
"properties": "Propriétés"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Rechercher des nœuds...",
|
||||
"message": "Et {{count}} autres"
|
||||
},
|
||||
"graphLabels": {
|
||||
"selectTooltip": "Sélectionner l'étiquette de la requête",
|
||||
"noLabels": "Aucune étiquette trouvée",
|
||||
"label": "Étiquette",
|
||||
"placeholder": "Rechercher des étiquettes...",
|
||||
"andOthers": "Et {{count}} autres",
|
||||
"refreshTooltip": "Recharger les données du graphe"
|
||||
},
|
||||
"emptyGraph": "Le graphe est vide"
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
"copyTooltip": "Copier dans le presse-papiers",
|
||||
"copyError": "Échec de la copie du texte dans le presse-papiers"
|
||||
},
|
||||
"retrieval": {
|
||||
"startPrompt": "Démarrez une récupération en tapant votre requête ci-dessous",
|
||||
"clear": "Effacer",
|
||||
"send": "Envoyer",
|
||||
"placeholder": "Tapez votre requête...",
|
||||
"error": "Erreur : Échec de l'obtention de la réponse"
|
||||
},
|
||||
"querySettings": {
|
||||
"parametersTitle": "Paramètres",
|
||||
"parametersDescription": "Configurez vos paramètres de requête",
|
||||
"queryMode": "Mode de requête",
|
||||
"queryModeTooltip": "Sélectionnez la stratégie de récupération :\n• Naïf : Recherche de base sans techniques avancées\n• Local : Récupération d'informations dépendante du contexte\n• Global : Utilise une base de connaissances globale\n• Hybride : Combine récupération locale et globale\n• Mixte : Intègre le graphe de connaissances avec la récupération vectorielle",
|
||||
"queryModeOptions": {
|
||||
"naive": "Naïf",
|
||||
"local": "Local",
|
||||
"global": "Global",
|
||||
"hybrid": "Hybride",
|
||||
"mix": "Mixte"
|
||||
},
|
||||
"responseFormat": "Format de réponse",
|
||||
"responseFormatTooltip": "Définit le format de la réponse. Exemples :\n• Plusieurs paragraphes\n• Paragraphe unique\n• Points à puces",
|
||||
"responseFormatOptions": {
|
||||
"multipleParagraphs": "Plusieurs paragraphes",
|
||||
"singleParagraph": "Paragraphe unique",
|
||||
"bulletPoints": "Points à puces"
|
||||
},
|
||||
"topK": "Top K résultats",
|
||||
"topKTooltip": "Nombre d'éléments supérieurs à récupérer. Représente les entités en mode 'local' et les relations en mode 'global'",
|
||||
"topKPlaceholder": "Nombre de résultats",
|
||||
"maxTokensTextUnit": "Nombre maximum de jetons pour l'unité de texte",
|
||||
"maxTokensTextUnitTooltip": "Nombre maximum de jetons autorisés pour chaque fragment de texte récupéré",
|
||||
"maxTokensGlobalContext": "Nombre maximum de jetons pour le contexte global",
|
||||
"maxTokensGlobalContextTooltip": "Nombre maximum de jetons alloués pour les descriptions des relations dans la récupération globale",
|
||||
"maxTokensLocalContext": "Nombre maximum de jetons pour le contexte local",
|
||||
"maxTokensLocalContextTooltip": "Nombre maximum de jetons alloués pour les descriptions des entités dans la récupération locale",
|
||||
"historyTurns": "Tours d'historique",
|
||||
"historyTurnsTooltip": "Nombre de tours complets de conversation (paires utilisateur-assistant) à prendre en compte dans le contexte de la réponse",
|
||||
"historyTurnsPlaceholder": "Nombre de tours d'historique",
|
||||
"hlKeywords": "Mots-clés de haut niveau",
|
||||
"hlKeywordsTooltip": "Liste de mots-clés de haut niveau à prioriser dans la récupération. Séparez par des virgules",
|
||||
"hlkeywordsPlaceHolder": "Entrez les mots-clés",
|
||||
"llKeywords": "Mots-clés de bas niveau",
|
||||
"llKeywordsTooltip": "Liste de mots-clés de bas niveau pour affiner la focalisation de la récupération. Séparez par des virgules",
|
||||
"onlyNeedContext": "Besoin uniquement du contexte",
|
||||
"onlyNeedContextTooltip": "Si vrai, ne renvoie que le contexte récupéré sans générer de réponse",
|
||||
"onlyNeedPrompt": "Besoin uniquement de l'invite",
|
||||
"onlyNeedPromptTooltip": "Si vrai, ne renvoie que l'invite générée sans produire de réponse",
|
||||
"streamResponse": "Réponse en flux",
|
||||
"streamResponseTooltip": "Si vrai, active la sortie en flux pour des réponses en temps réel"
|
||||
}
|
||||
},
|
||||
"apiSite": {
|
||||
"loading": "Chargement de la documentation de l'API..."
|
||||
}
|
||||
}
|
@@ -164,7 +164,7 @@
|
||||
"labels": "标签",
|
||||
"degree": "度数",
|
||||
"properties": "属性",
|
||||
"relationships": "关系",
|
||||
"relationships": "关系(子图内)",
|
||||
"expandNode": "扩展节点",
|
||||
"pruneNode": "修剪节点",
|
||||
"deleteAllNodesError": "拒绝删除图中的所有节点",
|
||||
@@ -198,7 +198,8 @@
|
||||
"placeholder": "搜索标签...",
|
||||
"andOthers": "还有 {count} 个",
|
||||
"refreshTooltip": "重新加载图形数据"
|
||||
}
|
||||
},
|
||||
"emptyGraph": "图谱数据为空"
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
|
@@ -2,7 +2,7 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import AppRouter from './AppRouter'
|
||||
import './i18n';
|
||||
import './i18n.ts';
|
||||
|
||||
|
||||
|
||||
|
@@ -67,14 +67,10 @@ class NavigationService {
|
||||
return;
|
||||
}
|
||||
|
||||
// First navigate to login page
|
||||
this.navigate('/login');
|
||||
this.resetAllApplicationState();
|
||||
useAuthStore.getState().logout();
|
||||
|
||||
// Then reset state after navigation
|
||||
setTimeout(() => {
|
||||
this.resetAllApplicationState();
|
||||
useAuthStore.getState().logout();
|
||||
}, 0);
|
||||
this.navigate('/login');
|
||||
}
|
||||
|
||||
navigateToHome() {
|
||||
|
@@ -74,6 +74,8 @@ interface GraphState {
|
||||
|
||||
moveToSelectedNode: boolean
|
||||
isFetching: boolean
|
||||
graphIsEmpty: boolean
|
||||
lastSuccessfulQueryLabel: string
|
||||
|
||||
// Global flags to track data fetching attempts
|
||||
graphDataFetchAttempted: boolean
|
||||
@@ -88,6 +90,8 @@ interface GraphState {
|
||||
reset: () => void
|
||||
|
||||
setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
|
||||
setGraphIsEmpty: (isEmpty: boolean) => void
|
||||
setLastSuccessfulQueryLabel: (label: string) => void
|
||||
|
||||
setRawGraph: (rawGraph: RawGraph | null) => void
|
||||
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
||||
@@ -120,6 +124,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
|
||||
moveToSelectedNode: false,
|
||||
isFetching: false,
|
||||
graphIsEmpty: false,
|
||||
lastSuccessfulQueryLabel: '', // Initialize as empty to ensure fetchAllDatabaseLabels runs on first query
|
||||
|
||||
// Initialize global flags
|
||||
graphDataFetchAttempted: false,
|
||||
@@ -132,6 +138,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
|
||||
searchEngine: null,
|
||||
|
||||
setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
|
||||
setLastSuccessfulQueryLabel: (label: string) => set({ lastSuccessfulQueryLabel: label }),
|
||||
|
||||
|
||||
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
||||
@@ -155,7 +164,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
rawGraph: null,
|
||||
sigmaGraph: null, // to avoid other components from acccessing graph objects
|
||||
searchEngine: null,
|
||||
moveToSelectedNode: false
|
||||
moveToSelectedNode: false,
|
||||
graphIsEmpty: false
|
||||
// Do not reset lastSuccessfulQueryLabel here as it's used to track query history
|
||||
});
|
||||
},
|
||||
|
||||
|
@@ -5,7 +5,7 @@ import { defaultQueryLabel } from '@/lib/constants'
|
||||
import { Message, QueryRequest } from '@/api/lightrag'
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system'
|
||||
type Language = 'en' | 'zh'
|
||||
type Language = 'en' | 'zh' | 'fr' | 'ar'
|
||||
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
||||
|
||||
interface SettingsState {
|
||||
|
@@ -19,8 +19,11 @@ interface BackendState {
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
isGuestMode: boolean; // Add guest mode flag
|
||||
login: (token: string, isGuest?: boolean) => void;
|
||||
coreVersion: string | null;
|
||||
apiVersion: string | null;
|
||||
login: (token: string, isGuest?: boolean, coreVersion?: string | null, apiVersion?: string | null) => void;
|
||||
logout: () => void;
|
||||
setVersion: (coreVersion: string | null, apiVersion: string | null) => void;
|
||||
}
|
||||
|
||||
const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
||||
@@ -33,6 +36,14 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
|
||||
check: async () => {
|
||||
const health = await checkHealth()
|
||||
if (health.status === 'healthy') {
|
||||
// Update version information if health check returns it
|
||||
if (health.core_version || health.api_version) {
|
||||
useAuthStore.getState().setVersion(
|
||||
health.core_version || null,
|
||||
health.api_version || null
|
||||
);
|
||||
}
|
||||
|
||||
set({
|
||||
health: true,
|
||||
message: null,
|
||||
@@ -84,15 +95,25 @@ const isGuestToken = (token: string): boolean => {
|
||||
};
|
||||
|
||||
// Initialize auth state from localStorage
|
||||
const initAuthState = (): { isAuthenticated: boolean; isGuestMode: boolean } => {
|
||||
const initAuthState = (): { isAuthenticated: boolean; isGuestMode: boolean; coreVersion: string | null; apiVersion: string | null } => {
|
||||
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
|
||||
const coreVersion = localStorage.getItem('LIGHTRAG-CORE-VERSION');
|
||||
const apiVersion = localStorage.getItem('LIGHTRAG-API-VERSION');
|
||||
|
||||
if (!token) {
|
||||
return { isAuthenticated: false, isGuestMode: false };
|
||||
return {
|
||||
isAuthenticated: false,
|
||||
isGuestMode: false,
|
||||
coreVersion: coreVersion,
|
||||
apiVersion: apiVersion
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isAuthenticated: true,
|
||||
isGuestMode: isGuestToken(token)
|
||||
isGuestMode: isGuestToken(token),
|
||||
coreVersion: coreVersion,
|
||||
apiVersion: apiVersion
|
||||
};
|
||||
};
|
||||
|
||||
@@ -103,20 +124,54 @@ export const useAuthStore = create<AuthState>(set => {
|
||||
return {
|
||||
isAuthenticated: initialState.isAuthenticated,
|
||||
isGuestMode: initialState.isGuestMode,
|
||||
coreVersion: initialState.coreVersion,
|
||||
apiVersion: initialState.apiVersion,
|
||||
|
||||
login: (token, isGuest = false) => {
|
||||
login: (token, isGuest = false, coreVersion = null, apiVersion = null) => {
|
||||
localStorage.setItem('LIGHTRAG-API-TOKEN', token);
|
||||
|
||||
if (coreVersion) {
|
||||
localStorage.setItem('LIGHTRAG-CORE-VERSION', coreVersion);
|
||||
}
|
||||
if (apiVersion) {
|
||||
localStorage.setItem('LIGHTRAG-API-VERSION', apiVersion);
|
||||
}
|
||||
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
isGuestMode: isGuest
|
||||
isGuestMode: isGuest,
|
||||
coreVersion: coreVersion,
|
||||
apiVersion: apiVersion
|
||||
});
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem('LIGHTRAG-API-TOKEN');
|
||||
|
||||
const coreVersion = localStorage.getItem('LIGHTRAG-CORE-VERSION');
|
||||
const apiVersion = localStorage.getItem('LIGHTRAG-API-VERSION');
|
||||
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
isGuestMode: false
|
||||
isGuestMode: false,
|
||||
coreVersion: coreVersion,
|
||||
apiVersion: apiVersion
|
||||
});
|
||||
},
|
||||
|
||||
setVersion: (coreVersion, apiVersion) => {
|
||||
// Update localStorage
|
||||
if (coreVersion) {
|
||||
localStorage.setItem('LIGHTRAG-CORE-VERSION', coreVersion);
|
||||
}
|
||||
if (apiVersion) {
|
||||
localStorage.setItem('LIGHTRAG-API-VERSION', apiVersion);
|
||||
}
|
||||
|
||||
// Update state
|
||||
set({
|
||||
coreVersion: coreVersion,
|
||||
apiVersion: apiVersion
|
||||
});
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user