Merge branch 'main'

This commit is contained in:
Milin
2025-03-25 15:57:14 +08:00
36 changed files with 2780 additions and 914 deletions

View File

@@ -27,4 +27,4 @@ jobs:
pip install pre-commit
- name: Run pre-commit
run: pre-commit run --all-files
run: pre-commit run --all-files --show-diff-on-failure

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

280
README.md
View File

@@ -28,22 +28,11 @@
</tr>
</table>
<div align="center">
This repository hosts the code of LightRAG. The structure of this code is based on <a href="https://github.com/gusye1234/nano-graphrag">nano-graphrag</a>.
<img src="./README.assets/b2aaf634151b4706892693ffb43d9093.png" width="800" alt="LightRAG Diagram">
<img src="https://i-blog.csdnimg.cn/direct/b2aaf634151b4706892693ffb43d9093.png" width="800" alt="LightRAG Diagram">
</div>
</div>
</br>
<details>
<summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
🎉 News
</summary>
## 🎉 News
- [X] [2025.03.18]🎯📢LightRAG now supports citation functionality.
- [X] [2025.02.05]🎯📢Our team has released [VideoRAG](https://github.com/HKUDS/VideoRAG) understanding extremely long-context videos.
@@ -63,8 +52,6 @@ This repository hosts the code of LightRAG. The structure of this code is based
- [X] [2024.10.16]🎯📢LightRAG now supports [Ollama models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
- [X] [2024.10.15]🎯📢LightRAG now supports [Hugging Face models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
</details>
<details>
<summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
Algorithm Flowchart
@@ -630,11 +617,11 @@ rag.insert(["TEXT1", "TEXT2",...])
rag = LightRAG(
working_dir=WORKING_DIR,
addon_params={
"insert_batch_size": 20 # Process 20 documents per batch
"insert_batch_size": 4 # Process 4 documents per batch
}
)
rag.insert(["TEXT1", "TEXT2", "TEXT3", ...]) # Documents will be processed in batches of 20
rag.insert(["TEXT1", "TEXT2", "TEXT3", ...]) # Documents will be processed in batches of 4
```
The `insert_batch_size` parameter in `addon_params` controls how many documents are processed in each batch during insertion. This is useful for:
@@ -1081,33 +1068,33 @@ Valid modes are:
<details>
<summary> Parameters </summary>
| **Parameter** | **Type** | **Explanation** | **Default** |
| -------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **working\_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` |
| **kv\_storage** | `str` | Storage type for documents and text chunks. Supported types:`JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` |
| **vector\_storage** | `str` | Storage type for embedding vectors. Supported types:`NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` |
| **graph\_storage** | `str` | Storage type for graph edges and nodes. Supported types:`NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` |
| **chunk\_token\_size** | `int` | Maximum token size per chunk when splitting documents | `1200` |
| **chunk\_overlap\_token\_size** | `int` | Overlap token size between two chunks when splitting documents | `100` |
| **tiktoken\_model\_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` |
| **entity\_extract\_max\_gleaning** | `int` | Number of loops in the entity extraction process, appending history messages | `1` |
| **entity\_summary\_to\_max\_tokens** | `int` | Maximum token size for each entity summary | `500` |
| **node\_embedding\_algorithm** | `str` | Algorithm for node embedding (currently not used) | `node2vec` |
| **node2vec\_params** | `dict` | Parameters for node embedding | `{"dimensions": 1536,"num_walks": 10,"walk_length": 40,"window_size": 2,"iterations": 3,"random_seed": 3,}` |
| **embedding\_func** | `EmbeddingFunc` | Function to generate embedding vectors from text | `openai_embed` |
| **embedding\_batch\_num** | `int` | Maximum batch size for embedding processes (multiple texts sent per batch) | `32` |
| **embedding\_func\_max\_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
| **llm\_model\_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
| **llm\_model\_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
| **llm\_model\_max\_token\_size** | `int` | Maximum token size for LLM generation (affects entity relation summaries) | `32768`default value changed by env var MAX_TOKENS) |
| **llm\_model\_max\_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `4`default value changed by env var MAX_ASYNC) |
| **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
| **vector\_db\_storage\_cls\_kwargs** | `dict` | Additional parameters for vector database, like setting the threshold for nodes and relations retrieval. | cosine_better_than_threshold: 0.2default value changed by env var COSINE_THRESHOLD) |
| **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
| **enable\_llm\_cache\_for\_entity\_extract** | `bool` | If `TRUE`, stores LLM results in cache for entity extraction; Good for beginners to debug your application | `TRUE` |
| **addon\_params** | `dict` | Additional parameters, e.g.,`{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"], "insert_batch_size": 10}`: sets example limit, output language, and batch size for document processing | `example_number: all examples, language: English, insert_batch_size: 10` |
| **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
| **embedding\_cache\_config** | `dict` | Configuration for question-answer caching. Contains three parameters:`<br>`- `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers.`<br>`- `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM.`<br>`- `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default:`{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
| **Parameter** | **Type** | **Explanation** | **Default** |
|--------------|----------|-----------------|-------------|
| **working_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` |
| **kv_storage** | `str` | Storage type for documents and text chunks. Supported types: `JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` |
| **vector_storage** | `str` | Storage type for embedding vectors. Supported types: `NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` |
| **graph_storage** | `str` | Storage type for graph edges and nodes. Supported types: `NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` |
| **chunk_token_size** | `int` | Maximum token size per chunk when splitting documents | `1200` |
| **chunk_overlap_token_size** | `int` | Overlap token size between two chunks when splitting documents | `100` |
| **tiktoken_model_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` |
| **entity_extract_max_gleaning** | `int` | Number of loops in the entity extraction process, appending history messages | `1` |
| **entity_summary_to_max_tokens** | `int` | Maximum token size for each entity summary | `500` |
| **node_embedding_algorithm** | `str` | Algorithm for node embedding (currently not used) | `node2vec` |
| **node2vec_params** | `dict` | Parameters for node embedding | `{"dimensions": 1536,"num_walks": 10,"walk_length": 40,"window_size": 2,"iterations": 3,"random_seed": 3,}` |
| **embedding_func** | `EmbeddingFunc` | Function to generate embedding vectors from text | `openai_embed` |
| **embedding_batch_num** | `int` | Maximum batch size for embedding processes (multiple texts sent per batch) | `32` |
| **embedding_func_max_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
| **llm_model_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
| **llm_model_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
| **llm_model_max_token_size** | `int` | Maximum token size for LLM generation (affects entity relation summaries) | `32768`default value changed by env var MAX_TOKENS) |
| **llm_model_max_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `4`default value changed by env var MAX_ASYNC) |
| **llm_model_kwargs** | `dict` | Additional parameters for LLM generation | |
| **vector_db_storage_cls_kwargs** | `dict` | Additional parameters for vector database, like setting the threshold for nodes and relations retrieval | cosine_better_than_threshold: 0.2default value changed by env var COSINE_THRESHOLD) |
| **enable_llm_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
| **enable_llm_cache_for_entity_extract** | `bool` | If `TRUE`, stores LLM results in cache for entity extraction; Good for beginners to debug your application | `TRUE` |
| **addon_params** | `dict` | Additional parameters, e.g., `{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"], "insert_batch_size": 10}`: sets example limit, output language, and batch size for document processing | `example_number: all examples, language: English, insert_batch_size: 10` |
| **convert_response_to_json_func** | `callable` | Not used | `convert_response_to_json` |
| **embedding_cache_config** | `dict` | Configuration for question-answer caching. Contains three parameters: `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers. `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM. `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default: `{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
</details>
@@ -1132,166 +1119,9 @@ LightRag can be installed with API support to serve a Fast api interface to perf
## Graph Visualization
<details>
<summary> <b>Graph visualization with html</b> </summary>
The LightRAG Server offers a comprehensive knowledge graph visualization feature. It supports various gravity layouts, node queries, subgraph filtering, and more. **For more information about LightRAG Server, please refer to [LightRAG Server](./lightrag/api/README.md).**
* The following code can be found in `examples/graph_visual_with_html.py`
```python
import networkx as nx
from pyvis.network import Network
# Load the GraphML file
G = nx.read_graphml('./dickens/graph_chunk_entity_relation.graphml')
# Create a Pyvis network
net = Network(notebook=True)
# Convert NetworkX graph to Pyvis network
net.from_nx(G)
# Save and display the network
net.show('knowledge_graph.html')
```
</details>
<details>
<summary> <b>Graph visualization with Neo4</b> </summary>
* The following code can be found in `examples/graph_visual_with_neo4j.py`
```python
import os
import json
from lightrag.utils import xml_to_json
from neo4j import GraphDatabase
# Constants
WORKING_DIR = "./dickens"
BATCH_SIZE_NODES = 500
BATCH_SIZE_EDGES = 100
# Neo4j connection credentials
NEO4J_URI = "bolt://localhost:7687"
NEO4J_USERNAME = "neo4j"
NEO4J_PASSWORD = "your_password"
def convert_xml_to_json(xml_path, output_path):
"""Converts XML file to JSON and saves the output."""
if not os.path.exists(xml_path):
print(f"Error: File not found - {xml_path}")
return None
json_data = xml_to_json(xml_path)
if json_data:
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, ensure_ascii=False, indent=2)
print(f"JSON file created: {output_path}")
return json_data
else:
print("Failed to create JSON data")
return None
def process_in_batches(tx, query, data, batch_size):
"""Process data in batches and execute the given query."""
for i in range(0, len(data), batch_size):
batch = data[i:i + batch_size]
tx.run(query, {"nodes": batch} if "nodes" in query else {"edges": batch})
def main():
# Paths
xml_file = os.path.join(WORKING_DIR, 'graph_chunk_entity_relation.graphml')
json_file = os.path.join(WORKING_DIR, 'graph_data.json')
# Convert XML to JSON
json_data = convert_xml_to_json(xml_file, json_file)
if json_data is None:
return
# Load nodes and edges
nodes = json_data.get('nodes', [])
edges = json_data.get('edges', [])
# Neo4j queries
create_nodes_query = """
UNWIND $nodes AS node
MERGE (e:Entity {id: node.id})
SET e.entity_type = node.entity_type,
e.description = node.description,
e.source_id = node.source_id,
e.displayName = node.id
REMOVE e:Entity
WITH e, node
CALL apoc.create.addLabels(e, [node.entity_type]) YIELD node AS labeledNode
RETURN count(*)
"""
create_edges_query = """
UNWIND $edges AS edge
MATCH (source {id: edge.source})
MATCH (target {id: edge.target})
WITH source, target, edge,
CASE
WHEN edge.keywords CONTAINS 'lead' THEN 'lead'
WHEN edge.keywords CONTAINS 'participate' THEN 'participate'
WHEN edge.keywords CONTAINS 'uses' THEN 'uses'
WHEN edge.keywords CONTAINS 'located' THEN 'located'
WHEN edge.keywords CONTAINS 'occurs' THEN 'occurs'
ELSE REPLACE(SPLIT(edge.keywords, ',')[0], '\"', '')
END AS relType
CALL apoc.create.relationship(source, relType, {
weight: edge.weight,
description: edge.description,
keywords: edge.keywords,
source_id: edge.source_id
}, target) YIELD rel
RETURN count(*)
"""
set_displayname_and_labels_query = """
MATCH (n)
SET n.displayName = n.id
WITH n
CALL apoc.create.setLabels(n, [n.entity_type]) YIELD node
RETURN count(*)
"""
# Create a Neo4j driver
driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
try:
# Execute queries in batches
with driver.session() as session:
# Insert nodes in batches
session.execute_write(process_in_batches, create_nodes_query, nodes, BATCH_SIZE_NODES)
# Insert edges in batches
session.execute_write(process_in_batches, create_edges_query, edges, BATCH_SIZE_EDGES)
# Set displayName and labels
session.run(set_displayname_and_labels_query)
except Exception as e:
print(f"Error occurred: {e}")
finally:
driver.close()
if __name__ == "__main__":
main()
```
</details>
<details>
<summary> <b>Graphml 3d visualizer</b> </summary>
LightRag can be installed with Tools support to add extra tools like the graphml 3d visualizer.
[LightRag Visualizer](lightrag/tools/lightrag_visualizer/README.md)
</details>
![iShot_2025-03-23_12.40.08](./README.assets/iShot_2025-03-23_12.40.08.png)
## Evaluation
@@ -1386,28 +1216,28 @@ Output your evaluation in the following JSON format:
### Overall Performance Table
| | **Agriculture** | | **CS** | | **Legal** | | **Mix** | |
| --------------------------- | --------------------- | ------------------ | ------------ | ------------------ | --------------- | ------------------ | --------------- | ------------------ |
| | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** |
| **Comprehensiveness** | 32.4% | **67.6%** | 38.4% | **61.6%** | 16.4% | **83.6%** | 38.8% | **61.2%** |
| **Diversity** | 23.6% | **76.4%** | 38.0% | **62.0%** | 13.6% | **86.4%** | 32.4% | **67.6%** |
| **Empowerment** | 32.4% | **67.6%** | 38.8% | **61.2%** | 16.4% | **83.6%** | 42.8% | **57.2%** |
| **Overall** | 32.4% | **67.6%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 40.0% | **60.0%** |
| | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** |
| **Comprehensiveness** | 31.6% | **68.4%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 39.2% | **60.8%** |
| **Diversity** | 29.2% | **70.8%** | 39.2% | **60.8%** | 11.6% | **88.4%** | 30.8% | **69.2%** |
| **Empowerment** | 31.6% | **68.4%** | 36.4% | **63.6%** | 15.2% | **84.8%** | 42.4% | **57.6%** |
| **Overall** | 32.4% | **67.6%** | 38.0% | **62.0%** | 14.4% | **85.6%** | 40.0% | **60.0%** |
| | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** |
| **Comprehensiveness** | 26.0% | **74.0%** | 41.6% | **58.4%** | 26.8% | **73.2%** | 40.4% | **59.6%** |
| **Diversity** | 24.0% | **76.0%** | 38.8% | **61.2%** | 20.0% | **80.0%** | 32.4% | **67.6%** |
| **Empowerment** | 25.2% | **74.8%** | 40.8% | **59.2%** | 26.0% | **74.0%** | 46.0% | **54.0%** |
| **Overall** | 24.8% | **75.2%** | 41.6% | **58.4%** | 26.4% | **73.6%** | 42.4% | **57.6%** |
| | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** |
| **Comprehensiveness** | 45.6% | **54.4%** | 48.4% | **51.6%** | 48.4% | **51.6%** | **50.4%** | 49.6% |
| **Diversity** | 22.8% | **77.2%** | 40.8% | **59.2%** | 26.4% | **73.6%** | 36.0% | **64.0%** |
| **Empowerment** | 41.2% | **58.8%** | 45.2% | **54.8%** | 43.6% | **56.4%** | **50.8%** | 49.2% |
| **Overall** | 45.2% | **54.8%** | 48.0% | **52.0%** | 47.2% | **52.8%** | **50.4%** | 49.6% |
| |**Agriculture**| |**CS**| |**Legal**| |**Mix**| |
|----------------------|---------------|------------|------|------------|---------|------------|-------|------------|
| |NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|
|**Comprehensiveness**|32.4%|**67.6%**|38.4%|**61.6%**|16.4%|**83.6%**|38.8%|**61.2%**|
|**Diversity**|23.6%|**76.4%**|38.0%|**62.0%**|13.6%|**86.4%**|32.4%|**67.6%**|
|**Empowerment**|32.4%|**67.6%**|38.8%|**61.2%**|16.4%|**83.6%**|42.8%|**57.2%**|
|**Overall**|32.4%|**67.6%**|38.8%|**61.2%**|15.2%|**84.8%**|40.0%|**60.0%**|
| |RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|
|**Comprehensiveness**|31.6%|**68.4%**|38.8%|**61.2%**|15.2%|**84.8%**|39.2%|**60.8%**|
|**Diversity**|29.2%|**70.8%**|39.2%|**60.8%**|11.6%|**88.4%**|30.8%|**69.2%**|
|**Empowerment**|31.6%|**68.4%**|36.4%|**63.6%**|15.2%|**84.8%**|42.4%|**57.6%**|
|**Overall**|32.4%|**67.6%**|38.0%|**62.0%**|14.4%|**85.6%**|40.0%|**60.0%**|
| |HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|
|**Comprehensiveness**|26.0%|**74.0%**|41.6%|**58.4%**|26.8%|**73.2%**|40.4%|**59.6%**|
|**Diversity**|24.0%|**76.0%**|38.8%|**61.2%**|20.0%|**80.0%**|32.4%|**67.6%**|
|**Empowerment**|25.2%|**74.8%**|40.8%|**59.2%**|26.0%|**74.0%**|46.0%|**54.0%**|
|**Overall**|24.8%|**75.2%**|41.6%|**58.4%**|26.4%|**73.6%**|42.4%|**57.6%**|
| |GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|
|**Comprehensiveness**|45.6%|**54.4%**|48.4%|**51.6%**|48.4%|**51.6%**|**50.4%**|49.6%|
|**Diversity**|22.8%|**77.2%**|40.8%|**59.2%**|26.4%|**73.6%**|36.0%|**64.0%**|
|**Empowerment**|41.2%|**58.8%**|45.2%|**54.8%**|43.6%|**56.4%**|**50.8%**|49.2%|
|**Overall**|45.2%|**54.8%**|48.0%|**52.0%**|47.2%|**52.8%**|**50.4%**|49.6%|
## Reproduce

View File

@@ -13,9 +13,6 @@
# SSL_CERTFILE=/path/to/cert.pem
# SSL_KEYFILE=/path/to/key.pem
### Security (empty for no api-key is needed)
# LIGHTRAG_API_KEY=your-secure-api-key-here
### Directory Configuration
# WORKING_DIR=<absolute_path_for_working_dir>
# INPUT_DIR=<absolute_path_for_doc_input_dir>
@@ -39,21 +36,23 @@
# MAX_TOKEN_ENTITY_DESC=4000
### Settings for document indexing
# SUMMARY_LANGUAGE=English
ENABLE_LLM_CACHE_FOR_EXTRACT=true # Enable LLM cache for entity extraction
SUMMARY_LANGUAGE=English
# CHUNK_SIZE=1200
# CHUNK_OVERLAP_SIZE=100
# 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
### 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
TIMEOUT=150 # Time out in seconds for LLM, None for infinite timeout
TEMPERATURE=0.5
MAX_ASYNC=4 # Max concurrency requests of LLM
MAX_TOKENS=32768 # Max tokens send to LLM (less than context size of the model)
LLM_BINDING=ollama
LLM_MODEL=mistral-nemo:latest
LLM_BINDING_API_KEY=your_api_key
@@ -140,10 +139,17 @@ NEO4J_USERNAME=neo4j
NEO4J_PASSWORD='your_password'
### MongoDB Configuration
MONGODB_URI=mongodb://root:root@localhost:27017/
MONGODB_DATABASE=LightRAG
MONGO_URI=mongodb://root:root@localhost:27017/
MONGO_DATABASE=LightRAG
MONGODB_GRAPH=false # deprecated (keep for backward compatibility)
### Milvus Configuration
MILVUS_URI=http://localhost:19530
MILVUS_DB_NAME=lightrag
# MILVUS_USER=root
# MILVUS_PASSWORD=your_password
# MILVUS_TOKEN=your_token
### Qdrant
QDRANT_URL=http://localhost:16333
# QDRANT_API_KEY=your-api-key
@@ -152,7 +158,10 @@ QDRANT_URL=http://localhost:16333
REDIS_URI=redis://localhost:6379
### For JWT Auth
# AUTH_ACCOUNTS='admin:admin123,user1:pass456' # username:password,username:password
# TOKEN_SECRET=Your-Key-For-LightRAG-API-Server # JWT key
# TOKEN_EXPIRE_HOURS=4 # expire duration
# WHITELIST_PATHS= # white list
AUTH_ACCOUNTS='admin:admin123,user1:pass456' # username:password,username:password
TOKEN_SECRET=Your-Key-For-LightRAG-API-Server # JWT key
TOKEN_EXPIRE_HOURS=4 # expire duration
### API-Key to access LightRAG Server API
# LIGHTRAG_API_KEY=your-secure-api-key-here
# WHITELIST_PATHS=/health,/api/*

View File

@@ -1,5 +1,5 @@
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
__version__ = "1.2.8"
__version__ = "1.2.9"
__author__ = "Zirui Guo"
__url__ = "https://github.com/HKUDS/LightRAG"

559
lightrag/api/README-zh.md Normal file
View File

@@ -0,0 +1,559 @@
# LightRAG 服务器和 Web 界面
LightRAG 服务器旨在提供 Web 界面和 API 支持。Web 界面便于文档索引、知识图谱探索和简单的 RAG 查询界面。LightRAG 服务器还提供了与 Ollama 兼容的接口,旨在将 LightRAG 模拟为 Ollama 聊天模型。这使得 AI 聊天机器人(如 Open WebUI可以轻松访问 LightRAG。
![image-20250323122538997](./README.assets/image-20250323122538997.png)
![image-20250323122754387](./README.assets/image-20250323122754387.png)
![image-20250323123011220](./README.assets/image-20250323123011220.png)
## 入门指南
### 安装
* 从 PyPI 安装
```bash
pip install "lightrag-hku[api]"
```
* 从源代码安装
```bash
# 克隆仓库
git clone https://github.com/HKUDS/lightrag.git
# 切换到仓库目录
cd lightrag
# 如有必要,创建 Python 虚拟环境
# 以可编辑模式安装并支持 API
pip install -e ".[api]"
```
### 启动 LightRAG 服务器前的准备
LightRAG 需要同时集成 LLM大型语言模型和嵌入模型以有效执行文档索引和查询操作。在首次部署 LightRAG 服务器之前,必须配置 LLM 和嵌入模型的设置。LightRAG 支持绑定到各种 LLM/嵌入后端:
* ollama
* lollms
* openai 或 openai 兼容
* azure_openai
建议使用环境变量来配置 LightRAG 服务器。项目根目录中有一个名为 `env.example` 的示例环境变量文件。请将此文件复制到启动目录并重命名为 `.env`。之后,您可以在 `.env` 文件中修改与 LLM 和嵌入模型相关的参数。需要注意的是LightRAG 服务器每次启动时都会将 `.env` 中的环境变量加载到系统环境变量中。由于 LightRAG 服务器会优先使用系统环境变量中的设置,如果您在通过命令行启动 LightRAG 服务器后修改了 `.env` 文件,则需要执行 `source .env` 使新设置生效。
以下是 LLM 和嵌入模型的一些常见设置示例:
* OpenAI LLM + Ollama 嵌入
```
LLM_BINDING=openai
LLM_MODEL=gpt-4o
LLM_BINDING_HOST=https://api.openai.com/v1
LLM_BINDING_API_KEY=your_api_key
MAX_TOKENS=32768 # 发送给 LLM 的最大 token 数(小于模型上下文大小)
EMBEDDING_BINDING=ollama
EMBEDDING_BINDING_HOST=http://localhost:11434
EMBEDDING_MODEL=bge-m3:latest
EMBEDDING_DIM=1024
# EMBEDDING_BINDING_API_KEY=your_api_key
```
* Ollama LLM + Ollama 嵌入
```
LLM_BINDING=ollama
LLM_MODEL=mistral-nemo:latest
LLM_BINDING_HOST=http://localhost:11434
# LLM_BINDING_API_KEY=your_api_key
MAX_TOKENS=8192 # 发送给 LLM 的最大 token 数(基于您的 Ollama 服务器容量)
EMBEDDING_BINDING=ollama
EMBEDDING_BINDING_HOST=http://localhost:11434
EMBEDDING_MODEL=bge-m3:latest
EMBEDDING_DIM=1024
# EMBEDDING_BINDING_API_KEY=your_api_key
```
### 启动 LightRAG 服务器
LightRAG 服务器支持两种运行模式:
* 简单高效的 Uvicorn 模式
```
lightrag-server
```
* 多进程 Gunicorn + Uvicorn 模式(生产模式,不支持 Windows 环境)
```
lightrag-gunicorn --workers 4
```
`.env` 文件必须放在启动目录中。启动时LightRAG 服务器将创建一个文档目录(默认为 `./inputs`)和一个数据目录(默认为 `./rag_storage`)。这允许您从不同目录启动多个 LightRAG 服务器实例,每个实例配置为监听不同的网络端口。
以下是一些常用的启动参数:
- `--host`服务器监听地址默认0.0.0.0
- `--port`服务器监听端口默认9621
- `--timeout`LLM 请求超时时间默认150 秒)
- `--log-level`日志级别默认INFO
- --input-dir指定要扫描文档的目录默认./input
### 启动时自动扫描
当使用 `--auto-scan-at-startup` 参数启动任何服务器时,系统将自动:
1. 扫描输入目录中的新文件
2. 为尚未在数据库中的新文档建立索引
3. 使所有内容立即可用于 RAG 查询
> `--input-dir` 参数指定要扫描的输入目录。您可以从 webui 触发输入目录扫描。
### Gunicorn + Uvicorn 的多工作进程
LightRAG 服务器可以在 `Gunicorn + Uvicorn` 预加载模式下运行。Gunicorn 的多工作进程(多进程)功能可以防止文档索引任务阻塞 RAG 查询。使用 CPU 密集型文档提取工具(如 docling在纯 Uvicorn 模式下可能会导致整个系统被阻塞。
虽然 LightRAG 服务器使用一个工作进程来处理文档索引流程,但通过 Uvicorn 的异步任务支持,可以并行处理多个文件。文档索引速度的瓶颈主要在于 LLM。如果您的 LLM 支持高并发,您可以通过增加 LLM 的并发级别来加速文档索引。以下是几个与并发处理相关的环境变量及其默认值:
```
WORKERS=2 # 工作进程数,不大于 (2 x 核心数) + 1
MAX_PARALLEL_INSERT=2 # 一批中并行处理的文件数
MAX_ASYNC=4 # LLM 的最大并发请求数
```
### 将 Lightrag 安装为 Linux 服务
从示例文件 `lightrag.sevice.example` 创建您的服务文件 `lightrag.sevice`。修改服务文件中的 WorkingDirectory 和 ExecStart
```text
Description=LightRAG Ollama Service
WorkingDirectory=<lightrag 安装目录>
ExecStart=<lightrag 安装目录>/lightrag/api/lightrag-api
```
修改您的服务启动脚本:`lightrag-api`。根据需要更改 python 虚拟环境激活命令:
```shell
#!/bin/bash
# 您的 python 虚拟环境激活命令
source /home/netman/lightrag-xyj/venv/bin/activate
# 启动 lightrag api 服务器
lightrag-server
```
安装 LightRAG 服务。如果您的系统是 Ubuntu以下命令将生效
```shell
sudo cp lightrag.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl start lightrag.service
sudo systemctl status lightrag.service
sudo systemctl enable lightrag.service
```
## Ollama 模拟
我们为 LightRAG 提供了 Ollama 兼容接口,旨在将 LightRAG 模拟为 Ollama 聊天模型。这使得支持 Ollama 的 AI 聊天前端(如 Open WebUI可以轻松访问 LightRAG。
### 将 Open WebUI 连接到 LightRAG
启动 lightrag-server 后,您可以在 Open WebUI 管理面板中添加 Ollama 类型的连接。然后,一个名为 lightrag:latest 的模型将出现在 Open WebUI 的模型管理界面中。用户随后可以通过聊天界面向 LightRAG 发送查询。对于这种用例,最好将 LightRAG 安装为服务。
Open WebUI 使用 LLM 来执行会话标题和会话关键词生成任务。因此Ollama 聊天补全 API 会检测并将 OpenWebUI 会话相关请求直接转发给底层 LLM。Open WebUI 的截图:
![image-20250323194750379](./README.assets/image-20250323194750379.png)
### 在聊天中选择查询模式
查询字符串中的查询前缀可以决定使用哪种 LightRAG 查询模式来生成响应。支持的前缀包括:
```
/local
/global
/hybrid
/naive
/mix
/bypass
```
例如,聊天消息 "/mix 唐僧有几个徒弟" 将触发 LightRAG 的混合模式查询。没有查询前缀的聊天消息默认会触发混合模式查询。
"/bypass" 不是 LightRAG 查询模式,它会告诉 API 服务器将查询连同聊天历史直接传递给底层 LLM。因此用户可以使用 LLM 基于聊天历史回答问题。如果您使用 Open WebUI 作为前端,您可以直接切换到普通 LLM 模型,而不是使用 /bypass 前缀。
## API 密钥和认证
默认情况下LightRAG 服务器可以在没有任何认证的情况下访问。我们可以使用 API 密钥或账户凭证配置服务器以确保其安全。
* API 密钥
```
LIGHTRAG_API_KEY=your-secure-api-key-here
WHITELIST_PATHS=/health,/api/*
```
> 健康检查和 Ollama 模拟端点默认不进行 API 密钥检查。
* 账户凭证Web 界面需要登录后才能访问)
LightRAG API 服务器使用基于 HS256 算法的 JWT 认证。要启用安全访问控制,需要以下环境变量:
```bash
# JWT 认证
AUTH_USERNAME=admin # 登录名
AUTH_PASSWORD=admin123 # 密码
TOKEN_SECRET=your-key # JWT 密钥
TOKEN_EXPIRE_HOURS=4 # 过期时间
```
> 目前仅支持配置一个管理员账户和密码。尚未开发和实现完整的账户系统。
如果未配置账户凭证Web 界面将以访客身份访问系统。因此,即使仅配置了 API 密钥,所有 API 仍然可以通过访客账户访问,这仍然不安全。因此,要保护 API需要同时配置这两种认证方法。
## Azure OpenAI 后端配置
可以使用以下 Azure CLI 命令创建 Azure OpenAI API您需要先从 [https://docs.microsoft.com/en-us/cli/azure/install-azure-cli](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) 安装 Azure CLI
```bash
# 根据需要更改资源组名称、位置和 OpenAI 资源名称
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
```
最后一个命令的输出将提供 OpenAI API 的端点和密钥。您可以使用这些值在 `.env` 文件中设置环境变量。
```
# .env 中的 Azure OpenAI 配置
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 # 可选,默认为最新版本
EMBEDDING_BINDING=azure_openai # 如果使用 Azure OpenAI 进行嵌入
EMBEDDING_MODEL=your-embedding-deployment-name
```
## LightRAG 服务器详细配置
API 服务器可以通过三种方式配置(优先级从高到低):
* 命令行参数
* 环境变量或 .env 文件
* Config.ini仅用于存储配置
大多数配置都有默认设置,详细信息请查看示例文件:`.env.example`。数据存储配置也可以通过 config.ini 设置。为方便起见,提供了示例文件 `config.ini.example`
### 支持的 LLM 和嵌入后端
LightRAG 支持绑定到各种 LLM/嵌入后端:
* ollama
* lollms
* openai 和 openai 兼容
* azure_openai
使用环境变量 `LLM_BINDING` 或 CLI 参数 `--llm-binding` 选择 LLM 后端类型。使用环境变量 `EMBEDDING_BINDING` 或 CLI 参数 `--embedding-binding` 选择嵌入后端类型。
### 实体提取配置
* ENABLE_LLM_CACHE_FOR_EXTRACT为实体提取启用 LLM 缓存默认true
在测试环境中将 `ENABLE_LLM_CACHE_FOR_EXTRACT` 设置为 true 以减少 LLM 调用成本是很常见的做法。
### 支持的存储类型
LightRAG 使用 4 种类型的存储用于不同目的:
* KV_STORAGEllm 响应缓存、文本块、文档信息
* VECTOR_STORAGE实体向量、关系向量、块向量
* GRAPH_STORAGE实体关系图
* DOC_STATUS_STORAGE文档索引状态
每种存储类型都有几种实现:
* KV_STORAGE 支持的实现名称
```
JsonKVStorage JsonFile(默认)
MongoKVStorage MogonDB
RedisKVStorage Redis
TiDBKVStorage TiDB
PGKVStorage Postgres
OracleKVStorage Oracle
```
* GRAPH_STORAGE 支持的实现名称
```
NetworkXStorage NetworkX(默认)
Neo4JStorage Neo4J
MongoGraphStorage MongoDB
TiDBGraphStorage TiDB
AGEStorage AGE
GremlinStorage Gremlin
PGGraphStorage Postgres
OracleGraphStorage Postgres
```
* VECTOR_STORAGE 支持的实现名称
```
NanoVectorDBStorage NanoVector(默认)
MilvusVectorDBStorge Milvus
ChromaVectorDBStorage Chroma
TiDBVectorDBStorage TiDB
PGVectorStorage Postgres
FaissVectorDBStorage Faiss
QdrantVectorDBStorage Qdrant
OracleVectorDBStorage Oracle
MongoVectorDBStorage MongoDB
```
* DOC_STATUS_STORAGE 支持的实现名称
```
JsonDocStatusStorage JsonFile(默认)
PGDocStatusStorage Postgres
MongoDocStatusStorage MongoDB
```
### 如何选择存储实现
您可以通过环境变量选择存储实现。在首次启动 API 服务器之前,您可以将以下环境变量设置为特定的存储实现名称:
```
LIGHTRAG_KV_STORAGE=PGKVStorage
LIGHTRAG_VECTOR_STORAGE=PGVectorStorage
LIGHTRAG_GRAPH_STORAGE=PGGraphStorage
LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage
```
在向 LightRAG 添加文档后,您不能更改存储实现选择。目前尚不支持从一个存储实现迁移到另一个存储实现。更多信息请阅读示例 env 文件或 config.ini 文件。
### LightRag API 服务器命令行选项
| 参数 | 默认值 | 描述 |
|-----------|---------|-------------|
| --host | 0.0.0.0 | 服务器主机 |
| --port | 9621 | 服务器端口 |
| --working-dir | ./rag_storage | RAG 存储的工作目录 |
| --input-dir | ./inputs | 包含输入文档的目录 |
| --max-async | 4 | 最大异步操作数 |
| --max-tokens | 32768 | 最大 token 大小 |
| --timeout | 150 | 超时时间。None 表示无限超时(不推荐) |
| --log-level | INFO | 日志级别DEBUG、INFO、WARNING、ERROR、CRITICAL |
| --verbose | - | 详细调试输出True、False |
| --key | None | 用于认证的 API 密钥。保护 lightrag 服务器免受未授权访问 |
| --ssl | False | 启用 HTTPS |
| --ssl-certfile | None | SSL 证书文件路径(如果启用 --ssl 则必需) |
| --ssl-keyfile | None | SSL 私钥文件路径(如果启用 --ssl 则必需) |
| --top-k | 50 | 要检索的 top-k 项目数;在"local"模式下对应实体,在"global"模式下对应关系。 |
| --cosine-threshold | 0.4 | 节点和关系检索的余弦阈值,与 top-k 一起控制节点和关系的检索。 |
| --llm-binding | ollama | LLM 绑定类型lollms、ollama、openai、openai-ollama、azure_openai |
| --embedding-binding | ollama | 嵌入绑定类型lollms、ollama、openai、azure_openai |
| auto-scan-at-startup | - | 扫描输入目录中的新文件并开始索引 |
### 使用示例
#### 使用 ollama 默认本地服务器作为 llm 和嵌入后端运行 Lightrag 服务器
Ollama 是 llm 和嵌入的默认后端,因此默认情况下您可以不带参数运行 lightrag-server将使用默认值。确保已安装 ollama 并且正在运行,且默认模型已安装在 ollama 上。
```bash
# 使用 ollama 运行 lightragllm 使用 mistral-nemo:latest嵌入使用 bge-m3:latest
lightrag-server
# 使用认证密钥
lightrag-server --key my-key
```
#### 使用 lollms 默认本地服务器作为 llm 和嵌入后端运行 Lightrag 服务器
```bash
# 使用 lollms 运行 lightragllm 使用 mistral-nemo:latest嵌入使用 bge-m3:latest
# 在 .env 或 config.ini 中配置 LLM_BINDING=lollms 和 EMBEDDING_BINDING=lollms
lightrag-server
# 使用认证密钥
lightrag-server --key my-key
```
#### 使用 openai 服务器作为 llm 和嵌入后端运行 Lightrag 服务器
```bash
# 使用 openai 运行 lightragllm 使用 GPT-4o-mini嵌入使用 text-embedding-3-small
# 在 .env 或 config.ini 中配置:
# LLM_BINDING=openai
# LLM_MODEL=GPT-4o-mini
# EMBEDDING_BINDING=openai
# EMBEDDING_MODEL=text-embedding-3-small
lightrag-server
# 使用认证密钥
lightrag-server --key my-key
```
#### 使用 azure openai 服务器作为 llm 和嵌入后端运行 Lightrag 服务器
```bash
# 使用 azure_openai 运行 lightrag
# 在 .env 或 config.ini 中配置:
# LLM_BINDING=azure_openai
# LLM_MODEL=your-model
# EMBEDDING_BINDING=azure_openai
# EMBEDDING_MODEL=your-embedding-model
lightrag-server
# 使用认证密钥
lightrag-server --key my-key
```
**重要说明:**
- 对于 LoLLMs确保指定的模型已安装在您的 LoLLMs 实例中
- 对于 Ollama确保指定的模型已安装在您的 Ollama 实例中
- 对于 OpenAI确保您已设置 OPENAI_API_KEY 环境变量
- 对于 Azure OpenAI按照先决条件部分所述构建和配置您的服务器
要获取任何服务器的帮助,使用 --help 标志:
```bash
lightrag-server --help
```
注意:如果您不需要 API 功能,可以使用以下命令安装不带 API 支持的基本包:
```bash
pip install lightrag-hku
```
## API 端点
所有服务器LoLLMs、Ollama、OpenAI 和 Azure OpenAI都为 RAG 功能提供相同的 REST API 端点。当 API 服务器运行时,访问:
- Swagger UIhttp://localhost:9621/docs
- ReDochttp://localhost:9621/redoc
您可以使用提供的 curl 命令或通过 Swagger UI 界面测试 API 端点。确保:
1. 启动适当的后端服务LoLLMs、Ollama 或 OpenAI
2. 启动 RAG 服务器
3. 使用文档管理端点上传一些文档
4. 使用查询端点查询系统
5. 如果在输入目录中放入新文件,触发文档扫描
### 查询端点
#### POST /query
使用不同搜索模式查询 RAG 系统。
```bash
curl -X POST "http://localhost:9621/query" \
-H "Content-Type: application/json" \
-d '{"query": "您的问题", "mode": "hybrid", ""}'
```
#### POST /query/stream
从 RAG 系统流式获取响应。
```bash
curl -X POST "http://localhost:9621/query/stream" \
-H "Content-Type: application/json" \
-d '{"query": "您的问题", "mode": "hybrid"}'
```
### 文档管理端点
#### POST /documents/text
直接将文本插入 RAG 系统。
```bash
curl -X POST "http://localhost:9621/documents/text" \
-H "Content-Type: application/json" \
-d '{"text": "您的文本内容", "description": "可选描述"}'
```
#### POST /documents/file
向 RAG 系统上传单个文件。
```bash
curl -X POST "http://localhost:9621/documents/file" \
-F "file=@/path/to/your/document.txt" \
-F "description=可选描述"
```
#### POST /documents/batch
一次上传多个文件。
```bash
curl -X POST "http://localhost:9621/documents/batch" \
-F "files=@/path/to/doc1.txt" \
-F "files=@/path/to/doc2.txt"
```
#### POST /documents/scan
触发输入目录中新文件的文档扫描。
```bash
curl -X POST "http://localhost:9621/documents/scan" --max-time 1800
```
> 根据所有新文件的预计索引时间调整 max-time。
#### DELETE /documents
从 RAG 系统中清除所有文档。
```bash
curl -X DELETE "http://localhost:9621/documents"
```
### Ollama 模拟端点
#### GET /api/version
获取 Ollama 版本信息。
```bash
curl http://localhost:9621/api/version
```
#### GET /api/tags
获取 Ollama 可用模型。
```bash
curl http://localhost:9621/api/tags
```
#### POST /api/chat
处理聊天补全请求。通过根据查询前缀选择查询模式将用户查询路由到 LightRAG。检测并将 OpenWebUI 会话相关请求(用于元数据生成任务)直接转发给底层 LLM。
```shell
curl -N -X POST http://localhost:9621/api/chat -H "Content-Type: application/json" -d \
'{"model":"lightrag:latest","messages":[{"role":"user","content":"猪八戒是谁"}],"stream":true}'
```
> 有关 Ollama API 的更多信息,请访问:[Ollama API 文档](https://github.com/ollama/ollama/blob/main/docs/api.md)
#### POST /api/generate
处理生成补全请求。为了兼容性目的,该请求不由 LightRAG 处理,而是由底层 LLM 模型处理。
### 实用工具端点
#### GET /health
检查服务器健康状况和配置。
```bash
curl "http://localhost:9621/health"
```

View File

@@ -153,10 +153,6 @@ sudo systemctl status lightrag.service
sudo systemctl enable lightrag.service
```
## 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.
@@ -196,8 +192,11 @@ By default, the LightRAG Server can be accessed without any authentication. We c
```
LIGHTRAG_API_KEY=your-secure-api-key-here
WHITELIST_PATHS=/health,/api/*
```
> Health check and Ollama emuluation endpoins is exclude from API-KEY check by default.
* 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:
@@ -317,7 +316,7 @@ OracleGraphStorage Postgres
```
NanoVectorDBStorage NanoVector(default)
MilvusVectorDBStorge Milvus
MilvusVectorDBStorage Milvus
ChromaVectorDBStorage Chroma
TiDBVectorDBStorage TiDB
PGVectorStorage Postgres

View File

@@ -1 +1 @@
__api_version__ = "1.2.2"
__api_version__ = "1.2.5"

View File

@@ -18,7 +18,7 @@ from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from lightrag.api.utils_api import (
get_api_key_dependency,
get_combined_auth_dependency,
parse_args,
get_default_host,
display_splash_screen,
@@ -41,7 +41,6 @@ from lightrag.kg.shared_storage import (
get_namespace_data,
get_pipeline_status_lock,
initialize_pipeline_status,
get_all_update_flags_status,
)
from fastapi.security import OAuth2PasswordRequestForm
from .auth import auth_handler
@@ -136,19 +135,28 @@ def create_app(args):
await rag.finalize_storages()
# Initialize FastAPI
app = FastAPI(
title="LightRAG API",
description="API for querying text using LightRAG with separate storage and input directories"
app_kwargs = {
"title": "LightRAG Server API",
"description": "Providing API for LightRAG core, Web UI and Ollama Model Emulation"
+ "(With authentication)"
if api_key
else "",
version=__api_version__,
openapi_url="/openapi.json", # Explicitly set OpenAPI schema URL
docs_url="/docs", # Explicitly set docs URL
redoc_url="/redoc", # Explicitly set redoc URL
openapi_tags=[{"name": "api"}],
lifespan=lifespan,
)
"version": __api_version__,
"openapi_url": "/openapi.json", # Explicitly set OpenAPI schema URL
"docs_url": "/docs", # Explicitly set docs URL
"redoc_url": "/redoc", # Explicitly set redoc URL
"openapi_tags": [{"name": "api"}],
"lifespan": lifespan,
}
# Configure Swagger UI parameters
# Enable persistAuthorization and tryItOutEnabled for better user experience
app_kwargs["swagger_ui_parameters"] = {
"persistAuthorization": True,
"tryItOutEnabled": True,
}
app = FastAPI(**app_kwargs)
def get_cors_origins():
"""Get allowed origins from environment variable
@@ -168,8 +176,8 @@ def create_app(args):
allow_headers=["*"],
)
# Create the optional API key dependency
optional_api_key = get_api_key_dependency(api_key)
# Create combined auth dependency for all endpoints
combined_auth = get_combined_auth_dependency(api_key)
# Create working directory if it doesn't exist
Path(args.working_dir).mkdir(parents=True, exist_ok=True)
@@ -200,6 +208,7 @@ def create_app(args):
kwargs["response_format"] = GPTKeywordExtractionFormat
if history_messages is None:
history_messages = []
kwargs["temperature"] = args.temperature
return await openai_complete_if_cache(
args.llm_model,
prompt,
@@ -222,6 +231,7 @@ def create_app(args):
kwargs["response_format"] = GPTKeywordExtractionFormat
if history_messages is None:
history_messages = []
kwargs["temperature"] = args.temperature
return await azure_openai_complete_if_cache(
args.llm_model,
prompt,
@@ -302,6 +312,7 @@ def create_app(args):
},
namespace_prefix=args.namespace_prefix,
auto_manage_storages_states=False,
max_parallel_insert=args.max_parallel_insert,
)
else: # azure_openai
rag = LightRAG(
@@ -331,6 +342,7 @@ def create_app(args):
},
namespace_prefix=args.namespace_prefix,
auto_manage_storages_states=False,
max_parallel_insert=args.max_parallel_insert,
)
# Add routes
@@ -339,7 +351,7 @@ def create_app(args):
app.include_router(create_graph_routes(rag, api_key))
# Add Ollama API routes
ollama_api = OllamaAPI(rag, top_k=args.top_k)
ollama_api = OllamaAPI(rag, top_k=args.top_k, api_key=api_key)
app.include_router(ollama_api.router, prefix="/api")
@app.get("/")
@@ -347,7 +359,7 @@ def create_app(args):
"""Redirect root path to /webui"""
return RedirectResponse(url="/webui")
@app.get("/auth-status", dependencies=[Depends(optional_api_key)])
@app.get("/auth-status")
async def get_auth_status():
"""Get authentication status and guest token if auth is not configured"""
@@ -373,7 +385,7 @@ def create_app(args):
"api_version": __api_version__,
}
@app.post("/login", dependencies=[Depends(optional_api_key)])
@app.post("/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
if not auth_handler.accounts:
# Authentication not configured, return guest token
@@ -406,12 +418,9 @@ def create_app(args):
"api_version": __api_version__,
}
@app.get("/health", dependencies=[Depends(optional_api_key)])
@app.get("/health", dependencies=[Depends(combined_auth)])
async def get_status():
"""Get current system status"""
# 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):
@@ -439,7 +448,6 @@ def create_app(args):
"vector_storage": args.vector_storage,
"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,

View File

@@ -17,15 +17,13 @@ from pydantic import BaseModel, Field, field_validator
from lightrag import LightRAG
from lightrag.base import DocProcessingStatus, DocStatus
from lightrag.api.utils_api import (
get_api_key_dependency,
get_combined_auth_dependency,
global_args,
get_auth_dependency,
)
router = APIRouter(
prefix="/documents",
tags=["documents"],
dependencies=[Depends(get_auth_dependency())],
)
# Temporary file prefix
@@ -113,6 +111,7 @@ class PipelineStatusResponse(BaseModel):
request_pending: Flag for pending request for processing
latest_message: Latest message from pipeline processing
history_messages: List of history messages
update_status: Status of update flags for all namespaces
"""
autoscanned: bool = False
@@ -125,6 +124,7 @@ class PipelineStatusResponse(BaseModel):
request_pending: bool = False
latest_message: str = ""
history_messages: Optional[List[str]] = None
update_status: Optional[dict] = None
class Config:
extra = "allow" # Allow additional fields from the pipeline status
@@ -475,8 +475,8 @@ async def run_scanning_process(rag: LightRAG, doc_manager: DocumentManager):
if not new_files:
return
# Get MAX_PARALLEL_INSERT from global_args
max_parallel = global_args["max_parallel_insert"]
# Get MAX_PARALLEL_INSERT from global_args["main_args"]
max_parallel = global_args["main_args"].max_parallel_insert
# Calculate batch size as 2 * MAX_PARALLEL_INSERT
batch_size = 2 * max_parallel
@@ -505,9 +505,10 @@ async def run_scanning_process(rag: LightRAG, doc_manager: DocumentManager):
def create_document_routes(
rag: LightRAG, doc_manager: DocumentManager, api_key: Optional[str] = None
):
optional_api_key = get_api_key_dependency(api_key)
# Create combined auth dependency for document routes
combined_auth = get_combined_auth_dependency(api_key)
@router.post("/scan", dependencies=[Depends(optional_api_key)])
@router.post("/scan", dependencies=[Depends(combined_auth)])
async def scan_for_new_documents(background_tasks: BackgroundTasks):
"""
Trigger the scanning process for new documents.
@@ -523,7 +524,7 @@ def create_document_routes(
background_tasks.add_task(run_scanning_process, rag, doc_manager)
return {"status": "scanning_started"}
@router.post("/upload", dependencies=[Depends(optional_api_key)])
@router.post("/upload", dependencies=[Depends(combined_auth)])
async def upload_to_input_dir(
background_tasks: BackgroundTasks, file: UploadFile = File(...)
):
@@ -568,7 +569,7 @@ def create_document_routes(
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/text", response_model=InsertResponse, dependencies=[Depends(optional_api_key)]
"/text", response_model=InsertResponse, dependencies=[Depends(combined_auth)]
)
async def insert_text(
request: InsertTextRequest, background_tasks: BackgroundTasks
@@ -603,7 +604,7 @@ def create_document_routes(
@router.post(
"/texts",
response_model=InsertResponse,
dependencies=[Depends(optional_api_key)],
dependencies=[Depends(combined_auth)],
)
async def insert_texts(
request: InsertTextsRequest, background_tasks: BackgroundTasks
@@ -636,7 +637,7 @@ def create_document_routes(
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/file", response_model=InsertResponse, dependencies=[Depends(optional_api_key)]
"/file", response_model=InsertResponse, dependencies=[Depends(combined_auth)]
)
async def insert_file(
background_tasks: BackgroundTasks, file: UploadFile = File(...)
@@ -681,7 +682,7 @@ def create_document_routes(
@router.post(
"/file_batch",
response_model=InsertResponse,
dependencies=[Depends(optional_api_key)],
dependencies=[Depends(combined_auth)],
)
async def insert_batch(
background_tasks: BackgroundTasks, files: List[UploadFile] = File(...)
@@ -742,7 +743,7 @@ def create_document_routes(
raise HTTPException(status_code=500, detail=str(e))
@router.delete(
"", response_model=InsertResponse, dependencies=[Depends(optional_api_key)]
"", response_model=InsertResponse, dependencies=[Depends(combined_auth)]
)
async def clear_documents():
"""
@@ -771,7 +772,7 @@ def create_document_routes(
@router.get(
"/pipeline_status",
dependencies=[Depends(optional_api_key)],
dependencies=[Depends(combined_auth)],
response_model=PipelineStatusResponse,
)
async def get_pipeline_status() -> PipelineStatusResponse:
@@ -798,13 +799,34 @@ def create_document_routes(
HTTPException: If an error occurs while retrieving pipeline status (500)
"""
try:
from lightrag.kg.shared_storage import get_namespace_data
from lightrag.kg.shared_storage import (
get_namespace_data,
get_all_update_flags_status,
)
pipeline_status = await get_namespace_data("pipeline_status")
# Get update flags status for all namespaces
update_status = await get_all_update_flags_status()
# Convert MutableBoolean objects to regular boolean values
processed_update_status = {}
for namespace, flags in update_status.items():
processed_flags = []
for flag in flags:
# Handle both multiprocess and single process cases
if hasattr(flag, "value"):
processed_flags.append(bool(flag.value))
else:
processed_flags.append(bool(flag))
processed_update_status[namespace] = processed_flags
# Convert to regular dict if it's a Manager.dict
status_dict = dict(pipeline_status)
# Add processed update_status to the status dictionary
status_dict["update_status"] = processed_update_status
# Convert history_messages to a regular list if it's a Manager.list
if "history_messages" in status_dict:
status_dict["history_messages"] = list(status_dict["history_messages"])
@@ -819,7 +841,7 @@ def create_document_routes(
logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@router.get("", dependencies=[Depends(optional_api_key)])
@router.get("", dependencies=[Depends(combined_auth)])
async def documents() -> DocsStatusesResponse:
"""
Get the status of all documents in the system.

View File

@@ -5,15 +5,15 @@ This module contains all graph-related routes for the LightRAG API.
from typing import Optional
from fastapi import APIRouter, Depends
from ..utils_api import get_api_key_dependency, get_auth_dependency
from ..utils_api import get_combined_auth_dependency
router = APIRouter(tags=["graph"], dependencies=[Depends(get_auth_dependency())])
router = APIRouter(tags=["graph"])
def create_graph_routes(rag, api_key: Optional[str] = None):
optional_api_key = get_api_key_dependency(api_key)
combined_auth = get_combined_auth_dependency(api_key)
@router.get("/graph/label/list", dependencies=[Depends(optional_api_key)])
@router.get("/graph/label/list", dependencies=[Depends(combined_auth)])
async def get_graph_labels():
"""
Get all graph labels
@@ -23,7 +23,7 @@ def create_graph_routes(rag, api_key: Optional[str] = None):
"""
return await rag.get_graph_labels()
@router.get("/graphs", dependencies=[Depends(optional_api_key)])
@router.get("/graphs", dependencies=[Depends(combined_auth)])
async def get_knowledge_graph(
label: str, max_depth: int = 3, min_degree: int = 0, inclusive: bool = False
):

View File

@@ -11,7 +11,8 @@ import asyncio
from ascii_colors import trace_exception
from lightrag import LightRAG, QueryParam
from lightrag.utils import encode_string_by_tiktoken
from lightrag.api.utils_api import ollama_server_infos
from lightrag.api.utils_api import ollama_server_infos, get_combined_auth_dependency
from fastapi import Depends
# query mode according to query prefix (bypass is not LightRAG quer mode)
@@ -122,20 +123,24 @@ def parse_query_mode(query: str) -> tuple[str, SearchMode]:
class OllamaAPI:
def __init__(self, rag: LightRAG, top_k: int = 60):
def __init__(self, rag: LightRAG, top_k: int = 60, api_key: Optional[str] = None):
self.rag = rag
self.ollama_server_infos = ollama_server_infos
self.top_k = top_k
self.api_key = api_key
self.router = APIRouter(tags=["ollama"])
self.setup_routes()
def setup_routes(self):
@self.router.get("/version")
# Create combined auth dependency for Ollama API routes
combined_auth = get_combined_auth_dependency(self.api_key)
@self.router.get("/version", dependencies=[Depends(combined_auth)])
async def get_version():
"""Get Ollama version information"""
return OllamaVersionResponse(version="0.5.4")
@self.router.get("/tags")
@self.router.get("/tags", dependencies=[Depends(combined_auth)])
async def get_tags():
"""Return available models acting as an Ollama server"""
return OllamaTagResponse(
@@ -158,7 +163,7 @@ class OllamaAPI:
]
)
@self.router.post("/generate")
@self.router.post("/generate", dependencies=[Depends(combined_auth)])
async def generate(raw_request: Request, request: OllamaGenerateRequest):
"""Handle generate completion requests acting as an Ollama model
For compatibility purpose, the request is not processed by LightRAG,
@@ -324,7 +329,7 @@ class OllamaAPI:
trace_exception(e)
raise HTTPException(status_code=500, detail=str(e))
@self.router.post("/chat")
@self.router.post("/chat", dependencies=[Depends(combined_auth)])
async def chat(raw_request: Request, request: OllamaChatRequest):
"""Process chat completion requests acting as an Ollama model
Routes user queries through LightRAG by selecting query mode based on prefix indicators.

View File

@@ -8,12 +8,12 @@ from typing import Any, Dict, List, Literal, Optional
from fastapi import APIRouter, Depends, HTTPException
from lightrag.base import QueryParam
from ..utils_api import get_api_key_dependency, get_auth_dependency
from ..utils_api import get_combined_auth_dependency
from pydantic import BaseModel, Field, field_validator
from ascii_colors import trace_exception
router = APIRouter(tags=["query"], dependencies=[Depends(get_auth_dependency())])
router = APIRouter(tags=["query"])
class QueryRequest(BaseModel):
@@ -139,10 +139,10 @@ class QueryResponse(BaseModel):
def create_query_routes(rag, api_key: Optional[str] = None, top_k: int = 60):
optional_api_key = get_api_key_dependency(api_key)
combined_auth = get_combined_auth_dependency(api_key)
@router.post(
"/query", response_model=QueryResponse, dependencies=[Depends(optional_api_key)]
"/query", response_model=QueryResponse, dependencies=[Depends(combined_auth)]
)
async def query_text(request: QueryRequest):
"""
@@ -176,7 +176,7 @@ def create_query_routes(rag, api_key: Optional[str] = None, top_k: int = 60):
trace_exception(e)
raise HTTPException(status_code=500, detail=str(e))
@router.post("/query/stream", dependencies=[Depends(optional_api_key)])
@router.post("/query/stream", dependencies=[Depends(combined_auth)])
async def query_text_stream(request: QueryRequest):
"""
This endpoint performs a retrieval-augmented generation (RAG) query and streams the response.

View File

@@ -4,22 +4,42 @@ Utility functions for the LightRAG API.
import os
import argparse
from typing import Optional
from typing import Optional, List, Tuple
import sys
import logging
from ascii_colors import ASCIIColors
from lightrag.api import __api_version__
from fastapi import HTTPException, Security, Depends, Request, status
from fastapi import HTTPException, Security, Request, status
from dotenv import load_dotenv
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
from starlette.status import HTTP_403_FORBIDDEN
from .auth import auth_handler
from ..prompt import PROMPTS
# Load environment variables
load_dotenv()
global_args = {"main_args": None}
# Get whitelist paths from environment variable, only once during initialization
default_whitelist = "/health,/api/*"
whitelist_paths = os.getenv("WHITELIST_PATHS", default_whitelist).split(",")
# Pre-compile path matching patterns
whitelist_patterns: List[Tuple[str, bool]] = []
for path in whitelist_paths:
path = path.strip()
if path:
# If path ends with /*, match all paths with that prefix
if path.endswith("/*"):
prefix = path[:-2]
whitelist_patterns.append((prefix, True)) # (prefix, is_prefix_match)
else:
whitelist_patterns.append((path, False)) # (exact_path, is_prefix_match)
# Global authentication configuration
auth_configured = bool(auth_handler.accounts)
class OllamaServerInfos:
# Constants for emulated Ollama model information
@@ -34,47 +54,114 @@ class OllamaServerInfos:
ollama_server_infos = OllamaServerInfos()
def get_auth_dependency():
# Set default whitelist paths
whitelist = os.getenv("WHITELIST_PATHS", "/login,/health").split(",")
def get_combined_auth_dependency(api_key: Optional[str] = None):
"""
Create a combined authentication dependency that implements authentication logic
based on API key, OAuth2 token, and whitelist paths.
async def dependency(
Args:
api_key (Optional[str]): API key for validation
Returns:
Callable: A dependency function that implements the authentication logic
"""
# Use global whitelist_patterns and auth_configured variables
# whitelist_patterns and auth_configured are already initialized at module level
# Only calculate api_key_configured as it depends on the function parameter
api_key_configured = bool(api_key)
# Create security dependencies with proper descriptions for Swagger UI
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="login", auto_error=False, description="OAuth2 Password Authentication"
)
# If API key is configured, create an API key header security
api_key_header = None
if api_key_configured:
api_key_header = APIKeyHeader(
name="X-API-Key", auto_error=False, description="API Key Authentication"
)
async def combined_dependency(
request: Request,
token: str = Depends(OAuth2PasswordBearer(tokenUrl="login", auto_error=False)),
token: str = Security(oauth2_scheme),
api_key_header_value: Optional[str] = None
if api_key_header is None
else Security(api_key_header),
):
# Check if authentication is configured
auth_configured = bool(auth_handler.accounts)
# 1. Check if path is in whitelist
path = request.url.path
for pattern, is_prefix in whitelist_patterns:
if (is_prefix and path.startswith(pattern)) or (
not is_prefix and path == pattern
):
return # Whitelist path, allow access
# If authentication is not configured, skip all validation
if not auth_configured:
return
# 2. Validate token first if provided in the request (Ensure 401 error if token is invalid)
if token:
try:
token_info = auth_handler.validate_token(token)
# Accept guest token if no auth is configured
if not auth_configured and token_info.get("role") == "guest":
return
# Accept non-guest token if auth is configured
if auth_configured and token_info.get("role") != "guest":
return
# For configured auth, allow whitelist paths without token
if request.url.path in whitelist:
return
# Require token for all other paths when auth is configured
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token required"
)
try:
token_info = auth_handler.validate_token(token)
# Reject guest tokens when authentication is configured
if token_info.get("role") == "guest":
# Token validation failed, immediately return 401 error
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required. Guest access not allowed when authentication is configured.",
detail="Invalid token. Please login again.",
)
except Exception:
except HTTPException as e:
# If already a 401 error, re-raise it
if e.status_code == status.HTTP_401_UNAUTHORIZED:
raise
# For other exceptions, continue processing
# 3. Acept all request if no API protection needed
if not auth_configured and not api_key_configured:
return
# 4. Validate API key if provided and API-Key authentication is configured
if (
api_key_configured
and api_key_header_value
and api_key_header_value == api_key
):
return # API key validation successful
### Authentication failed ####
# if password authentication is configured but not provided, ensure 401 error if auth_configured
if auth_configured and not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
status_code=status.HTTP_401_UNAUTHORIZED,
detail="No credentials provided. Please login.",
)
return
# if api key is provided but validation failed
if api_key_header_value:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="Invalid API Key",
)
return dependency
# if api_key_configured but not provided
if api_key_configured and not api_key_header_value:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="API Key required",
)
# Otherwise: refuse access and return 403 error
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
detail="API Key required or login authentication required.",
)
return combined_dependency
def get_api_key_dependency(api_key: Optional[str]):
@@ -88,19 +175,37 @@ def get_api_key_dependency(api_key: Optional[str]):
Returns:
Callable: A dependency function that validates the API key.
"""
if not api_key:
# Use global whitelist_patterns and auth_configured variables
# whitelist_patterns and auth_configured are already initialized at module level
# Only calculate api_key_configured as it depends on the function parameter
api_key_configured = bool(api_key)
if not api_key_configured:
# If no API key is configured, return a dummy dependency that always succeeds
async def no_auth():
async def no_auth(request: Request = None, **kwargs):
return None
return no_auth
# If API key is configured, use proper authentication
# If API key is configured, use proper authentication with Security for Swagger UI
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
async def api_key_auth(
api_key_header_value: Optional[str] = Security(api_key_header),
request: Request,
api_key_header_value: Optional[str] = Security(
api_key_header, description="API Key for authentication"
),
):
# Check if request path is in whitelist
path = request.url.path
for pattern, is_prefix in whitelist_patterns:
if (is_prefix and path.startswith(pattern)) or (
not is_prefix and path == pattern
):
return # Whitelist path, allow access
# Non-whitelist path, validate API key
if not api_key_header_value:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="API Key required"
@@ -364,7 +469,7 @@ def parse_args(is_uvicorn_mode: bool = False) -> argparse.Namespace:
)
# Get MAX_PARALLEL_INSERT from environment
global_args["max_parallel_insert"] = get_env_value("MAX_PARALLEL_INSERT", 2, int)
args.max_parallel_insert = get_env_value("MAX_PARALLEL_INSERT", 2, int)
# Handle openai-ollama special case
if args.llm_binding == "openai-ollama":
@@ -395,6 +500,9 @@ def parse_args(is_uvicorn_mode: bool = False) -> argparse.Namespace:
"ENABLE_LLM_CACHE_FOR_EXTRACT", True, bool
)
# Inject LLM temperature configuration
args.temperature = get_env_value("TEMPERATURE", 0.5, float)
# Select Document loading tool (DOCLING, DEFAULT)
args.document_loading_engine = get_env_value("DOCUMENT_LOADING_ENGINE", "DEFAULT")
@@ -462,6 +570,12 @@ def display_splash_screen(args: argparse.Namespace) -> None:
ASCIIColors.yellow(f"{args.llm_binding_host}")
ASCIIColors.white(" ├─ Model: ", end="")
ASCIIColors.yellow(f"{args.llm_model}")
ASCIIColors.white(" ├─ Temperature: ", end="")
ASCIIColors.yellow(f"{args.temperature}")
ASCIIColors.white(" ├─ Max Async for LLM: ", end="")
ASCIIColors.yellow(f"{args.max_async}")
ASCIIColors.white(" ├─ Max Tokens: ", end="")
ASCIIColors.yellow(f"{args.max_tokens}")
ASCIIColors.white(" └─ Timeout: ", end="")
ASCIIColors.yellow(f"{args.timeout if args.timeout else 'None (infinite)'}")
@@ -477,13 +591,12 @@ def display_splash_screen(args: argparse.Namespace) -> None:
ASCIIColors.yellow(f"{args.embedding_dim}")
# RAG Configuration
summary_language = os.getenv("SUMMARY_LANGUAGE", PROMPTS["DEFAULT_LANGUAGE"])
ASCIIColors.magenta("\n⚙️ RAG Configuration:")
ASCIIColors.white(" ├─ Max Async for LLM: ", end="")
ASCIIColors.yellow(f"{args.max_async}")
ASCIIColors.white(" ├─ Summary Language: ", end="")
ASCIIColors.yellow(f"{summary_language}")
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.yellow(f"{args.max_parallel_insert}")
ASCIIColors.white(" ├─ Max Embed Tokens: ", end="")
ASCIIColors.yellow(f"{args.max_embed_tokens}")
ASCIIColors.white(" ├─ Chunk Size: ", end="")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,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-qXLILB5u.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-BcBS1RaQ.css">
<script type="module" crossorigin src="/webui/assets/index-DJ53id6i.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-BwFyYQzx.css">
</head>
<body>
<div id="root"></div>

View File

@@ -19,7 +19,6 @@ from .shared_storage import (
get_storage_lock,
get_update_flag,
set_all_update_flags,
is_multiprocess,
)
@@ -73,9 +72,7 @@ class FaissVectorDBStorage(BaseVectorStorage):
# Acquire lock to prevent concurrent read and write
async with self._storage_lock:
# Check if storage was updated by another process
if (is_multiprocess and self.storage_updated.value) or (
not is_multiprocess and self.storage_updated
):
if self.storage_updated.value:
logger.info(
f"Process {os.getpid()} FAISS reloading {self.namespace} due to update by another process"
)
@@ -83,10 +80,7 @@ class FaissVectorDBStorage(BaseVectorStorage):
self._index = faiss.IndexFlatIP(self._dim)
self._id_to_meta = {}
self._load_faiss_index()
if is_multiprocess:
self.storage_updated.value = False
else:
self.storage_updated = False
self.storage_updated.value = False
return self._index
async def upsert(self, data: dict[str, dict[str, Any]]) -> None:
@@ -343,18 +337,19 @@ class FaissVectorDBStorage(BaseVectorStorage):
self._id_to_meta = {}
async def index_done_callback(self) -> None:
# Check if storage was updated by another process
if is_multiprocess and self.storage_updated.value:
# Storage was updated by another process, reload data instead of saving
logger.warning(
f"Storage for FAISS {self.namespace} was updated by another process, reloading..."
)
async with self._storage_lock:
self._index = faiss.IndexFlatIP(self._dim)
self._id_to_meta = {}
self._load_faiss_index()
self.storage_updated.value = False
return False # Return error
async with self._storage_lock:
# Check if storage was updated by another process
if self.storage_updated.value:
# Storage was updated by another process, reload data instead of saving
logger.warning(
f"Storage for FAISS {self.namespace} was updated by another process, reloading..."
)
async with self._storage_lock:
self._index = faiss.IndexFlatIP(self._dim)
self._id_to_meta = {}
self._load_faiss_index()
self.storage_updated.value = False
return False # Return error
# Acquire lock and perform persistence
async with self._storage_lock:
@@ -364,10 +359,7 @@ class FaissVectorDBStorage(BaseVectorStorage):
# Notify other processes that data has been updated
await set_all_update_flags(self.namespace)
# Reset own update flag to avoid self-reloading
if is_multiprocess:
self.storage_updated.value = False
else:
self.storage_updated = False
self.storage_updated.value = False
except Exception as e:
logger.error(f"Error saving FAISS index for {self.namespace}: {e}")
return False # Return error

View File

@@ -221,6 +221,7 @@ class MongoDocStatusStorage(DocStatusStorage):
created_at=doc.get("created_at"),
updated_at=doc.get("updated_at"),
chunks_count=doc.get("chunks_count", -1),
file_path=doc.get("file_path", doc["_id"]),
)
for doc in result
}

View File

@@ -20,7 +20,6 @@ from .shared_storage import (
get_storage_lock,
get_update_flag,
set_all_update_flags,
is_multiprocess,
)
@@ -57,16 +56,14 @@ class NanoVectorDBStorage(BaseVectorStorage):
# Get the update flag for cross-process update notification
self.storage_updated = await get_update_flag(self.namespace)
# Get the storage lock for use in other methods
self._storage_lock = get_storage_lock()
self._storage_lock = get_storage_lock(enable_logging=False)
async def _get_client(self):
"""Check if the storage should be reloaded"""
# Acquire lock to prevent concurrent read and write
async with self._storage_lock:
# Check if data needs to be reloaded
if (is_multiprocess and self.storage_updated.value) or (
not is_multiprocess and self.storage_updated
):
if self.storage_updated.value:
logger.info(
f"Process {os.getpid()} reloading {self.namespace} due to update by another process"
)
@@ -76,10 +73,7 @@ class NanoVectorDBStorage(BaseVectorStorage):
storage_file=self._client_file_name,
)
# Reset update flag
if is_multiprocess:
self.storage_updated.value = False
else:
self.storage_updated = False
self.storage_updated.value = False
return self._client
@@ -206,19 +200,20 @@ class NanoVectorDBStorage(BaseVectorStorage):
async def index_done_callback(self) -> bool:
"""Save data to disk"""
# Check if storage was updated by another process
if is_multiprocess and self.storage_updated.value:
# Storage was updated by another process, reload data instead of saving
logger.warning(
f"Storage for {self.namespace} was updated by another process, reloading..."
)
self._client = NanoVectorDB(
self.embedding_func.embedding_dim,
storage_file=self._client_file_name,
)
# Reset update flag
self.storage_updated.value = False
return False # Return error
async with self._storage_lock:
# Check if storage was updated by another process
if self.storage_updated.value:
# Storage was updated by another process, reload data instead of saving
logger.warning(
f"Storage for {self.namespace} was updated by another process, reloading..."
)
self._client = NanoVectorDB(
self.embedding_func.embedding_dim,
storage_file=self._client_file_name,
)
# Reset update flag
self.storage_updated.value = False
return False # Return error
# Acquire lock and perform persistence
async with self._storage_lock:
@@ -228,10 +223,7 @@ class NanoVectorDBStorage(BaseVectorStorage):
# Notify other processes that data has been updated
await set_all_update_flags(self.namespace)
# Reset own update flag to avoid self-reloading
if is_multiprocess:
self.storage_updated.value = False
else:
self.storage_updated = False
self.storage_updated.value = False
return True # Return success
except Exception as e:
logger.error(f"Error saving data for {self.namespace}: {e}")

View File

@@ -21,7 +21,6 @@ from .shared_storage import (
get_storage_lock,
get_update_flag,
set_all_update_flags,
is_multiprocess,
)
MAX_GRAPH_NODES = int(os.getenv("MAX_GRAPH_NODES", 1000))
@@ -110,9 +109,7 @@ class NetworkXStorage(BaseGraphStorage):
# Acquire lock to prevent concurrent read and write
async with self._storage_lock:
# Check if data needs to be reloaded
if (is_multiprocess and self.storage_updated.value) or (
not is_multiprocess and self.storage_updated
):
if self.storage_updated.value:
logger.info(
f"Process {os.getpid()} reloading graph {self.namespace} due to update by another process"
)
@@ -121,10 +118,7 @@ class NetworkXStorage(BaseGraphStorage):
NetworkXStorage.load_nx_graph(self._graphml_xml_file) or nx.Graph()
)
# Reset update flag
if is_multiprocess:
self.storage_updated.value = False
else:
self.storage_updated = False
self.storage_updated.value = False
return self._graph
@@ -401,18 +395,19 @@ class NetworkXStorage(BaseGraphStorage):
async def index_done_callback(self) -> bool:
"""Save data to disk"""
# Check if storage was updated by another process
if is_multiprocess and self.storage_updated.value:
# Storage was updated by another process, reload data instead of saving
logger.warning(
f"Graph for {self.namespace} was updated by another process, reloading..."
)
self._graph = (
NetworkXStorage.load_nx_graph(self._graphml_xml_file) or nx.Graph()
)
# Reset update flag
self.storage_updated.value = False
return False # Return error
async with self._storage_lock:
# Check if storage was updated by another process
if self.storage_updated.value:
# Storage was updated by another process, reload data instead of saving
logger.warning(
f"Graph for {self.namespace} was updated by another process, reloading..."
)
self._graph = (
NetworkXStorage.load_nx_graph(self._graphml_xml_file) or nx.Graph()
)
# Reset update flag
self.storage_updated.value = False
return False # Return error
# Acquire lock and perform persistence
async with self._storage_lock:
@@ -422,10 +417,7 @@ class NetworkXStorage(BaseGraphStorage):
# Notify other processes that data has been updated
await set_all_update_flags(self.namespace)
# Reset own update flag to avoid self-reloading
if is_multiprocess:
self.storage_updated.value = False
else:
self.storage_updated = False
self.storage_updated.value = False
return True # Return success
except Exception as e:
logger.error(f"Error saving graph for {self.namespace}: {e}")

View File

@@ -24,7 +24,7 @@ def direct_log(message, level="INFO", enable_output: bool = True):
T = TypeVar("T")
LockType = Union[ProcessLock, asyncio.Lock]
is_multiprocess = None
_is_multiprocess = None
_workers = None
_manager = None
_initialized = None
@@ -218,10 +218,10 @@ 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
async_lock = _async_locks.get("internal_lock") if _is_multiprocess else None
return UnifiedLock(
lock=_internal_lock,
is_async=not is_multiprocess,
is_async=not _is_multiprocess,
name="internal_lock",
enable_logging=enable_logging,
async_lock=async_lock,
@@ -230,10 +230,10 @@ def get_internal_lock(enable_logging: bool = False) -> UnifiedLock:
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
async_lock = _async_locks.get("storage_lock") if _is_multiprocess else None
return UnifiedLock(
lock=_storage_lock,
is_async=not is_multiprocess,
is_async=not _is_multiprocess,
name="storage_lock",
enable_logging=enable_logging,
async_lock=async_lock,
@@ -242,10 +242,10 @@ def get_storage_lock(enable_logging: bool = False) -> UnifiedLock:
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
async_lock = _async_locks.get("pipeline_status_lock") if _is_multiprocess else None
return UnifiedLock(
lock=_pipeline_status_lock,
is_async=not is_multiprocess,
is_async=not _is_multiprocess,
name="pipeline_status_lock",
enable_logging=enable_logging,
async_lock=async_lock,
@@ -254,10 +254,10 @@ def get_pipeline_status_lock(enable_logging: bool = False) -> UnifiedLock:
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
async_lock = _async_locks.get("graph_db_lock") if _is_multiprocess else None
return UnifiedLock(
lock=_graph_db_lock,
is_async=not is_multiprocess,
is_async=not _is_multiprocess,
name="graph_db_lock",
enable_logging=enable_logging,
async_lock=async_lock,
@@ -266,10 +266,10 @@ def get_graph_db_lock(enable_logging: bool = False) -> UnifiedLock:
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
async_lock = _async_locks.get("data_init_lock") if _is_multiprocess else None
return UnifiedLock(
lock=_data_init_lock,
is_async=not is_multiprocess,
is_async=not _is_multiprocess,
name="data_init_lock",
enable_logging=enable_logging,
async_lock=async_lock,
@@ -297,7 +297,7 @@ def initialize_share_data(workers: int = 1):
global \
_manager, \
_workers, \
is_multiprocess, \
_is_multiprocess, \
_storage_lock, \
_internal_lock, \
_pipeline_status_lock, \
@@ -312,14 +312,14 @@ def initialize_share_data(workers: int = 1):
# Check if already initialized
if _initialized:
direct_log(
f"Process {os.getpid()} Shared-Data already initialized (multiprocess={is_multiprocess})"
f"Process {os.getpid()} Shared-Data already initialized (multiprocess={_is_multiprocess})"
)
return
_workers = workers
if workers > 1:
is_multiprocess = True
_is_multiprocess = True
_manager = Manager()
_internal_lock = _manager.Lock()
_storage_lock = _manager.Lock()
@@ -343,7 +343,7 @@ def initialize_share_data(workers: int = 1):
f"Process {os.getpid()} Shared-Data created for Multiple Process (workers={workers})"
)
else:
is_multiprocess = False
_is_multiprocess = False
_internal_lock = asyncio.Lock()
_storage_lock = asyncio.Lock()
_pipeline_status_lock = asyncio.Lock()
@@ -372,7 +372,7 @@ async def initialize_pipeline_status():
return
# Create a shared list object for history_messages
history_messages = _manager.list() if is_multiprocess else []
history_messages = _manager.list() if _is_multiprocess else []
pipeline_namespace.update(
{
"autoscanned": False, # Auto-scan started
@@ -401,7 +401,7 @@ async def get_update_flag(namespace: str):
async with get_internal_lock():
if namespace not in _update_flags:
if is_multiprocess and _manager is not None:
if _is_multiprocess and _manager is not None:
_update_flags[namespace] = _manager.list()
else:
_update_flags[namespace] = []
@@ -409,7 +409,7 @@ async def get_update_flag(namespace: str):
f"Process {os.getpid()} initialized updated flags for namespace: [{namespace}]"
)
if is_multiprocess and _manager is not None:
if _is_multiprocess and _manager is not None:
new_update_flag = _manager.Value("b", False)
else:
# Create a simple mutable object to store boolean value for compatibility with mutiprocess
@@ -434,11 +434,7 @@ async def set_all_update_flags(namespace: str):
raise ValueError(f"Namespace {namespace} not found in update flags")
# Update flags for both modes
for i in range(len(_update_flags[namespace])):
if is_multiprocess:
_update_flags[namespace][i].value = True
else:
# Use .value attribute instead of direct assignment
_update_flags[namespace][i].value = True
_update_flags[namespace][i].value = True
async def clear_all_update_flags(namespace: str):
@@ -452,11 +448,7 @@ async def clear_all_update_flags(namespace: str):
raise ValueError(f"Namespace {namespace} not found in update flags")
# Update flags for both modes
for i in range(len(_update_flags[namespace])):
if is_multiprocess:
_update_flags[namespace][i].value = False
else:
# Use .value attribute instead of direct assignment
_update_flags[namespace][i].value = False
_update_flags[namespace][i].value = False
async def get_all_update_flags_status() -> Dict[str, list]:
@@ -474,7 +466,7 @@ async def get_all_update_flags_status() -> Dict[str, list]:
for namespace, flags in _update_flags.items():
worker_statuses = []
for flag in flags:
if is_multiprocess:
if _is_multiprocess:
worker_statuses.append(flag.value)
else:
worker_statuses.append(flag)
@@ -518,7 +510,7 @@ async def get_namespace_data(namespace: str) -> Dict[str, Any]:
async with get_internal_lock():
if namespace not in _shared_dicts:
if is_multiprocess and _manager is not None:
if _is_multiprocess and _manager is not None:
_shared_dicts[namespace] = _manager.dict()
else:
_shared_dicts[namespace] = {}
@@ -538,7 +530,7 @@ def finalize_share_data():
"""
global \
_manager, \
is_multiprocess, \
_is_multiprocess, \
_storage_lock, \
_internal_lock, \
_pipeline_status_lock, \
@@ -558,11 +550,11 @@ def finalize_share_data():
return
direct_log(
f"Process {os.getpid()} finalizing storage data (multiprocess={is_multiprocess})"
f"Process {os.getpid()} finalizing storage data (multiprocess={_is_multiprocess})"
)
# In multi-process mode, shut down the Manager
if is_multiprocess and _manager is not None:
if _is_multiprocess and _manager is not None:
try:
# Clear shared resources before shutting down Manager
if _shared_dicts is not None:
@@ -604,7 +596,7 @@ def finalize_share_data():
# Reset global variables
_manager = None
_initialized = None
is_multiprocess = None
_is_multiprocess = None
_shared_dicts = None
_init_flags = None
_storage_lock = None

View File

@@ -1,9 +1,8 @@
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 StatusIndicator from '@/components/status/StatusIndicator'
import { healthCheckInterval } from '@/lib/constants'
import { useBackendState, useAuthStore } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings'
@@ -22,26 +21,30 @@ function App() {
const message = useBackendState.use.message()
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
const currentTab = useSettingsStore.use.currentTab()
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
const [apiKeyAlertOpen, setApiKeyAlertOpen] = useState(false)
const versionCheckRef = useRef(false); // Prevent duplicate calls in Vite dev mode
const handleApiKeyAlertOpenChange = useCallback((open: boolean) => {
setApiKeyAlertOpen(open)
if (!open) {
useBackendState.getState().clear()
}
}, [])
// Health check - can be disabled
useEffect(() => {
// Only execute if health check is enabled
if (!enableHealthCheck) return;
// Only execute if health check is enabled and ApiKeyAlert is closed
if (!enableHealthCheck || apiKeyAlertOpen) return;
// 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]);
}, [enableHealthCheck, apiKeyAlertOpen]);
// Version check - independent and executed only once
useEffect(() => {
@@ -90,12 +93,10 @@ function App() {
useEffect(() => {
if (message) {
if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) {
setApiKeyInvalid(true)
return
setApiKeyAlertOpen(true)
}
}
setApiKeyInvalid(false)
}, [message, setApiKeyInvalid])
}, [message])
return (
<ThemeProvider>
@@ -123,8 +124,7 @@ function App() {
</div>
</Tabs>
{enableHealthCheck && <StatusIndicator />}
{message !== null && !apiKeyInvalid && <MessageAlert />}
{apiKeyInvalid && <ApiKeyAlert />}
<ApiKeyAlert open={apiKeyAlertOpen} onOpenChange={handleApiKeyAlertOpenChange} />
</main>
</TabVisibilityProvider>
</ThemeProvider>

View File

@@ -1,4 +1,5 @@
import { useState, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import {
AlertDialog,
AlertDialogContent,
@@ -12,10 +13,13 @@ import { useSettingsStore } from '@/stores/settings'
import { useBackendState } from '@/stores/state'
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
import { toast } from 'sonner'
interface ApiKeyAlertProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
const ApiKeyAlert = () => {
const [opened, setOpened] = useState<boolean>(true)
const ApiKeyAlert = ({ open: opened, onOpenChange: setOpened }: ApiKeyAlertProps) => {
const { t } = useTranslation()
const apiKey = useSettingsStore.use.apiKey()
const [tempApiKey, setTempApiKey] = useState<string>('')
const message = useBackendState.use.message()
@@ -32,14 +36,10 @@ const ApiKeyAlert = () => {
}
}, [message, setOpened])
const setApiKey = useCallback(async () => {
const setApiKey = useCallback(() => {
useSettingsStore.setState({ apiKey: tempApiKey || null })
if (await useBackendState.getState().check()) {
setOpened(false)
return
}
toast.error('API Key is invalid')
}, [tempApiKey])
setOpened(false)
}, [tempApiKey, setOpened])
const handleTempApiKeyChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -52,23 +52,32 @@ const ApiKeyAlert = () => {
<AlertDialog open={opened} onOpenChange={setOpened}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>API Key is required</AlertDialogTitle>
<AlertDialogDescription>Please enter your API key</AlertDialogDescription>
<AlertDialogTitle>{t('apiKeyAlert.title')}</AlertDialogTitle>
<AlertDialogDescription>
{t('apiKeyAlert.description')}
</AlertDialogDescription>
</AlertDialogHeader>
<form className="flex gap-2" onSubmit={(e) => e.preventDefault()}>
<Input
type="password"
value={tempApiKey}
onChange={handleTempApiKeyChange}
placeholder="Enter your API key"
className="max-h-full w-full min-w-0"
autoComplete="off"
/>
<div className="flex flex-col gap-4">
<form className="flex gap-2" onSubmit={(e) => e.preventDefault()}>
<Input
type="password"
value={tempApiKey}
onChange={handleTempApiKeyChange}
placeholder={t('apiKeyAlert.placeholder')}
className="max-h-full w-full min-w-0"
autoComplete="off"
/>
<Button onClick={setApiKey} variant="outline" size="sm">
Save
</Button>
</form>
<Button onClick={setApiKey} variant="outline" size="sm">
{t('apiKeyAlert.save')}
</Button>
</form>
{message && (
<div className="text-sm text-red-500">
{message}
</div>
)}
</div>
</AlertDialogContent>
</AlertDialog>
)

View File

@@ -1,56 +0,0 @@
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
import { useBackendState } from '@/stores/state'
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'
// import Button from '@/components/ui/Button'
// import { controlButtonVariant } from '@/lib/constants'
import { AlertCircle } from 'lucide-react'
const MessageAlert = () => {
const health = useBackendState.use.health()
const message = useBackendState.use.message()
const messageTitle = useBackendState.use.messageTitle()
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setTimeout(() => {
setIsMounted(true)
}, 50)
}, [])
return (
<Alert
// variant={health ? 'default' : 'destructive'}
className={cn(
'bg-background/90 absolute top-12 left-1/2 flex w-auto max-w-lg -translate-x-1/2 transform items-center gap-4 shadow-md backdrop-blur-lg transition-all duration-500 ease-in-out',
isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0',
!health && 'bg-red-700 text-white'
)}
>
{!health && (
<div>
<AlertCircle className="size-4" />
</div>
)}
<div>
<AlertTitle className="font-bold">{messageTitle}</AlertTitle>
<AlertDescription>{message}</AlertDescription>
</div>
{/* <div className="flex">
<div className="flex-auto" />
<Button
size="sm"
variant={controlButtonVariant}
className="border-primary max-h-8 border !p-2 text-xs"
onClick={() => useBackendState.getState().clear()}
>
Close
</Button>
</div> */}
</Alert>
)
}
export default MessageAlert

View File

@@ -2,7 +2,7 @@ import { cn } from '@/lib/utils'
import { useBackendState } from '@/stores/state'
import { useEffect, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
import StatusCard from '@/components/graph/StatusCard'
import StatusCard from '@/components/status/StatusCard'
import { useTranslation } from 'react-i18next'
const StatusIndicator = () => {

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react'
import React, { useState, useEffect, useCallback, useRef } from 'react'
import { Loader2 } from 'lucide-react'
import { useDebounce } from '@/hooks/useDebounce'
@@ -81,100 +81,97 @@ export function AsyncSearch<T>({
const [options, setOptions] = useState<T[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [selectedValue, setSelectedValue] = useState(value)
const [focusedValue, setFocusedValue] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
const [originalOptions, setOriginalOptions] = useState<T[]>([])
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
setMounted(true)
setSelectedValue(value)
}, [value])
}, [])
// Effect for initial fetch
// Handle clicks outside of the component
useEffect(() => {
const initializeOptions = async () => {
try {
setLoading(true)
setError(null)
// If we have a value, use it for the initial search
const data = value !== null ? await fetcher(value) : []
setOriginalOptions(data)
setOptions(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch options')
} finally {
setLoading(false)
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node) &&
open
) {
setOpen(false)
}
}
if (!mounted) {
initializeOptions()
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [mounted, fetcher, value])
}, [open])
const fetchOptions = useCallback(async (query: string) => {
try {
setLoading(true)
setError(null)
const data = await fetcher(query)
setOptions(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch options')
} finally {
setLoading(false)
}
}, [fetcher])
// Load options when search term changes
useEffect(() => {
const fetchOptions = async () => {
try {
setLoading(true)
setError(null)
const data = await fetcher(debouncedSearchTerm)
setOriginalOptions(data)
setOptions(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch options')
} finally {
setLoading(false)
}
}
if (!mounted) return
if (!mounted) {
fetchOptions()
} else if (!preload) {
fetchOptions()
} else if (preload) {
if (preload) {
if (debouncedSearchTerm) {
setOptions(
originalOptions.filter((option) =>
setOptions((prev) =>
prev.filter((option) =>
filterFn ? filterFn(option, debouncedSearchTerm) : true
)
)
} else {
setOptions(originalOptions)
}
} else {
fetchOptions(debouncedSearchTerm)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetcher, debouncedSearchTerm, mounted, preload, filterFn])
}, [mounted, debouncedSearchTerm, preload, filterFn, fetchOptions])
const handleSelect = useCallback(
(currentValue: string) => {
if (currentValue !== selectedValue) {
setSelectedValue(currentValue)
onChange(currentValue)
}
// Load initial value
useEffect(() => {
if (!mounted || !value) return
fetchOptions(value)
}, [mounted, value, fetchOptions])
const handleSelect = useCallback((currentValue: string) => {
onChange(currentValue)
requestAnimationFrame(() => {
// Blur the input to ensure focus event triggers on next click
const input = document.activeElement as HTMLElement
input?.blur()
// Close the dropdown
setOpen(false)
},
[selectedValue, setSelectedValue, setOpen, onChange]
)
})
}, [onChange])
const handleFocus = useCallback(
(currentValue: string) => {
if (currentValue !== focusedValue) {
setFocusedValue(currentValue)
onFocus(currentValue)
}
},
[focusedValue, setFocusedValue, onFocus]
)
const handleFocus = useCallback(() => {
setOpen(true)
// Use current search term to fetch options
fetchOptions(searchTerm)
}, [searchTerm, fetchOptions])
const handleMouseDown = useCallback((e: React.MouseEvent) => {
const target = e.target as HTMLElement
if (target.closest('.cmd-item')) {
e.preventDefault()
}
}, [])
return (
<div
ref={containerRef}
className={cn(disabled && 'cursor-not-allowed opacity-50', className)}
onFocus={() => {
setOpen(true)
}}
onBlur={() => setOpen(false)}
onMouseDown={handleMouseDown}
>
<Command shouldFilter={false} className="bg-transparent">
<div>
@@ -182,12 +179,13 @@ export function AsyncSearch<T>({
placeholder={placeholder}
value={searchTerm}
className="max-h-8"
onFocus={handleFocus}
onValueChange={(value) => {
setSearchTerm(value)
if (value && !open) setOpen(true)
if (!open) setOpen(true)
}}
/>
{loading && options.length > 0 && (
{loading && (
<div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
@@ -209,8 +207,8 @@ export function AsyncSearch<T>({
key={getOptionValue(option) + `${idx}`}
value={getOptionValue(option)}
onSelect={handleSelect}
onMouseEnter={() => handleFocus(getOptionValue(option))}
className="truncate"
onMouseMove={() => onFocus(getOptionValue(option))}
className="truncate cmd-item"
>
{renderOption(option)}
</CommandItem>

View File

@@ -67,18 +67,20 @@ export default function SiteHeader() {
return (
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
<a href={webuiPrefix} className="mr-6 flex items-center gap-2">
<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="w-[200px] flex items-center">
<a href={webuiPrefix} className="flex items-center gap-2">
<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>
<div className="flex h-10 flex-1 justify-center">
<div className="flex h-10 flex-1 items-center justify-center">
<TabsNavigation />
{isGuestMode && (
<div className="ml-2 self-center px-2 py-1 text-xs bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200 rounded-md">
@@ -87,7 +89,7 @@ export default function SiteHeader() {
)}
</div>
<nav className="flex items-center">
<nav className="w-[200px] flex items-center justify-end">
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">

View File

@@ -259,5 +259,11 @@
},
"apiSite": {
"loading": "جارٍ تحميل وثائق واجهة برمجة التطبيقات..."
},
"apiKeyAlert": {
"title": "مفتاح واجهة برمجة التطبيقات مطلوب",
"description": "الرجاء إدخال مفتاح واجهة برمجة التطبيقات للوصول إلى الخدمة",
"placeholder": "أدخل مفتاح واجهة برمجة التطبيقات",
"save": "حفظ"
}
}

View File

@@ -274,5 +274,11 @@
},
"apiSite": {
"loading": "Loading API Documentation..."
},
"apiKeyAlert": {
"title": "API Key is required",
"description": "Please enter your API key to access the service",
"placeholder": "Enter your API key",
"save": "Save"
}
}

View File

@@ -259,5 +259,11 @@
},
"apiSite": {
"loading": "Chargement de la documentation de l'API..."
},
"apiKeyAlert": {
"title": "Clé API requise",
"description": "Veuillez entrer votre clé API pour accéder au service",
"placeholder": "Entrez votre clé API",
"save": "Sauvegarder"
}
}

View File

@@ -259,5 +259,11 @@
},
"apiSite": {
"loading": "正在加载 API 文档..."
},
"apiKeyAlert": {
"title": "需要 API Key",
"description": "请输入您的 API Key 以访问服务",
"placeholder": "请输入 API Key",
"save": "保存"
}
}