choizhang commited on
Commit
c4b1597
·
2 Parent(s): c9b5d82 1f583de

Merge branch 'improve-property-tooltip' into loginPage

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +3 -0
  3. .pre-commit-config.yaml +6 -0
  4. README.md +166 -67
  5. lightrag/__init__.py +1 -1
  6. lightrag/api/gunicorn_config.py +1 -1
  7. lightrag/api/lightrag_server.py +17 -37
  8. lightrag/api/routers/document_routes.py +57 -10
  9. lightrag/api/webui/index.html +0 -0
  10. lightrag/kg/chroma_impl.py +3 -1
  11. lightrag/kg/faiss_impl.py +3 -1
  12. lightrag/kg/milvus_impl.py +3 -1
  13. lightrag/kg/mongo_impl.py +3 -1
  14. lightrag/kg/nano_vector_db_impl.py +3 -1
  15. lightrag/kg/neo4j_impl.py +7 -27
  16. lightrag/kg/oracle_impl.py +3 -1
  17. lightrag/kg/qdrant_impl.py +3 -1
  18. lightrag/kg/tidb_impl.py +3 -1
  19. lightrag/lightrag.py +323 -1
  20. lightrag/utils.py +1 -0
  21. lightrag_webui/bun.lock +3 -0
  22. lightrag_webui/index.html +3 -0
  23. lightrag_webui/package.json +1 -0
  24. lightrag_webui/src/App.tsx +33 -27
  25. lightrag_webui/src/components/AppSettings.tsx +66 -0
  26. lightrag_webui/src/components/Root.tsx +24 -0
  27. lightrag_webui/src/components/ThemeProvider.tsx +16 -14
  28. lightrag_webui/src/components/graph/FocusOnNode.tsx +13 -4
  29. lightrag_webui/src/components/graph/GraphControl.tsx +51 -22
  30. lightrag_webui/src/components/graph/GraphLabels.tsx +69 -37
  31. lightrag_webui/src/components/graph/GraphSearch.tsx +25 -4
  32. lightrag_webui/src/components/graph/PropertiesView.tsx +14 -6
  33. lightrag_webui/src/components/graph/Settings.tsx +115 -103
  34. lightrag_webui/src/components/graph/SettingsDisplay.tsx +21 -0
  35. lightrag_webui/src/components/retrieval/QuerySettings.tsx +1 -1
  36. lightrag_webui/src/components/ui/AsyncSearch.tsx +5 -5
  37. lightrag_webui/src/components/ui/TabContent.tsx +37 -0
  38. lightrag_webui/src/components/ui/Tabs.tsx +5 -1
  39. lightrag_webui/src/components/ui/Tooltip.tsx +33 -20
  40. lightrag_webui/src/contexts/TabVisibilityProvider.tsx +53 -0
  41. lightrag_webui/src/contexts/context.ts +12 -0
  42. lightrag_webui/src/contexts/types.ts +5 -0
  43. lightrag_webui/src/contexts/useTabVisibility.ts +17 -0
  44. lightrag_webui/src/features/ApiSite.tsx +36 -1
  45. lightrag_webui/src/features/DocumentManager.tsx +20 -9
  46. lightrag_webui/src/features/GraphViewer.tsx +102 -47
  47. lightrag_webui/src/features/SiteHeader.tsx +10 -18
  48. lightrag_webui/src/hooks/useLightragGraph.tsx +143 -21
  49. lightrag_webui/src/i18n.ts +37 -0
  50. lightrag_webui/src/lib/constants.ts +3 -3
.gitattributes CHANGED
@@ -1,3 +1,4 @@
 
1
  *.png filter=lfs diff=lfs merge=lfs -text
2
  *.ttf filter=lfs diff=lfs merge=lfs -text
3
  *.ico filter=lfs diff=lfs merge=lfs -text
 
1
+ lightrag/api/webui/** -diff
2
  *.png filter=lfs diff=lfs merge=lfs -text
3
  *.ttf filter=lfs diff=lfs merge=lfs -text
4
  *.ico filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -64,3 +64,6 @@ gui/
64
 
65
  # unit-test files
66
  test_*
 
 
 
 
64
 
65
  # unit-test files
66
  test_*
67
+
68
+ # Cline files
69
+ memory-bank/
.pre-commit-config.yaml CHANGED
@@ -3,16 +3,21 @@ repos:
3
  rev: v5.0.0
4
  hooks:
5
  - id: trailing-whitespace
 
6
  - id: end-of-file-fixer
 
7
  - id: requirements-txt-fixer
 
8
 
9
 
10
  - repo: https://github.com/astral-sh/ruff-pre-commit
11
  rev: v0.6.4
12
  hooks:
13
  - id: ruff-format
 
14
  - id: ruff
15
  args: [--fix, --ignore=E402]
 
16
 
17
 
18
  - repo: https://github.com/mgedmin/check-manifest
@@ -20,3 +25,4 @@ repos:
20
  hooks:
21
  - id: check-manifest
22
  stages: [manual]
 
 
3
  rev: v5.0.0
4
  hooks:
5
  - id: trailing-whitespace
6
+ exclude: ^lightrag/api/webui/
7
  - id: end-of-file-fixer
8
+ exclude: ^lightrag/api/webui/
9
  - id: requirements-txt-fixer
10
+ exclude: ^lightrag/api/webui/
11
 
12
 
13
  - repo: https://github.com/astral-sh/ruff-pre-commit
14
  rev: v0.6.4
15
  hooks:
16
  - id: ruff-format
17
+ exclude: ^lightrag/api/webui/
18
  - id: ruff
19
  args: [--fix, --ignore=E402]
20
+ exclude: ^lightrag/api/webui/
21
 
22
 
23
  - repo: https://github.com/mgedmin/check-manifest
 
25
  hooks:
26
  - id: check-manifest
27
  stages: [manual]
28
+ exclude: ^lightrag/api/webui/
README.md CHANGED
@@ -37,28 +37,30 @@ This repository hosts the code of LightRAG. The structure of this code is based
37
  </br>
38
 
39
 
 
 
 
40
  <details>
41
  <summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
42
  🎉 News
43
  </summary>
44
 
45
-
46
- - [x] [2025.02.05]🎯📢Our team has released [VideoRAG](https://github.com/HKUDS/VideoRAG) understanding extremely long-context videos.
47
- - [x] [2025.01.13]🎯📢Our team has released [MiniRAG](https://github.com/HKUDS/MiniRAG) making RAG simpler with small models.
48
- - [x] [2025.01.06]🎯📢You can now [use PostgreSQL for Storage](#using-postgresql-for-storage).
49
- - [x] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
50
- - [x] [2024.11.25]🎯📢LightRAG now supports seamless integration of [custom knowledge graphs](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#insert-custom-kg), empowering users to enhance the system with their own domain expertise.
51
- - [x] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
52
- - [x] [2024.11.12]🎯📢LightRAG now supports [Oracle Database 23ai for all storage types (KV, vector, and graph)](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_oracle_demo.py).
53
- - [x] [2024.11.11]🎯📢LightRAG now supports [deleting entities by their names](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
54
- - [x] [2024.11.09]🎯📢Introducing the [LightRAG Gui](https://lightrag-gui.streamlit.app), which allows you to insert, query, visualize, and download LightRAG knowledge.
55
- - [x] [2024.11.04]🎯📢You can now [use Neo4J for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage).
56
- - [x] [2024.10.29]🎯📢LightRAG now supports multiple file types, including PDF, DOC, PPT, and CSV via `textract`.
57
- - [x] [2024.10.20]🎯📢We've added a new feature to LightRAG: Graph Visualization.
58
- - [x] [2024.10.18]🎯📢We've added a link to a [LightRAG Introduction Video](https://youtu.be/oageL-1I0GE). Thanks to the author!
59
- - [x] [2024.10.17]🎯📢We have created a [Discord channel](https://discord.gg/yF2MmDJyGJ)! Welcome to join for sharing and discussions! 🎉🎉
60
- - [x] [2024.10.16]🎯📢LightRAG now supports [Ollama models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
61
- - [x] [2024.10.15]🎯📢LightRAG now supports [Hugging Face models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
62
 
63
  </details>
64
 
@@ -82,16 +84,20 @@ This repository hosts the code of LightRAG. The structure of this code is based
82
  cd LightRAG
83
  pip install -e .
84
  ```
 
85
  * Install from PyPI
 
86
  ```bash
87
  pip install lightrag-hku
88
  ```
89
 
90
  ## Quick Start
 
91
  * [Video demo](https://www.youtube.com/watch?v=g21royNJ4fw) of running LightRAG locally.
92
  * All the code can be found in the `examples`.
93
  * Set OpenAI API key in environment if using OpenAI models: `export OPENAI_API_KEY="sk-...".`
94
  * Download the demo text "A Christmas Carol by Charles Dickens":
 
95
  ```bash
96
  curl https://raw.githubusercontent.com/gusye1234/nano-graphrag/main/tests/mock_data.txt > ./book.txt
97
  ```
@@ -187,6 +193,7 @@ class QueryParam:
187
  <summary> <b>Using Open AI-like APIs</b> </summary>
188
 
189
  * LightRAG also supports Open AI-like chat/embeddings APIs:
 
190
  ```python
191
  async def llm_model_func(
192
  prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
@@ -225,6 +232,7 @@ async def initialize_rag():
225
 
226
  return rag
227
  ```
 
228
  </details>
229
 
230
  <details>
@@ -252,12 +260,14 @@ rag = LightRAG(
252
  ),
253
  )
254
  ```
 
255
  </details>
256
 
257
  <details>
258
  <summary> <b>Using Ollama Models</b> </summary>
259
 
260
  ### Overview
 
261
  If you want to use Ollama models, you need to pull model you plan to use and embedding model, for example `nomic-embed-text`.
262
 
263
  Then you only need to set LightRAG as follows:
@@ -281,31 +291,37 @@ rag = LightRAG(
281
  ```
282
 
283
  ### Increasing context size
 
284
  In order for LightRAG to work context should be at least 32k tokens. By default Ollama models have context size of 8k. You can achieve this using one of two ways:
285
 
286
  #### Increasing the `num_ctx` parameter in Modelfile.
287
 
288
  1. Pull the model:
 
289
  ```bash
290
  ollama pull qwen2
291
  ```
292
 
293
  2. Display the model file:
 
294
  ```bash
295
  ollama show --modelfile qwen2 > Modelfile
296
  ```
297
 
298
  3. Edit the Modelfile by adding the following line:
 
299
  ```bash
300
  PARAMETER num_ctx 32768
301
  ```
302
 
303
  4. Create the modified model:
 
304
  ```bash
305
  ollama create -f Modelfile qwen2m
306
  ```
307
 
308
  #### Setup `num_ctx` via Ollama API.
 
309
  Tiy can use `llm_model_kwargs` param to configure ollama:
310
 
311
  ```python
@@ -325,6 +341,7 @@ rag = LightRAG(
325
  ),
326
  )
327
  ```
 
328
  #### Low RAM GPUs
329
 
330
  In order to run this experiment on low RAM GPU you should select small model and tune context window (increasing context increase memory consumption). For example, running this ollama example on repurposed mining GPU with 6Gb of RAM required to set context size to 26k while using `gemma2:2b`. It was able to find 197 entities and 19 relations on `book.txt`.
@@ -402,6 +419,7 @@ if __name__ == "__main__":
402
  ```
403
 
404
  #### For detailed documentation and examples, see:
 
405
  - [LlamaIndex Documentation](lightrag/llm/Readme.md)
406
  - [Direct OpenAI Example](examples/lightrag_llamaindex_direct_demo.py)
407
  - [LiteLLM Proxy Example](examples/lightrag_llamaindex_litellm_demo.py)
@@ -483,13 +501,16 @@ print(response_custom)
483
  We've introduced a new function `query_with_separate_keyword_extraction` to enhance the keyword extraction capabilities. This function separates the keyword extraction process from the user's prompt, focusing solely on the query to improve the relevance of extracted keywords.
484
 
485
  ##### How It Works?
 
486
  The function operates by dividing the input into two parts:
 
487
  - `User Query`
488
  - `Prompt`
489
 
490
  It then performs keyword extraction exclusively on the `user query`. This separation ensures that the extraction process is focused and relevant, unaffected by any additional language in the `prompt`. It also allows the `prompt` to serve purely for response formatting, maintaining the intent and clarity of the user's original question.
491
 
492
  ##### Usage Example
 
493
  This `example` shows how to tailor the function for educational content, focusing on detailed explanations for older students.
494
 
495
  ```python
@@ -563,6 +584,7 @@ custom_kg = {
563
 
564
  rag.insert_custom_kg(custom_kg)
565
  ```
 
566
  </details>
567
 
568
  ## Insert
@@ -593,6 +615,7 @@ rag.insert(["TEXT1", "TEXT2", "TEXT3", ...]) # Documents will be processed in b
593
  ```
594
 
595
  The `insert_batch_size` parameter in `addon_params` controls how many documents are processed in each batch during insertion. This is useful for:
 
596
  - Managing memory usage with large document collections
597
  - Optimizing processing speed
598
  - Providing better progress tracking
@@ -647,6 +670,7 @@ text_content = textract.process(file_path)
647
 
648
  rag.insert(text_content.decode('utf-8'))
649
  ```
 
650
  </details>
651
 
652
  ## Storage
@@ -685,6 +709,7 @@ async def initialize_rag():
685
 
686
  return rag
687
  ```
 
688
  see test_neo4j.py for a working example.
689
 
690
  </details>
@@ -693,6 +718,7 @@ see test_neo4j.py for a working example.
693
  <summary> <b>Using PostgreSQL for Storage</b> </summary>
694
 
695
  For production level scenarios you will most likely want to leverage an enterprise solution. PostgreSQL can provide a one-stop solution for you as KV store, VectorDB (pgvector) and GraphDB (apache AGE).
 
696
  * PostgreSQL is lightweight,the whole binary distribution including all necessary plugins can be zipped to 40MB: Ref to [Windows Release](https://github.com/ShanGor/apache-age-windows/releases/tag/PG17%2Fv1.5.0-rc0) as it is easy to install for Linux/Mac.
697
  * If you prefer docker, please start with this image if you are a beginner to avoid hiccups (DO read the overview): https://hub.docker.com/r/shangor/postgres-for-rag
698
  * How to start? Ref to: [examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
@@ -735,6 +761,7 @@ For production level scenarios you will most likely want to leverage an enterpri
735
  > It is a known issue of the release version: https://github.com/apache/age/pull/1721
736
  >
737
  > You can Compile the AGE from source code and fix it.
 
738
 
739
  </details>
740
 
@@ -742,9 +769,11 @@ For production level scenarios you will most likely want to leverage an enterpri
742
  <summary> <b>Using Faiss for Storage</b> </summary>
743
 
744
  - Install the required dependencies:
 
745
  ```
746
  pip install faiss-cpu
747
  ```
 
748
  You can also install `faiss-gpu` if you have GPU support.
749
 
750
  - Here we are using `sentence-transformers` but you can also use `OpenAIEmbedding` model with `3072` dimensions.
@@ -810,6 +839,7 @@ relation = rag.create_relation("Google", "Gmail", {
810
  "weight": 2.0
811
  })
812
  ```
 
813
  </details>
814
 
815
  <details>
@@ -835,6 +865,7 @@ updated_relation = rag.edit_relation("Google", "Google Mail", {
835
  "weight": 3.0
836
  })
837
  ```
 
838
  </details>
839
 
840
  All operations are available in both synchronous and asynchronous versions. The asynchronous versions have the prefix "a" (e.g., `acreate_entity`, `aedit_relation`).
@@ -851,6 +882,55 @@ All operations are available in both synchronous and asynchronous versions. The
851
 
852
  These operations maintain data consistency across both the graph database and vector database components, ensuring your knowledge graph remains coherent.
853
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
854
  ## Entity Merging
855
 
856
  <details>
@@ -913,6 +993,7 @@ rag.merge_entities(
913
  ```
914
 
915
  When merging entities:
 
916
  * All relationships from source entities are redirected to the target entity
917
  * Duplicate relationships are intelligently merged
918
  * Self-relationships (loops) are prevented
@@ -946,6 +1027,7 @@ rag.clear_cache(modes=["local"])
946
  ```
947
 
948
  Valid modes are:
 
949
  - `"default"`: Extraction cache
950
  - `"naive"`: Naive search cache
951
  - `"local"`: Local search cache
@@ -960,33 +1042,33 @@ Valid modes are:
960
  <details>
961
  <summary> Parameters </summary>
962
 
963
- | **Parameter** | **Type** | **Explanation** | **Default** |
964
- |----------------------------------------------| --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
965
- | **working\_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` |
966
- | **kv\_storage** | `str` | Storage type for documents and text chunks. Supported types: `JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` |
967
- | **vector\_storage** | `str` | Storage type for embedding vectors. Supported types: `NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` |
968
- | **graph\_storage** | `str` | Storage type for graph edges and nodes. Supported types: `NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` |
969
- | **chunk\_token\_size** | `int` | Maximum token size per chunk when splitting documents | `1200` |
970
- | **chunk\_overlap\_token\_size** | `int` | Overlap token size between two chunks when splitting documents | `100` |
971
- | **tiktoken\_model\_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` |
972
- | **entity\_extract\_max\_gleaning** | `int` | Number of loops in the entity extraction process, appending history messages | `1` |
973
- | **entity\_summary\_to\_max\_tokens** | `int` | Maximum token size for each entity summary | `500` |
974
- | **node\_embedding\_algorithm** | `str` | Algorithm for node embedding (currently not used) | `node2vec` |
975
- | **node2vec\_params** | `dict` | Parameters for node embedding | `{"dimensions": 1536,"num_walks": 10,"walk_length": 40,"window_size": 2,"iterations": 3,"random_seed": 3,}` |
976
- | **embedding\_func** | `EmbeddingFunc` | Function to generate embedding vectors from text | `openai_embed` |
977
- | **embedding\_batch\_num** | `int` | Maximum batch size for embedding processes (multiple texts sent per batch) | `32` |
978
- | **embedding\_func\_max\_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
979
- | **llm\_model\_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
980
- | **llm\_model\_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
981
- | **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) |
982
- | **llm\_model\_max\_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `16`(default value changed by env var MAX_ASYNC) |
983
- | **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
984
- | **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.2(default value changed by env var COSINE_THRESHOLD) |
985
- | **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
986
- | **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` |
987
- | **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` |
988
- | **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
989
- | **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}` |
990
 
991
  </details>
992
 
@@ -996,12 +1078,15 @@ Valid modes are:
996
  <summary>Click to view error handling details</summary>
997
 
998
  The API includes comprehensive error handling:
 
999
  - File not found errors (404)
1000
  - Processing errors (500)
1001
  - Supports multiple file encodings (UTF-8 and GBK)
 
1002
  </details>
1003
 
1004
  ## API
 
1005
  LightRag can be installed with API support to serve a Fast api interface to perform data upload and indexing/Rag operations/Rescan of the input folder etc..
1006
 
1007
  [LightRag API](lightrag/api/README.md)
@@ -1035,7 +1120,6 @@ net.show('knowledge_graph.html')
1035
  <details>
1036
  <summary> <b>Graph visualization with Neo4</b> </summary>
1037
 
1038
-
1039
  * The following code can be found in `examples/graph_visual_with_neo4j.py`
1040
 
1041
  ```python
@@ -1171,10 +1255,13 @@ LightRag can be installed with Tools support to add extra tools like the graphml
1171
  </details>
1172
 
1173
  ## Evaluation
 
1174
  ### Dataset
 
1175
  The dataset used in LightRAG can be downloaded from [TommyChien/UltraDomain](https://huggingface.co/datasets/TommyChien/UltraDomain).
1176
 
1177
  ### Generate Query
 
1178
  LightRAG uses the following prompt to generate high-level queries, with the corresponding code in `example/generate_query.py`.
1179
 
1180
  <details>
@@ -1203,9 +1290,11 @@ Output the results in the following structure:
1203
  - User 5: [user description]
1204
  ...
1205
  ```
 
1206
  </details>
1207
 
1208
  ### Batch Eval
 
1209
  To evaluate the performance of two RAG systems on high-level queries, LightRAG uses the following prompt, with the specific code available in `example/batch_eval.py`.
1210
 
1211
  <details>
@@ -1253,37 +1342,40 @@ Output your evaluation in the following JSON format:
1253
  }}
1254
  }}
1255
  ```
 
1256
  </details>
1257
 
1258
  ### Overall Performance Table
1259
 
1260
- | | **Agriculture** | | **CS** | | **Legal** | | **Mix** | |
1261
- |----------------------|-------------------------|-----------------------|-----------------------|-----------------------|-----------------------|-----------------------|-----------------------|-----------------------|
1262
- | | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** |
1263
- | **Comprehensiveness** | 32.4% | **67.6%** | 38.4% | **61.6%** | 16.4% | **83.6%** | 38.8% | **61.2%** |
1264
- | **Diversity** | 23.6% | **76.4%** | 38.0% | **62.0%** | 13.6% | **86.4%** | 32.4% | **67.6%** |
1265
- | **Empowerment** | 32.4% | **67.6%** | 38.8% | **61.2%** | 16.4% | **83.6%** | 42.8% | **57.2%** |
1266
- | **Overall** | 32.4% | **67.6%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 40.0% | **60.0%** |
1267
- | | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** |
1268
- | **Comprehensiveness** | 31.6% | **68.4%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 39.2% | **60.8%** |
1269
- | **Diversity** | 29.2% | **70.8%** | 39.2% | **60.8%** | 11.6% | **88.4%** | 30.8% | **69.2%** |
1270
- | **Empowerment** | 31.6% | **68.4%** | 36.4% | **63.6%** | 15.2% | **84.8%** | 42.4% | **57.6%** |
1271
- | **Overall** | 32.4% | **67.6%** | 38.0% | **62.0%** | 14.4% | **85.6%** | 40.0% | **60.0%** |
1272
- | | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** |
1273
- | **Comprehensiveness** | 26.0% | **74.0%** | 41.6% | **58.4%** | 26.8% | **73.2%** | 40.4% | **59.6%** |
1274
- | **Diversity** | 24.0% | **76.0%** | 38.8% | **61.2%** | 20.0% | **80.0%** | 32.4% | **67.6%** |
1275
- | **Empowerment** | 25.2% | **74.8%** | 40.8% | **59.2%** | 26.0% | **74.0%** | 46.0% | **54.0%** |
1276
- | **Overall** | 24.8% | **75.2%** | 41.6% | **58.4%** | 26.4% | **73.6%** | 42.4% | **57.6%** |
1277
- | | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** |
1278
- | **Comprehensiveness** | 45.6% | **54.4%** | 48.4% | **51.6%** | 48.4% | **51.6%** | **50.4%** | 49.6% |
1279
- | **Diversity** | 22.8% | **77.2%** | 40.8% | **59.2%** | 26.4% | **73.6%** | 36.0% | **64.0%** |
1280
- | **Empowerment** | 41.2% | **58.8%** | 45.2% | **54.8%** | 43.6% | **56.4%** | **50.8%** | 49.2% |
1281
- | **Overall** | 45.2% | **54.8%** | 48.0% | **52.0%** | 47.2% | **52.8%** | **50.4%** | 49.6% |
1282
 
1283
  ## Reproduce
 
1284
  All the code can be found in the `./reproduce` directory.
1285
 
1286
  ### Step-0 Extract Unique Contexts
 
1287
  First, we need to extract unique contexts in the datasets.
1288
 
1289
  <details>
@@ -1340,9 +1432,11 @@ def extract_unique_contexts(input_directory, output_directory):
1340
  print("All files have been processed.")
1341
 
1342
  ```
 
1343
  </details>
1344
 
1345
  ### Step-1 Insert Contexts
 
1346
  For the extracted contexts, we insert them into the LightRAG system.
1347
 
1348
  <details>
@@ -1366,6 +1460,7 @@ def insert_text(rag, file_path):
1366
  if retries == max_retries:
1367
  print("Insertion failed after exceeding the maximum number of retries")
1368
  ```
 
1369
  </details>
1370
 
1371
  ### Step-2 Generate Queries
@@ -1390,9 +1485,11 @@ def get_summary(context, tot_tokens=2000):
1390
 
1391
  return summary
1392
  ```
 
1393
  </details>
1394
 
1395
  ### Step-3 Query
 
1396
  For the queries generated in Step-2, we will extract them and query LightRAG.
1397
 
1398
  <details>
@@ -1409,6 +1506,7 @@ def extract_queries(file_path):
1409
 
1410
  return queries
1411
  ```
 
1412
  </details>
1413
 
1414
  ## Star History
@@ -1441,4 +1539,5 @@ archivePrefix={arXiv},
1441
  primaryClass={cs.IR}
1442
  }
1443
  ```
 
1444
  **Thank you for your interest in our work!**
 
37
  </br>
38
 
39
 
40
+
41
+
42
+
43
  <details>
44
  <summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
45
  🎉 News
46
  </summary>
47
 
48
+ - [X] [2025.02.05]🎯📢Our team has released [VideoRAG](https://github.com/HKUDS/VideoRAG) understanding extremely long-context videos.
49
+ - [X] [2025.01.13]🎯📢Our team has released [MiniRAG](https://github.com/HKUDS/MiniRAG) making RAG simpler with small models.
50
+ - [X] [2025.01.06]🎯📢You can now [use PostgreSQL for Storage](#using-postgresql-for-storage).
51
+ - [X] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
52
+ - [X] [2024.11.25]🎯📢LightRAG now supports seamless integration of [custom knowledge graphs](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#insert-custom-kg), empowering users to enhance the system with their own domain expertise.
53
+ - [X] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
54
+ - [X] [2024.11.12]🎯📢LightRAG now supports [Oracle Database 23ai for all storage types (KV, vector, and graph)](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_oracle_demo.py).
55
+ - [X] [2024.11.11]🎯📢LightRAG now supports [deleting entities by their names](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
56
+ - [X] [2024.11.09]🎯📢Introducing the [LightRAG Gui](https://lightrag-gui.streamlit.app), which allows you to insert, query, visualize, and download LightRAG knowledge.
57
+ - [X] [2024.11.04]🎯📢You can now [use Neo4J for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage).
58
+ - [X] [2024.10.29]🎯📢LightRAG now supports multiple file types, including PDF, DOC, PPT, and CSV via `textract`.
59
+ - [X] [2024.10.20]🎯📢We've added a new feature to LightRAG: Graph Visualization.
60
+ - [X] [2024.10.18]🎯📢We've added a link to a [LightRAG Introduction Video](https://youtu.be/oageL-1I0GE). Thanks to the author!
61
+ - [X] [2024.10.17]🎯📢We have created a [Discord channel](https://discord.gg/yF2MmDJyGJ)! Welcome to join for sharing and discussions! 🎉🎉
62
+ - [X] [2024.10.16]🎯📢LightRAG now supports [Ollama models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
63
+ - [X] [2024.10.15]🎯📢LightRAG now supports [Hugging Face models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
 
64
 
65
  </details>
66
 
 
84
  cd LightRAG
85
  pip install -e .
86
  ```
87
+
88
  * Install from PyPI
89
+
90
  ```bash
91
  pip install lightrag-hku
92
  ```
93
 
94
  ## Quick Start
95
+
96
  * [Video demo](https://www.youtube.com/watch?v=g21royNJ4fw) of running LightRAG locally.
97
  * All the code can be found in the `examples`.
98
  * Set OpenAI API key in environment if using OpenAI models: `export OPENAI_API_KEY="sk-...".`
99
  * Download the demo text "A Christmas Carol by Charles Dickens":
100
+
101
  ```bash
102
  curl https://raw.githubusercontent.com/gusye1234/nano-graphrag/main/tests/mock_data.txt > ./book.txt
103
  ```
 
193
  <summary> <b>Using Open AI-like APIs</b> </summary>
194
 
195
  * LightRAG also supports Open AI-like chat/embeddings APIs:
196
+
197
  ```python
198
  async def llm_model_func(
199
  prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
 
232
 
233
  return rag
234
  ```
235
+
236
  </details>
237
 
238
  <details>
 
260
  ),
261
  )
262
  ```
263
+
264
  </details>
265
 
266
  <details>
267
  <summary> <b>Using Ollama Models</b> </summary>
268
 
269
  ### Overview
270
+
271
  If you want to use Ollama models, you need to pull model you plan to use and embedding model, for example `nomic-embed-text`.
272
 
273
  Then you only need to set LightRAG as follows:
 
291
  ```
292
 
293
  ### Increasing context size
294
+
295
  In order for LightRAG to work context should be at least 32k tokens. By default Ollama models have context size of 8k. You can achieve this using one of two ways:
296
 
297
  #### Increasing the `num_ctx` parameter in Modelfile.
298
 
299
  1. Pull the model:
300
+
301
  ```bash
302
  ollama pull qwen2
303
  ```
304
 
305
  2. Display the model file:
306
+
307
  ```bash
308
  ollama show --modelfile qwen2 > Modelfile
309
  ```
310
 
311
  3. Edit the Modelfile by adding the following line:
312
+
313
  ```bash
314
  PARAMETER num_ctx 32768
315
  ```
316
 
317
  4. Create the modified model:
318
+
319
  ```bash
320
  ollama create -f Modelfile qwen2m
321
  ```
322
 
323
  #### Setup `num_ctx` via Ollama API.
324
+
325
  Tiy can use `llm_model_kwargs` param to configure ollama:
326
 
327
  ```python
 
341
  ),
342
  )
343
  ```
344
+
345
  #### Low RAM GPUs
346
 
347
  In order to run this experiment on low RAM GPU you should select small model and tune context window (increasing context increase memory consumption). For example, running this ollama example on repurposed mining GPU with 6Gb of RAM required to set context size to 26k while using `gemma2:2b`. It was able to find 197 entities and 19 relations on `book.txt`.
 
419
  ```
420
 
421
  #### For detailed documentation and examples, see:
422
+
423
  - [LlamaIndex Documentation](lightrag/llm/Readme.md)
424
  - [Direct OpenAI Example](examples/lightrag_llamaindex_direct_demo.py)
425
  - [LiteLLM Proxy Example](examples/lightrag_llamaindex_litellm_demo.py)
 
501
  We've introduced a new function `query_with_separate_keyword_extraction` to enhance the keyword extraction capabilities. This function separates the keyword extraction process from the user's prompt, focusing solely on the query to improve the relevance of extracted keywords.
502
 
503
  ##### How It Works?
504
+
505
  The function operates by dividing the input into two parts:
506
+
507
  - `User Query`
508
  - `Prompt`
509
 
510
  It then performs keyword extraction exclusively on the `user query`. This separation ensures that the extraction process is focused and relevant, unaffected by any additional language in the `prompt`. It also allows the `prompt` to serve purely for response formatting, maintaining the intent and clarity of the user's original question.
511
 
512
  ##### Usage Example
513
+
514
  This `example` shows how to tailor the function for educational content, focusing on detailed explanations for older students.
515
 
516
  ```python
 
584
 
585
  rag.insert_custom_kg(custom_kg)
586
  ```
587
+
588
  </details>
589
 
590
  ## Insert
 
615
  ```
616
 
617
  The `insert_batch_size` parameter in `addon_params` controls how many documents are processed in each batch during insertion. This is useful for:
618
+
619
  - Managing memory usage with large document collections
620
  - Optimizing processing speed
621
  - Providing better progress tracking
 
670
 
671
  rag.insert(text_content.decode('utf-8'))
672
  ```
673
+
674
  </details>
675
 
676
  ## Storage
 
709
 
710
  return rag
711
  ```
712
+
713
  see test_neo4j.py for a working example.
714
 
715
  </details>
 
718
  <summary> <b>Using PostgreSQL for Storage</b> </summary>
719
 
720
  For production level scenarios you will most likely want to leverage an enterprise solution. PostgreSQL can provide a one-stop solution for you as KV store, VectorDB (pgvector) and GraphDB (apache AGE).
721
+
722
  * PostgreSQL is lightweight,the whole binary distribution including all necessary plugins can be zipped to 40MB: Ref to [Windows Release](https://github.com/ShanGor/apache-age-windows/releases/tag/PG17%2Fv1.5.0-rc0) as it is easy to install for Linux/Mac.
723
  * If you prefer docker, please start with this image if you are a beginner to avoid hiccups (DO read the overview): https://hub.docker.com/r/shangor/postgres-for-rag
724
  * How to start? Ref to: [examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
 
761
  > It is a known issue of the release version: https://github.com/apache/age/pull/1721
762
  >
763
  > You can Compile the AGE from source code and fix it.
764
+ >
765
 
766
  </details>
767
 
 
769
  <summary> <b>Using Faiss for Storage</b> </summary>
770
 
771
  - Install the required dependencies:
772
+
773
  ```
774
  pip install faiss-cpu
775
  ```
776
+
777
  You can also install `faiss-gpu` if you have GPU support.
778
 
779
  - Here we are using `sentence-transformers` but you can also use `OpenAIEmbedding` model with `3072` dimensions.
 
839
  "weight": 2.0
840
  })
841
  ```
842
+
843
  </details>
844
 
845
  <details>
 
865
  "weight": 3.0
866
  })
867
  ```
868
+
869
  </details>
870
 
871
  All operations are available in both synchronous and asynchronous versions. The asynchronous versions have the prefix "a" (e.g., `acreate_entity`, `aedit_relation`).
 
882
 
883
  These operations maintain data consistency across both the graph database and vector database components, ensuring your knowledge graph remains coherent.
884
 
885
+ ## Data Export Functions
886
+
887
+ ## Overview
888
+
889
+ LightRAG allows you to export your knowledge graph data in various formats for analysis, sharing, and backup purposes. The system supports exporting entities, relations, and relationship data.
890
+
891
+ ## Export Functions
892
+
893
+ ### Basic Usage
894
+
895
+ ```python
896
+ # Basic CSV export (default format)
897
+ rag.export_data("knowledge_graph.csv")
898
+
899
+ # Specify any format
900
+ rag.export_data("output.xlsx", file_format="excel")
901
+ ```
902
+
903
+ ### Different File Formats supported
904
+
905
+ ```python
906
+ #Export data in CSV format
907
+ rag.export_data("graph_data.csv", file_format="csv")
908
+
909
+ # Export data in Excel sheet
910
+ rag.export_data("graph_data.xlsx", file_format="excel")
911
+
912
+ # Export data in markdown format
913
+ rag.export_data("graph_data.md", file_format="md")
914
+
915
+ # Export data in Text
916
+ rag.export_data("graph_data.txt", file_format="txt")
917
+ ```
918
+ ## Additional Options
919
+
920
+ Include vector embeddings in the export (optional):
921
+
922
+ ```python
923
+ rag.export_data("complete_data.csv", include_vector_data=True)
924
+ ```
925
+ ## Data Included in Export
926
+
927
+ All exports include:
928
+
929
+ * Entity information (names, IDs, metadata)
930
+ * Relation data (connections between entities)
931
+ * Relationship information from vector database
932
+
933
+
934
  ## Entity Merging
935
 
936
  <details>
 
993
  ```
994
 
995
  When merging entities:
996
+
997
  * All relationships from source entities are redirected to the target entity
998
  * Duplicate relationships are intelligently merged
999
  * Self-relationships (loops) are prevented
 
1027
  ```
1028
 
1029
  Valid modes are:
1030
+
1031
  - `"default"`: Extraction cache
1032
  - `"naive"`: Naive search cache
1033
  - `"local"`: Local search cache
 
1042
  <details>
1043
  <summary> Parameters </summary>
1044
 
1045
+ | **Parameter** | **Type** | **Explanation** | **Default** |
1046
+ | -------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
1047
+ | **working\_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` |
1048
+ | **kv\_storage** | `str` | Storage type for documents and text chunks. Supported types:`JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` |
1049
+ | **vector\_storage** | `str` | Storage type for embedding vectors. Supported types:`NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` |
1050
+ | **graph\_storage** | `str` | Storage type for graph edges and nodes. Supported types:`NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` |
1051
+ | **chunk\_token\_size** | `int` | Maximum token size per chunk when splitting documents | `1200` |
1052
+ | **chunk\_overlap\_token\_size** | `int` | Overlap token size between two chunks when splitting documents | `100` |
1053
+ | **tiktoken\_model\_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` |
1054
+ | **entity\_extract\_max\_gleaning** | `int` | Number of loops in the entity extraction process, appending history messages | `1` |
1055
+ | **entity\_summary\_to\_max\_tokens** | `int` | Maximum token size for each entity summary | `500` |
1056
+ | **node\_embedding\_algorithm** | `str` | Algorithm for node embedding (currently not used) | `node2vec` |
1057
+ | **node2vec\_params** | `dict` | Parameters for node embedding | `{"dimensions": 1536,"num_walks": 10,"walk_length": 40,"window_size": 2,"iterations": 3,"random_seed": 3,}` |
1058
+ | **embedding\_func** | `EmbeddingFunc` | Function to generate embedding vectors from text | `openai_embed` |
1059
+ | **embedding\_batch\_num** | `int` | Maximum batch size for embedding processes (multiple texts sent per batch) | `32` |
1060
+ | **embedding\_func\_max\_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
1061
+ | **llm\_model\_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
1062
+ | **llm\_model\_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
1063
+ | **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) |
1064
+ | **llm\_model\_max\_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `16`(default value changed by env var MAX_ASYNC) |
1065
+ | **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
1066
+ | **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.2(default value changed by env var COSINE_THRESHOLD) |
1067
+ | **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
1068
+ | **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` |
1069
+ | **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` |
1070
+ | **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
1071
+ | **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}` |
1072
 
1073
  </details>
1074
 
 
1078
  <summary>Click to view error handling details</summary>
1079
 
1080
  The API includes comprehensive error handling:
1081
+
1082
  - File not found errors (404)
1083
  - Processing errors (500)
1084
  - Supports multiple file encodings (UTF-8 and GBK)
1085
+
1086
  </details>
1087
 
1088
  ## API
1089
+
1090
  LightRag can be installed with API support to serve a Fast api interface to perform data upload and indexing/Rag operations/Rescan of the input folder etc..
1091
 
1092
  [LightRag API](lightrag/api/README.md)
 
1120
  <details>
1121
  <summary> <b>Graph visualization with Neo4</b> </summary>
1122
 
 
1123
  * The following code can be found in `examples/graph_visual_with_neo4j.py`
1124
 
1125
  ```python
 
1255
  </details>
1256
 
1257
  ## Evaluation
1258
+
1259
  ### Dataset
1260
+
1261
  The dataset used in LightRAG can be downloaded from [TommyChien/UltraDomain](https://huggingface.co/datasets/TommyChien/UltraDomain).
1262
 
1263
  ### Generate Query
1264
+
1265
  LightRAG uses the following prompt to generate high-level queries, with the corresponding code in `example/generate_query.py`.
1266
 
1267
  <details>
 
1290
  - User 5: [user description]
1291
  ...
1292
  ```
1293
+
1294
  </details>
1295
 
1296
  ### Batch Eval
1297
+
1298
  To evaluate the performance of two RAG systems on high-level queries, LightRAG uses the following prompt, with the specific code available in `example/batch_eval.py`.
1299
 
1300
  <details>
 
1342
  }}
1343
  }}
1344
  ```
1345
+
1346
  </details>
1347
 
1348
  ### Overall Performance Table
1349
 
1350
+ | | **Agriculture** | | **CS** | | **Legal** | | **Mix** | |
1351
+ | --------------------------- | --------------------- | ------------------ | ------------ | ------------------ | --------------- | ------------------ | --------------- | ------------------ |
1352
+ | | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** |
1353
+ | **Comprehensiveness** | 32.4% | **67.6%** | 38.4% | **61.6%** | 16.4% | **83.6%** | 38.8% | **61.2%** |
1354
+ | **Diversity** | 23.6% | **76.4%** | 38.0% | **62.0%** | 13.6% | **86.4%** | 32.4% | **67.6%** |
1355
+ | **Empowerment** | 32.4% | **67.6%** | 38.8% | **61.2%** | 16.4% | **83.6%** | 42.8% | **57.2%** |
1356
+ | **Overall** | 32.4% | **67.6%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 40.0% | **60.0%** |
1357
+ | | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** |
1358
+ | **Comprehensiveness** | 31.6% | **68.4%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 39.2% | **60.8%** |
1359
+ | **Diversity** | 29.2% | **70.8%** | 39.2% | **60.8%** | 11.6% | **88.4%** | 30.8% | **69.2%** |
1360
+ | **Empowerment** | 31.6% | **68.4%** | 36.4% | **63.6%** | 15.2% | **84.8%** | 42.4% | **57.6%** |
1361
+ | **Overall** | 32.4% | **67.6%** | 38.0% | **62.0%** | 14.4% | **85.6%** | 40.0% | **60.0%** |
1362
+ | | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** |
1363
+ | **Comprehensiveness** | 26.0% | **74.0%** | 41.6% | **58.4%** | 26.8% | **73.2%** | 40.4% | **59.6%** |
1364
+ | **Diversity** | 24.0% | **76.0%** | 38.8% | **61.2%** | 20.0% | **80.0%** | 32.4% | **67.6%** |
1365
+ | **Empowerment** | 25.2% | **74.8%** | 40.8% | **59.2%** | 26.0% | **74.0%** | 46.0% | **54.0%** |
1366
+ | **Overall** | 24.8% | **75.2%** | 41.6% | **58.4%** | 26.4% | **73.6%** | 42.4% | **57.6%** |
1367
+ | | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** |
1368
+ | **Comprehensiveness** | 45.6% | **54.4%** | 48.4% | **51.6%** | 48.4% | **51.6%** | **50.4%** | 49.6% |
1369
+ | **Diversity** | 22.8% | **77.2%** | 40.8% | **59.2%** | 26.4% | **73.6%** | 36.0% | **64.0%** |
1370
+ | **Empowerment** | 41.2% | **58.8%** | 45.2% | **54.8%** | 43.6% | **56.4%** | **50.8%** | 49.2% |
1371
+ | **Overall** | 45.2% | **54.8%** | 48.0% | **52.0%** | 47.2% | **52.8%** | **50.4%** | 49.6% |
1372
 
1373
  ## Reproduce
1374
+
1375
  All the code can be found in the `./reproduce` directory.
1376
 
1377
  ### Step-0 Extract Unique Contexts
1378
+
1379
  First, we need to extract unique contexts in the datasets.
1380
 
1381
  <details>
 
1432
  print("All files have been processed.")
1433
 
1434
  ```
1435
+
1436
  </details>
1437
 
1438
  ### Step-1 Insert Contexts
1439
+
1440
  For the extracted contexts, we insert them into the LightRAG system.
1441
 
1442
  <details>
 
1460
  if retries == max_retries:
1461
  print("Insertion failed after exceeding the maximum number of retries")
1462
  ```
1463
+
1464
  </details>
1465
 
1466
  ### Step-2 Generate Queries
 
1485
 
1486
  return summary
1487
  ```
1488
+
1489
  </details>
1490
 
1491
  ### Step-3 Query
1492
+
1493
  For the queries generated in Step-2, we will extract them and query LightRAG.
1494
 
1495
  <details>
 
1506
 
1507
  return queries
1508
  ```
1509
+
1510
  </details>
1511
 
1512
  ## Star History
 
1539
  primaryClass={cs.IR}
1540
  }
1541
  ```
1542
+
1543
  **Thank you for your interest in our work!**
lightrag/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
2
 
3
- __version__ = "1.2.5"
4
  __author__ = "Zirui Guo"
5
  __url__ = "https://github.com/HKUDS/LightRAG"
 
1
  from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
2
 
3
+ __version__ = "1.2.6"
4
  __author__ = "Zirui Guo"
5
  __url__ = "https://github.com/HKUDS/LightRAG"
lightrag/api/gunicorn_config.py CHANGED
@@ -59,7 +59,7 @@ logconfig_dict = {
59
  },
60
  "filters": {
61
  "path_filter": {
62
- "()": "lightrag.api.lightrag_server.LightragPathFilter",
63
  },
64
  },
65
  "loggers": {
 
59
  },
60
  "filters": {
61
  "path_filter": {
62
+ "()": "lightrag.utils.LightragPathFilter",
63
  },
64
  },
65
  "loggers": {
lightrag/api/lightrag_server.py CHANGED
@@ -55,41 +55,6 @@ config = configparser.ConfigParser()
55
  config.read("config.ini")
56
 
57
 
58
- class LightragPathFilter(logging.Filter):
59
- """Filter for lightrag logger to filter out frequent path access logs"""
60
-
61
- def __init__(self):
62
- super().__init__()
63
- # Define paths to be filtered
64
- self.filtered_paths = ["/documents", "/health", "/webui/"]
65
-
66
- def filter(self, record):
67
- try:
68
- # Check if record has the required attributes for an access log
69
- if not hasattr(record, "args") or not isinstance(record.args, tuple):
70
- return True
71
- if len(record.args) < 5:
72
- return True
73
-
74
- # Extract method, path and status from the record args
75
- method = record.args[1]
76
- path = record.args[2]
77
- status = record.args[4]
78
-
79
- # Filter out successful GET requests to filtered paths
80
- if (
81
- method == "GET"
82
- and (status == 200 or status == 304)
83
- and path in self.filtered_paths
84
- ):
85
- return False
86
-
87
- return True
88
- except Exception:
89
- # In case of any error, let the message through
90
- return True
91
-
92
-
93
  def create_app(args):
94
  # Setup logging
95
  logger.setLevel(args.log_level)
@@ -177,6 +142,9 @@ def create_app(args):
177
  if api_key
178
  else "",
179
  version=__api_version__,
 
 
 
180
  openapi_tags=[{"name": "api"}],
181
  lifespan=lifespan,
182
  )
@@ -423,12 +391,24 @@ def create_app(args):
423
  "update_status": update_status,
424
  }
425
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  # Webui mount webui/index.html
427
  static_dir = Path(__file__).parent / "webui"
428
  static_dir.mkdir(exist_ok=True)
429
  app.mount(
430
  "/webui",
431
- StaticFiles(directory=static_dir, html=True, check_dir=True),
432
  name="webui",
433
  )
434
 
@@ -516,7 +496,7 @@ def configure_logging():
516
  },
517
  "filters": {
518
  "path_filter": {
519
- "()": "lightrag.api.lightrag_server.LightragPathFilter",
520
  },
521
  },
522
  }
 
55
  config.read("config.ini")
56
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  def create_app(args):
59
  # Setup logging
60
  logger.setLevel(args.log_level)
 
142
  if api_key
143
  else "",
144
  version=__api_version__,
145
+ openapi_url="/openapi.json", # Explicitly set OpenAPI schema URL
146
+ docs_url="/docs", # Explicitly set docs URL
147
+ redoc_url="/redoc", # Explicitly set redoc URL
148
  openapi_tags=[{"name": "api"}],
149
  lifespan=lifespan,
150
  )
 
391
  "update_status": update_status,
392
  }
393
 
394
+ # Custom StaticFiles class to prevent caching of HTML files
395
+ class NoCacheStaticFiles(StaticFiles):
396
+ async def get_response(self, path: str, scope):
397
+ response = await super().get_response(path, scope)
398
+ if path.endswith(".html"):
399
+ response.headers["Cache-Control"] = (
400
+ "no-cache, no-store, must-revalidate"
401
+ )
402
+ response.headers["Pragma"] = "no-cache"
403
+ response.headers["Expires"] = "0"
404
+ return response
405
+
406
  # Webui mount webui/index.html
407
  static_dir = Path(__file__).parent / "webui"
408
  static_dir.mkdir(exist_ok=True)
409
  app.mount(
410
  "/webui",
411
+ NoCacheStaticFiles(directory=static_dir, html=True, check_dir=True),
412
  name="webui",
413
  )
414
 
 
496
  },
497
  "filters": {
498
  "path_filter": {
499
+ "()": "lightrag.utils.LightragPathFilter",
500
  },
501
  },
502
  }
lightrag/api/routers/document_routes.py CHANGED
@@ -99,6 +99,37 @@ class DocsStatusesResponse(BaseModel):
99
  statuses: Dict[DocStatus, List[DocStatusResponse]] = {}
100
 
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  class DocumentManager:
103
  def __init__(
104
  self,
@@ -247,7 +278,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
247
  if global_args["main_args"].document_loading_engine == "DOCLING":
248
  if not pm.is_installed("docling"): # type: ignore
249
  pm.install("docling")
250
- from docling.document_converter import DocumentConverter
251
 
252
  converter = DocumentConverter()
253
  result = converter.convert(file_path)
@@ -266,7 +297,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
266
  if global_args["main_args"].document_loading_engine == "DOCLING":
267
  if not pm.is_installed("docling"): # type: ignore
268
  pm.install("docling")
269
- from docling.document_converter import DocumentConverter
270
 
271
  converter = DocumentConverter()
272
  result = converter.convert(file_path)
@@ -286,7 +317,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
286
  if global_args["main_args"].document_loading_engine == "DOCLING":
287
  if not pm.is_installed("docling"): # type: ignore
288
  pm.install("docling")
289
- from docling.document_converter import DocumentConverter
290
 
291
  converter = DocumentConverter()
292
  result = converter.convert(file_path)
@@ -307,7 +338,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
307
  if global_args["main_args"].document_loading_engine == "DOCLING":
308
  if not pm.is_installed("docling"): # type: ignore
309
  pm.install("docling")
310
- from docling.document_converter import DocumentConverter
311
 
312
  converter = DocumentConverter()
313
  result = converter.convert(file_path)
@@ -718,17 +749,33 @@ def create_document_routes(
718
  logger.error(traceback.format_exc())
719
  raise HTTPException(status_code=500, detail=str(e))
720
 
721
- @router.get("/pipeline_status", dependencies=[Depends(optional_api_key)])
722
- async def get_pipeline_status():
 
 
 
 
723
  """
724
  Get the current status of the document indexing pipeline.
725
 
726
  This endpoint returns information about the current state of the document processing pipeline,
727
- including whether it's busy, the current job name, when it started, how many documents
728
- are being processed, how many batches there are, and which batch is currently being processed.
729
 
730
  Returns:
731
- dict: A dictionary containing the pipeline status information
 
 
 
 
 
 
 
 
 
 
 
 
 
732
  """
733
  try:
734
  from lightrag.kg.shared_storage import get_namespace_data
@@ -746,7 +793,7 @@ def create_document_routes(
746
  if status_dict.get("job_start"):
747
  status_dict["job_start"] = str(status_dict["job_start"])
748
 
749
- return status_dict
750
  except Exception as e:
751
  logger.error(f"Error getting pipeline status: {str(e)}")
752
  logger.error(traceback.format_exc())
 
99
  statuses: Dict[DocStatus, List[DocStatusResponse]] = {}
100
 
101
 
102
+ class PipelineStatusResponse(BaseModel):
103
+ """Response model for pipeline status
104
+
105
+ Attributes:
106
+ autoscanned: Whether auto-scan has started
107
+ busy: Whether the pipeline is currently busy
108
+ job_name: Current job name (e.g., indexing files/indexing texts)
109
+ job_start: Job start time as ISO format string (optional)
110
+ docs: Total number of documents to be indexed
111
+ batchs: Number of batches for processing documents
112
+ cur_batch: Current processing batch
113
+ request_pending: Flag for pending request for processing
114
+ latest_message: Latest message from pipeline processing
115
+ history_messages: List of history messages
116
+ """
117
+
118
+ autoscanned: bool = False
119
+ busy: bool = False
120
+ job_name: str = "Default Job"
121
+ job_start: Optional[str] = None
122
+ docs: int = 0
123
+ batchs: int = 0
124
+ cur_batch: int = 0
125
+ request_pending: bool = False
126
+ latest_message: str = ""
127
+ history_messages: Optional[List[str]] = None
128
+
129
+ class Config:
130
+ extra = "allow" # Allow additional fields from the pipeline status
131
+
132
+
133
  class DocumentManager:
134
  def __init__(
135
  self,
 
278
  if global_args["main_args"].document_loading_engine == "DOCLING":
279
  if not pm.is_installed("docling"): # type: ignore
280
  pm.install("docling")
281
+ from docling.document_converter import DocumentConverter # type: ignore
282
 
283
  converter = DocumentConverter()
284
  result = converter.convert(file_path)
 
297
  if global_args["main_args"].document_loading_engine == "DOCLING":
298
  if not pm.is_installed("docling"): # type: ignore
299
  pm.install("docling")
300
+ from docling.document_converter import DocumentConverter # type: ignore
301
 
302
  converter = DocumentConverter()
303
  result = converter.convert(file_path)
 
317
  if global_args["main_args"].document_loading_engine == "DOCLING":
318
  if not pm.is_installed("docling"): # type: ignore
319
  pm.install("docling")
320
+ from docling.document_converter import DocumentConverter # type: ignore
321
 
322
  converter = DocumentConverter()
323
  result = converter.convert(file_path)
 
338
  if global_args["main_args"].document_loading_engine == "DOCLING":
339
  if not pm.is_installed("docling"): # type: ignore
340
  pm.install("docling")
341
+ from docling.document_converter import DocumentConverter # type: ignore
342
 
343
  converter = DocumentConverter()
344
  result = converter.convert(file_path)
 
749
  logger.error(traceback.format_exc())
750
  raise HTTPException(status_code=500, detail=str(e))
751
 
752
+ @router.get(
753
+ "/pipeline_status",
754
+ dependencies=[Depends(optional_api_key)],
755
+ response_model=PipelineStatusResponse,
756
+ )
757
+ async def get_pipeline_status() -> PipelineStatusResponse:
758
  """
759
  Get the current status of the document indexing pipeline.
760
 
761
  This endpoint returns information about the current state of the document processing pipeline,
762
+ including the processing status, progress information, and history messages.
 
763
 
764
  Returns:
765
+ PipelineStatusResponse: A response object containing:
766
+ - autoscanned (bool): Whether auto-scan has started
767
+ - busy (bool): Whether the pipeline is currently busy
768
+ - job_name (str): Current job name (e.g., indexing files/indexing texts)
769
+ - job_start (str, optional): Job start time as ISO format string
770
+ - docs (int): Total number of documents to be indexed
771
+ - batchs (int): Number of batches for processing documents
772
+ - cur_batch (int): Current processing batch
773
+ - request_pending (bool): Flag for pending request for processing
774
+ - latest_message (str): Latest message from pipeline processing
775
+ - history_messages (List[str], optional): List of history messages
776
+
777
+ Raises:
778
+ HTTPException: If an error occurs while retrieving pipeline status (500)
779
  """
780
  try:
781
  from lightrag.kg.shared_storage import get_namespace_data
 
793
  if status_dict.get("job_start"):
794
  status_dict["job_start"] = str(status_dict["job_start"])
795
 
796
+ return PipelineStatusResponse(**status_dict)
797
  except Exception as e:
798
  logger.error(f"Error getting pipeline status: {str(e)}")
799
  logger.error(traceback.format_exc())
lightrag/api/webui/index.html ADDED
Binary file (642 Bytes). View file
 
lightrag/kg/chroma_impl.py CHANGED
@@ -156,7 +156,9 @@ class ChromaVectorDBStorage(BaseVectorStorage):
156
  logger.error(f"Error during ChromaDB upsert: {str(e)}")
157
  raise
158
 
159
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
160
  try:
161
  embedding = await self.embedding_func([query])
162
 
 
156
  logger.error(f"Error during ChromaDB upsert: {str(e)}")
157
  raise
158
 
159
+ async def query(
160
+ self, query: str, top_k: int, ids: list[str] | None = None
161
+ ) -> list[dict[str, Any]]:
162
  try:
163
  embedding = await self.embedding_func([query])
164
 
lightrag/kg/faiss_impl.py CHANGED
@@ -171,7 +171,9 @@ class FaissVectorDBStorage(BaseVectorStorage):
171
  logger.info(f"Upserted {len(list_data)} vectors into Faiss index.")
172
  return [m["__id__"] for m in list_data]
173
 
174
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
175
  """
176
  Search by a textual query; returns top_k results with their metadata + similarity distance.
177
  """
 
171
  logger.info(f"Upserted {len(list_data)} vectors into Faiss index.")
172
  return [m["__id__"] for m in list_data]
173
 
174
+ async def query(
175
+ self, query: str, top_k: int, ids: list[str] | None = None
176
+ ) -> list[dict[str, Any]]:
177
  """
178
  Search by a textual query; returns top_k results with their metadata + similarity distance.
179
  """
lightrag/kg/milvus_impl.py CHANGED
@@ -101,7 +101,9 @@ class MilvusVectorDBStorage(BaseVectorStorage):
101
  results = self._client.upsert(collection_name=self.namespace, data=list_data)
102
  return results
103
 
104
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
105
  embedding = await self.embedding_func([query])
106
  results = self._client.search(
107
  collection_name=self.namespace,
 
101
  results = self._client.upsert(collection_name=self.namespace, data=list_data)
102
  return results
103
 
104
+ async def query(
105
+ self, query: str, top_k: int, ids: list[str] | None = None
106
+ ) -> list[dict[str, Any]]:
107
  embedding = await self.embedding_func([query])
108
  results = self._client.search(
109
  collection_name=self.namespace,
lightrag/kg/mongo_impl.py CHANGED
@@ -938,7 +938,9 @@ class MongoVectorDBStorage(BaseVectorStorage):
938
 
939
  return list_data
940
 
941
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
942
  """Queries the vector database using Atlas Vector Search."""
943
  # Generate the embedding
944
  embedding = await self.embedding_func([query])
 
938
 
939
  return list_data
940
 
941
+ async def query(
942
+ self, query: str, top_k: int, ids: list[str] | None = None
943
+ ) -> list[dict[str, Any]]:
944
  """Queries the vector database using Atlas Vector Search."""
945
  # Generate the embedding
946
  embedding = await self.embedding_func([query])
lightrag/kg/nano_vector_db_impl.py CHANGED
@@ -120,7 +120,9 @@ class NanoVectorDBStorage(BaseVectorStorage):
120
  f"embedding is not 1-1 with data, {len(embeddings)} != {len(list_data)}"
121
  )
122
 
123
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
124
  # Execute embedding outside of lock to avoid long lock times
125
  embedding = await self.embedding_func([query])
126
  embedding = embedding[0]
 
120
  f"embedding is not 1-1 with data, {len(embeddings)} != {len(list_data)}"
121
  )
122
 
123
+ async def query(
124
+ self, query: str, top_k: int, ids: list[str] | None = None
125
+ ) -> list[dict[str, Any]]:
126
  # Execute embedding outside of lock to avoid long lock times
127
  embedding = await self.embedding_func([query])
128
  embedding = embedding[0]
lightrag/kg/neo4j_impl.py CHANGED
@@ -553,18 +553,6 @@ class Neo4JStorage(BaseGraphStorage):
553
  logger.error(f"Error during upsert: {str(e)}")
554
  raise
555
 
556
- @retry(
557
- stop=stop_after_attempt(3),
558
- wait=wait_exponential(multiplier=1, min=4, max=10),
559
- retry=retry_if_exception_type(
560
- (
561
- neo4jExceptions.ServiceUnavailable,
562
- neo4jExceptions.TransientError,
563
- neo4jExceptions.WriteServiceUnavailable,
564
- neo4jExceptions.ClientError,
565
- )
566
- ),
567
- )
568
  @retry(
569
  stop=stop_after_attempt(3),
570
  wait=wait_exponential(multiplier=1, min=4, max=10),
@@ -666,14 +654,14 @@ class Neo4JStorage(BaseGraphStorage):
666
  main_query = """
667
  MATCH (n)
668
  OPTIONAL MATCH (n)-[r]-()
669
- WITH n, count(r) AS degree
670
  WHERE degree >= $min_degree
671
  ORDER BY degree DESC
672
  LIMIT $max_nodes
673
  WITH collect({node: n}) AS filtered_nodes
674
  UNWIND filtered_nodes AS node_info
675
  WITH collect(node_info.node) AS kept_nodes, filtered_nodes
676
- MATCH (a)-[r]-(b)
677
  WHERE a IN kept_nodes AND b IN kept_nodes
678
  RETURN filtered_nodes AS node_info,
679
  collect(DISTINCT r) AS relationships
@@ -703,7 +691,7 @@ class Neo4JStorage(BaseGraphStorage):
703
  WITH start, nodes, relationships
704
  UNWIND nodes AS node
705
  OPTIONAL MATCH (node)-[r]-()
706
- WITH node, count(r) AS degree, start, nodes, relationships
707
  WHERE node = start OR EXISTS((start)--(node)) OR degree >= $min_degree
708
  ORDER BY
709
  CASE
@@ -716,7 +704,7 @@ class Neo4JStorage(BaseGraphStorage):
716
  WITH collect({node: node}) AS filtered_nodes
717
  UNWIND filtered_nodes AS node_info
718
  WITH collect(node_info.node) AS kept_nodes, filtered_nodes
719
- MATCH (a)-[r]-(b)
720
  WHERE a IN kept_nodes AND b IN kept_nodes
721
  RETURN filtered_nodes AS node_info,
722
  collect(DISTINCT r) AS relationships
@@ -744,11 +732,7 @@ class Neo4JStorage(BaseGraphStorage):
744
  result.nodes.append(
745
  KnowledgeGraphNode(
746
  id=f"{node_id}",
747
- labels=[
748
- label
749
- for label in node.labels
750
- if label != "base"
751
- ],
752
  properties=dict(node),
753
  )
754
  )
@@ -865,9 +849,7 @@ class Neo4JStorage(BaseGraphStorage):
865
  # Create KnowledgeGraphNode for target
866
  target_node = KnowledgeGraphNode(
867
  id=f"{target_id}",
868
- labels=[
869
- label for label in b_node.labels if label != "base"
870
- ],
871
  properties=dict(b_node.properties),
872
  )
873
 
@@ -907,9 +889,7 @@ class Neo4JStorage(BaseGraphStorage):
907
  # Create initial KnowledgeGraphNode
908
  start_node = KnowledgeGraphNode(
909
  id=f"{node_record['n'].get('entity_id')}",
910
- labels=[
911
- label for label in node_record["n"].labels if label != "base"
912
- ],
913
  properties=dict(node_record["n"].properties),
914
  )
915
  finally:
 
553
  logger.error(f"Error during upsert: {str(e)}")
554
  raise
555
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  @retry(
557
  stop=stop_after_attempt(3),
558
  wait=wait_exponential(multiplier=1, min=4, max=10),
 
654
  main_query = """
655
  MATCH (n)
656
  OPTIONAL MATCH (n)-[r]-()
657
+ WITH n, COALESCE(count(r), 0) AS degree
658
  WHERE degree >= $min_degree
659
  ORDER BY degree DESC
660
  LIMIT $max_nodes
661
  WITH collect({node: n}) AS filtered_nodes
662
  UNWIND filtered_nodes AS node_info
663
  WITH collect(node_info.node) AS kept_nodes, filtered_nodes
664
+ OPTIONAL MATCH (a)-[r]-(b)
665
  WHERE a IN kept_nodes AND b IN kept_nodes
666
  RETURN filtered_nodes AS node_info,
667
  collect(DISTINCT r) AS relationships
 
691
  WITH start, nodes, relationships
692
  UNWIND nodes AS node
693
  OPTIONAL MATCH (node)-[r]-()
694
+ WITH node, COALESCE(count(r), 0) AS degree, start, nodes, relationships
695
  WHERE node = start OR EXISTS((start)--(node)) OR degree >= $min_degree
696
  ORDER BY
697
  CASE
 
704
  WITH collect({node: node}) AS filtered_nodes
705
  UNWIND filtered_nodes AS node_info
706
  WITH collect(node_info.node) AS kept_nodes, filtered_nodes
707
+ OPTIONAL MATCH (a)-[r]-(b)
708
  WHERE a IN kept_nodes AND b IN kept_nodes
709
  RETURN filtered_nodes AS node_info,
710
  collect(DISTINCT r) AS relationships
 
732
  result.nodes.append(
733
  KnowledgeGraphNode(
734
  id=f"{node_id}",
735
+ labels=[node.get("entity_id")],
 
 
 
 
736
  properties=dict(node),
737
  )
738
  )
 
849
  # Create KnowledgeGraphNode for target
850
  target_node = KnowledgeGraphNode(
851
  id=f"{target_id}",
852
+ labels=list(f"{target_id}"),
 
 
853
  properties=dict(b_node.properties),
854
  )
855
 
 
889
  # Create initial KnowledgeGraphNode
890
  start_node = KnowledgeGraphNode(
891
  id=f"{node_record['n'].get('entity_id')}",
892
+ labels=list(f"{node_record['n'].get('entity_id')}"),
 
 
893
  properties=dict(node_record["n"].properties),
894
  )
895
  finally:
lightrag/kg/oracle_impl.py CHANGED
@@ -417,7 +417,9 @@ class OracleVectorDBStorage(BaseVectorStorage):
417
  self.db = None
418
 
419
  #################### query method ###############
420
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
421
  embeddings = await self.embedding_func([query])
422
  embedding = embeddings[0]
423
  # 转换精度
 
417
  self.db = None
418
 
419
  #################### query method ###############
420
+ async def query(
421
+ self, query: str, top_k: int, ids: list[str] | None = None
422
+ ) -> list[dict[str, Any]]:
423
  embeddings = await self.embedding_func([query])
424
  embedding = embeddings[0]
425
  # 转换精度
lightrag/kg/qdrant_impl.py CHANGED
@@ -123,7 +123,9 @@ class QdrantVectorDBStorage(BaseVectorStorage):
123
  )
124
  return results
125
 
126
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
127
  embedding = await self.embedding_func([query])
128
  results = self._client.search(
129
  collection_name=self.namespace,
 
123
  )
124
  return results
125
 
126
+ async def query(
127
+ self, query: str, top_k: int, ids: list[str] | None = None
128
+ ) -> list[dict[str, Any]]:
129
  embedding = await self.embedding_func([query])
130
  results = self._client.search(
131
  collection_name=self.namespace,
lightrag/kg/tidb_impl.py CHANGED
@@ -306,7 +306,9 @@ class TiDBVectorDBStorage(BaseVectorStorage):
306
  await ClientManager.release_client(self.db)
307
  self.db = None
308
 
309
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
310
  """Search from tidb vector"""
311
  embeddings = await self.embedding_func([query])
312
  embedding = embeddings[0]
 
306
  await ClientManager.release_client(self.db)
307
  self.db = None
308
 
309
+ async def query(
310
+ self, query: str, top_k: int, ids: list[str] | None = None
311
+ ) -> list[dict[str, Any]]:
312
  """Search from tidb vector"""
313
  embeddings = await self.embedding_func([query])
314
  embedding = embeddings[0]
lightrag/lightrag.py CHANGED
@@ -3,11 +3,14 @@ from __future__ import annotations
3
  import asyncio
4
  import configparser
5
  import os
 
6
  import warnings
7
  from dataclasses import asdict, dataclass, field
8
  from datetime import datetime
9
  from functools import partial
10
- from typing import Any, AsyncIterator, Callable, Iterator, cast, final
 
 
11
 
12
  from lightrag.kg import (
13
  STORAGE_ENV_REQUIREMENTS,
@@ -1111,6 +1114,7 @@ class LightRAG:
1111
 
1112
  # Prepare node data
1113
  node_data: dict[str, str] = {
 
1114
  "entity_type": entity_type,
1115
  "description": description,
1116
  "source_id": source_id,
@@ -1148,6 +1152,7 @@ class LightRAG:
1148
  await self.chunk_entity_relation_graph.upsert_node(
1149
  need_insert_id,
1150
  node_data={
 
1151
  "source_id": source_id,
1152
  "description": "UNKNOWN",
1153
  "entity_type": "UNKNOWN",
@@ -2157,6 +2162,7 @@ class LightRAG:
2157
 
2158
  # Prepare node data with defaults if missing
2159
  node_data = {
 
2160
  "entity_type": entity_data.get("entity_type", "UNKNOWN"),
2161
  "description": entity_data.get("description", ""),
2162
  "source_id": entity_data.get("source_id", "manual"),
@@ -2592,6 +2598,322 @@ class LightRAG:
2592
  logger.error(f"Error merging entities: {e}")
2593
  raise
2594
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2595
  def merge_entities(
2596
  self,
2597
  source_entities: list[str],
 
3
  import asyncio
4
  import configparser
5
  import os
6
+ import csv
7
  import warnings
8
  from dataclasses import asdict, dataclass, field
9
  from datetime import datetime
10
  from functools import partial
11
+ from typing import Any, AsyncIterator, Callable, Iterator, cast, final, Literal
12
+ import pandas as pd
13
+
14
 
15
  from lightrag.kg import (
16
  STORAGE_ENV_REQUIREMENTS,
 
1114
 
1115
  # Prepare node data
1116
  node_data: dict[str, str] = {
1117
+ "entity_id": entity_name,
1118
  "entity_type": entity_type,
1119
  "description": description,
1120
  "source_id": source_id,
 
1152
  await self.chunk_entity_relation_graph.upsert_node(
1153
  need_insert_id,
1154
  node_data={
1155
+ "entity_id": need_insert_id,
1156
  "source_id": source_id,
1157
  "description": "UNKNOWN",
1158
  "entity_type": "UNKNOWN",
 
2162
 
2163
  # Prepare node data with defaults if missing
2164
  node_data = {
2165
+ "entity_id": entity_name,
2166
  "entity_type": entity_data.get("entity_type", "UNKNOWN"),
2167
  "description": entity_data.get("description", ""),
2168
  "source_id": entity_data.get("source_id", "manual"),
 
2598
  logger.error(f"Error merging entities: {e}")
2599
  raise
2600
 
2601
+ async def aexport_data(
2602
+ self,
2603
+ output_path: str,
2604
+ file_format: Literal["csv", "excel", "md", "txt"] = "csv",
2605
+ include_vector_data: bool = False,
2606
+ ) -> None:
2607
+ """
2608
+ Asynchronously exports all entities, relations, and relationships to various formats.
2609
+ Args:
2610
+ output_path: The path to the output file (including extension).
2611
+ file_format: Output format - "csv", "excel", "md", "txt".
2612
+ - csv: Comma-separated values file
2613
+ - excel: Microsoft Excel file with multiple sheets
2614
+ - md: Markdown tables
2615
+ - txt: Plain text formatted output
2616
+ - table: Print formatted tables to console
2617
+ include_vector_data: Whether to include data from the vector database.
2618
+ """
2619
+ # Collect data
2620
+ entities_data = []
2621
+ relations_data = []
2622
+ relationships_data = []
2623
+
2624
+ # --- Entities ---
2625
+ all_entities = await self.chunk_entity_relation_graph.get_all_labels()
2626
+ for entity_name in all_entities:
2627
+ entity_info = await self.get_entity_info(
2628
+ entity_name, include_vector_data=include_vector_data
2629
+ )
2630
+ entity_row = {
2631
+ "entity_name": entity_name,
2632
+ "source_id": entity_info["source_id"],
2633
+ "graph_data": str(
2634
+ entity_info["graph_data"]
2635
+ ), # Convert to string to ensure compatibility
2636
+ }
2637
+ if include_vector_data and "vector_data" in entity_info:
2638
+ entity_row["vector_data"] = str(entity_info["vector_data"])
2639
+ entities_data.append(entity_row)
2640
+
2641
+ # --- Relations ---
2642
+ for src_entity in all_entities:
2643
+ for tgt_entity in all_entities:
2644
+ if src_entity == tgt_entity:
2645
+ continue
2646
+
2647
+ edge_exists = await self.chunk_entity_relation_graph.has_edge(
2648
+ src_entity, tgt_entity
2649
+ )
2650
+ if edge_exists:
2651
+ relation_info = await self.get_relation_info(
2652
+ src_entity, tgt_entity, include_vector_data=include_vector_data
2653
+ )
2654
+ relation_row = {
2655
+ "src_entity": src_entity,
2656
+ "tgt_entity": tgt_entity,
2657
+ "source_id": relation_info["source_id"],
2658
+ "graph_data": str(
2659
+ relation_info["graph_data"]
2660
+ ), # Convert to string
2661
+ }
2662
+ if include_vector_data and "vector_data" in relation_info:
2663
+ relation_row["vector_data"] = str(relation_info["vector_data"])
2664
+ relations_data.append(relation_row)
2665
+
2666
+ # --- Relationships (from VectorDB) ---
2667
+ all_relationships = await self.relationships_vdb.client_storage
2668
+ for rel in all_relationships["data"]:
2669
+ relationships_data.append(
2670
+ {
2671
+ "relationship_id": rel["__id__"],
2672
+ "data": str(rel), # Convert to string for compatibility
2673
+ }
2674
+ )
2675
+
2676
+ # Export based on format
2677
+ if file_format == "csv":
2678
+ # CSV export
2679
+ with open(output_path, "w", newline="", encoding="utf-8") as csvfile:
2680
+ # Entities
2681
+ if entities_data:
2682
+ csvfile.write("# ENTITIES\n")
2683
+ writer = csv.DictWriter(csvfile, fieldnames=entities_data[0].keys())
2684
+ writer.writeheader()
2685
+ writer.writerows(entities_data)
2686
+ csvfile.write("\n\n")
2687
+
2688
+ # Relations
2689
+ if relations_data:
2690
+ csvfile.write("# RELATIONS\n")
2691
+ writer = csv.DictWriter(
2692
+ csvfile, fieldnames=relations_data[0].keys()
2693
+ )
2694
+ writer.writeheader()
2695
+ writer.writerows(relations_data)
2696
+ csvfile.write("\n\n")
2697
+
2698
+ # Relationships
2699
+ if relationships_data:
2700
+ csvfile.write("# RELATIONSHIPS\n")
2701
+ writer = csv.DictWriter(
2702
+ csvfile, fieldnames=relationships_data[0].keys()
2703
+ )
2704
+ writer.writeheader()
2705
+ writer.writerows(relationships_data)
2706
+
2707
+ elif file_format == "excel":
2708
+ # Excel export
2709
+ entities_df = (
2710
+ pd.DataFrame(entities_data) if entities_data else pd.DataFrame()
2711
+ )
2712
+ relations_df = (
2713
+ pd.DataFrame(relations_data) if relations_data else pd.DataFrame()
2714
+ )
2715
+ relationships_df = (
2716
+ pd.DataFrame(relationships_data)
2717
+ if relationships_data
2718
+ else pd.DataFrame()
2719
+ )
2720
+
2721
+ with pd.ExcelWriter(output_path, engine="xlsxwriter") as writer:
2722
+ if not entities_df.empty:
2723
+ entities_df.to_excel(writer, sheet_name="Entities", index=False)
2724
+ if not relations_df.empty:
2725
+ relations_df.to_excel(writer, sheet_name="Relations", index=False)
2726
+ if not relationships_df.empty:
2727
+ relationships_df.to_excel(
2728
+ writer, sheet_name="Relationships", index=False
2729
+ )
2730
+
2731
+ elif file_format == "md":
2732
+ # Markdown export
2733
+ with open(output_path, "w", encoding="utf-8") as mdfile:
2734
+ mdfile.write("# LightRAG Data Export\n\n")
2735
+
2736
+ # Entities
2737
+ mdfile.write("## Entities\n\n")
2738
+ if entities_data:
2739
+ # Write header
2740
+ mdfile.write("| " + " | ".join(entities_data[0].keys()) + " |\n")
2741
+ mdfile.write(
2742
+ "| "
2743
+ + " | ".join(["---"] * len(entities_data[0].keys()))
2744
+ + " |\n"
2745
+ )
2746
+
2747
+ # Write rows
2748
+ for entity in entities_data:
2749
+ mdfile.write(
2750
+ "| " + " | ".join(str(v) for v in entity.values()) + " |\n"
2751
+ )
2752
+ mdfile.write("\n\n")
2753
+ else:
2754
+ mdfile.write("*No entity data available*\n\n")
2755
+
2756
+ # Relations
2757
+ mdfile.write("## Relations\n\n")
2758
+ if relations_data:
2759
+ # Write header
2760
+ mdfile.write("| " + " | ".join(relations_data[0].keys()) + " |\n")
2761
+ mdfile.write(
2762
+ "| "
2763
+ + " | ".join(["---"] * len(relations_data[0].keys()))
2764
+ + " |\n"
2765
+ )
2766
+
2767
+ # Write rows
2768
+ for relation in relations_data:
2769
+ mdfile.write(
2770
+ "| "
2771
+ + " | ".join(str(v) for v in relation.values())
2772
+ + " |\n"
2773
+ )
2774
+ mdfile.write("\n\n")
2775
+ else:
2776
+ mdfile.write("*No relation data available*\n\n")
2777
+
2778
+ # Relationships
2779
+ mdfile.write("## Relationships\n\n")
2780
+ if relationships_data:
2781
+ # Write header
2782
+ mdfile.write(
2783
+ "| " + " | ".join(relationships_data[0].keys()) + " |\n"
2784
+ )
2785
+ mdfile.write(
2786
+ "| "
2787
+ + " | ".join(["---"] * len(relationships_data[0].keys()))
2788
+ + " |\n"
2789
+ )
2790
+
2791
+ # Write rows
2792
+ for relationship in relationships_data:
2793
+ mdfile.write(
2794
+ "| "
2795
+ + " | ".join(str(v) for v in relationship.values())
2796
+ + " |\n"
2797
+ )
2798
+ else:
2799
+ mdfile.write("*No relationship data available*\n\n")
2800
+
2801
+ elif file_format == "txt":
2802
+ # Plain text export
2803
+ with open(output_path, "w", encoding="utf-8") as txtfile:
2804
+ txtfile.write("LIGHTRAG DATA EXPORT\n")
2805
+ txtfile.write("=" * 80 + "\n\n")
2806
+
2807
+ # Entities
2808
+ txtfile.write("ENTITIES\n")
2809
+ txtfile.write("-" * 80 + "\n")
2810
+ if entities_data:
2811
+ # Create fixed width columns
2812
+ col_widths = {
2813
+ k: max(len(k), max(len(str(e[k])) for e in entities_data))
2814
+ for k in entities_data[0]
2815
+ }
2816
+ header = " ".join(k.ljust(col_widths[k]) for k in entities_data[0])
2817
+ txtfile.write(header + "\n")
2818
+ txtfile.write("-" * len(header) + "\n")
2819
+
2820
+ # Write rows
2821
+ for entity in entities_data:
2822
+ row = " ".join(
2823
+ str(v).ljust(col_widths[k]) for k, v in entity.items()
2824
+ )
2825
+ txtfile.write(row + "\n")
2826
+ txtfile.write("\n\n")
2827
+ else:
2828
+ txtfile.write("No entity data available\n\n")
2829
+
2830
+ # Relations
2831
+ txtfile.write("RELATIONS\n")
2832
+ txtfile.write("-" * 80 + "\n")
2833
+ if relations_data:
2834
+ # Create fixed width columns
2835
+ col_widths = {
2836
+ k: max(len(k), max(len(str(r[k])) for r in relations_data))
2837
+ for k in relations_data[0]
2838
+ }
2839
+ header = " ".join(
2840
+ k.ljust(col_widths[k]) for k in relations_data[0]
2841
+ )
2842
+ txtfile.write(header + "\n")
2843
+ txtfile.write("-" * len(header) + "\n")
2844
+
2845
+ # Write rows
2846
+ for relation in relations_data:
2847
+ row = " ".join(
2848
+ str(v).ljust(col_widths[k]) for k, v in relation.items()
2849
+ )
2850
+ txtfile.write(row + "\n")
2851
+ txtfile.write("\n\n")
2852
+ else:
2853
+ txtfile.write("No relation data available\n\n")
2854
+
2855
+ # Relationships
2856
+ txtfile.write("RELATIONSHIPS\n")
2857
+ txtfile.write("-" * 80 + "\n")
2858
+ if relationships_data:
2859
+ # Create fixed width columns
2860
+ col_widths = {
2861
+ k: max(len(k), max(len(str(r[k])) for r in relationships_data))
2862
+ for k in relationships_data[0]
2863
+ }
2864
+ header = " ".join(
2865
+ k.ljust(col_widths[k]) for k in relationships_data[0]
2866
+ )
2867
+ txtfile.write(header + "\n")
2868
+ txtfile.write("-" * len(header) + "\n")
2869
+
2870
+ # Write rows
2871
+ for relationship in relationships_data:
2872
+ row = " ".join(
2873
+ str(v).ljust(col_widths[k]) for k, v in relationship.items()
2874
+ )
2875
+ txtfile.write(row + "\n")
2876
+ else:
2877
+ txtfile.write("No relationship data available\n\n")
2878
+
2879
+ else:
2880
+ raise ValueError(
2881
+ f"Unsupported file format: {file_format}. "
2882
+ f"Choose from: csv, excel, md, txt"
2883
+ )
2884
+ if file_format is not None:
2885
+ print(f"Data exported to: {output_path} with format: {file_format}")
2886
+ else:
2887
+ print("Data displayed as table format")
2888
+
2889
+ def export_data(
2890
+ self,
2891
+ output_path: str,
2892
+ file_format: Literal["csv", "excel", "md", "txt"] = "csv",
2893
+ include_vector_data: bool = False,
2894
+ ) -> None:
2895
+ """
2896
+ Synchronously exports all entities, relations, and relationships to various formats.
2897
+ Args:
2898
+ output_path: The path to the output file (including extension).
2899
+ file_format: Output format - "csv", "excel", "md", "txt".
2900
+ - csv: Comma-separated values file
2901
+ - excel: Microsoft Excel file with multiple sheets
2902
+ - md: Markdown tables
2903
+ - txt: Plain text formatted output
2904
+ - table: Print formatted tables to console
2905
+ include_vector_data: Whether to include data from the vector database.
2906
+ """
2907
+ try:
2908
+ loop = asyncio.get_event_loop()
2909
+ except RuntimeError:
2910
+ loop = asyncio.new_event_loop()
2911
+ asyncio.set_event_loop(loop)
2912
+
2913
+ loop.run_until_complete(
2914
+ self.aexport_data(output_path, file_format, include_vector_data)
2915
+ )
2916
+
2917
  def merge_entities(
2918
  self,
2919
  source_entities: list[str],
lightrag/utils.py CHANGED
@@ -76,6 +76,7 @@ class LightragPathFilter(logging.Filter):
76
  super().__init__()
77
  # Define paths to be filtered
78
  self.filtered_paths = ["/documents", "/health", "/webui/"]
 
79
 
80
  def filter(self, record):
81
  try:
 
76
  super().__init__()
77
  # Define paths to be filtered
78
  self.filtered_paths = ["/documents", "/health", "/webui/"]
79
+ # self.filtered_paths = ["/health", "/webui/"]
80
 
81
  def filter(self, record):
82
  try:
lightrag_webui/bun.lock CHANGED
@@ -63,6 +63,7 @@
63
  "@types/node": "^22.13.5",
64
  "@types/react": "^19.0.10",
65
  "@types/react-dom": "^19.0.4",
 
66
  "@types/react-syntax-highlighter": "^15.5.13",
67
  "@types/seedrandom": "^3.0.8",
68
  "@vitejs/plugin-react-swc": "^3.8.0",
@@ -446,6 +447,8 @@
446
 
447
  "@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
448
 
 
 
449
  "@types/react-syntax-highlighter": ["@types/[email protected]", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
450
 
451
  "@types/react-transition-group": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
 
63
  "@types/node": "^22.13.5",
64
  "@types/react": "^19.0.10",
65
  "@types/react-dom": "^19.0.4",
66
+ "@types/react-i18next": "^8.1.0",
67
  "@types/react-syntax-highlighter": "^15.5.13",
68
  "@types/seedrandom": "^3.0.8",
69
  "@vitejs/plugin-react-swc": "^3.8.0",
 
447
 
448
  "@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
449
 
450
+ "@types/react-i18next": ["@types/[email protected]", "", { "dependencies": { "react-i18next": "*" } }, "sha512-d4xhcjX5b3roNMObRNMfb1HinHQlQLPo8xlDj60dnHeeAw2bBymR2cy/l1giJpHzo/ZFgSvgVUvIWr4kCrenCg=="],
451
+
452
  "@types/react-syntax-highlighter": ["@types/[email protected]", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
453
 
454
  "@types/react-transition-group": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
lightrag_webui/index.html CHANGED
@@ -2,6 +2,9 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
 
 
 
5
  <link rel="icon" type="image/svg+xml" href="logo.png" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>Lightrag</title>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
6
+ <meta http-equiv="Pragma" content="no-cache" />
7
+ <meta http-equiv="Expires" content="0" />
8
  <link rel="icon" type="image/svg+xml" href="logo.png" />
9
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
  <title>Lightrag</title>
lightrag_webui/package.json CHANGED
@@ -72,6 +72,7 @@
72
  "@types/node": "^22.13.5",
73
  "@types/react": "^19.0.10",
74
  "@types/react-dom": "^19.0.4",
 
75
  "@types/react-syntax-highlighter": "^15.5.13",
76
  "@types/seedrandom": "^3.0.8",
77
  "@vitejs/plugin-react-swc": "^3.8.0",
 
72
  "@types/node": "^22.13.5",
73
  "@types/react": "^19.0.10",
74
  "@types/react-dom": "^19.0.4",
75
+ "@types/react-i18next": "^8.1.0",
76
  "@types/react-syntax-highlighter": "^15.5.13",
77
  "@types/seedrandom": "^3.0.8",
78
  "@vitejs/plugin-react-swc": "^3.8.0",
lightrag_webui/src/App.tsx CHANGED
@@ -1,4 +1,6 @@
1
  import { useState, useCallback } from 'react'
 
 
2
  import MessageAlert from '@/components/MessageAlert'
3
  import ApiKeyAlert from '@/components/ApiKeyAlert'
4
  import StatusIndicator from '@/components/graph/StatusIndicator'
@@ -19,7 +21,7 @@ import { Tabs, TabsContent } from '@/components/ui/Tabs'
19
  function App() {
20
  const message = useBackendState.use.message()
21
  const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
22
- const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
23
  const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
24
 
25
  // Health check
@@ -51,32 +53,36 @@ function App() {
51
  }, [message, setApiKeyInvalid])
52
 
53
  return (
54
- <main className="flex h-screen w-screen overflow-x-hidden">
55
- <Tabs
56
- defaultValue={currentTab}
57
- className="!m-0 flex grow flex-col !p-0"
58
- onValueChange={handleTabChange}
59
- >
60
- <SiteHeader />
61
- <div className="relative grow">
62
- <TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
63
- <DocumentManager />
64
- </TabsContent>
65
- <TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
66
- <GraphViewer />
67
- </TabsContent>
68
- <TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
69
- <RetrievalTesting />
70
- </TabsContent>
71
- <TabsContent value="api" className="absolute top-0 right-0 bottom-0 left-0">
72
- <ApiSite />
73
- </TabsContent>
74
- </div>
75
- </Tabs>
76
- {enableHealthCheck && <StatusIndicator />}
77
- {message !== null && !apiKeyInvalid && <MessageAlert />}
78
- {apiKeyInvalid && <ApiKeyAlert />}
79
- </main>
 
 
 
 
80
  )
81
  }
82
 
 
1
  import { useState, useCallback } from 'react'
2
+ import ThemeProvider from '@/components/ThemeProvider'
3
+ import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
4
  import MessageAlert from '@/components/MessageAlert'
5
  import ApiKeyAlert from '@/components/ApiKeyAlert'
6
  import StatusIndicator from '@/components/graph/StatusIndicator'
 
21
  function App() {
22
  const message = useBackendState.use.message()
23
  const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
24
+ const currentTab = useSettingsStore.use.currentTab()
25
  const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
26
 
27
  // Health check
 
53
  }, [message, setApiKeyInvalid])
54
 
55
  return (
56
+ <ThemeProvider>
57
+ <TabVisibilityProvider>
58
+ <main className="flex h-screen w-screen overflow-x-hidden">
59
+ <Tabs
60
+ defaultValue={currentTab}
61
+ className="!m-0 flex grow flex-col !p-0"
62
+ onValueChange={handleTabChange}
63
+ >
64
+ <SiteHeader />
65
+ <div className="relative grow">
66
+ <TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
67
+ <DocumentManager />
68
+ </TabsContent>
69
+ <TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
70
+ <GraphViewer />
71
+ </TabsContent>
72
+ <TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
73
+ <RetrievalTesting />
74
+ </TabsContent>
75
+ <TabsContent value="api" className="absolute top-0 right-0 bottom-0 left-0">
76
+ <ApiSite />
77
+ </TabsContent>
78
+ </div>
79
+ </Tabs>
80
+ {enableHealthCheck && <StatusIndicator />}
81
+ {message !== null && !apiKeyInvalid && <MessageAlert />}
82
+ {apiKeyInvalid && <ApiKeyAlert />}
83
+ </main>
84
+ </TabVisibilityProvider>
85
+ </ThemeProvider>
86
  )
87
  }
88
 
lightrag_webui/src/components/AppSettings.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react'
2
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
3
+ import Button from '@/components/ui/Button'
4
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'
5
+ import { useSettingsStore } from '@/stores/settings'
6
+ import { PaletteIcon } from 'lucide-react'
7
+ import { useTranslation } from 'react-i18next'
8
+
9
+ export default function AppSettings() {
10
+ const [opened, setOpened] = useState<boolean>(false)
11
+ const { t } = useTranslation()
12
+
13
+ const language = useSettingsStore.use.language()
14
+ const setLanguage = useSettingsStore.use.setLanguage()
15
+
16
+ const theme = useSettingsStore.use.theme()
17
+ const setTheme = useSettingsStore.use.setTheme()
18
+
19
+ const handleLanguageChange = useCallback((value: string) => {
20
+ setLanguage(value as 'en' | 'zh')
21
+ }, [setLanguage])
22
+
23
+ const handleThemeChange = useCallback((value: string) => {
24
+ setTheme(value as 'light' | 'dark' | 'system')
25
+ }, [setTheme])
26
+
27
+ return (
28
+ <Popover open={opened} onOpenChange={setOpened}>
29
+ <PopoverTrigger asChild>
30
+ <Button variant="outline" size="icon" className="h-9 w-9">
31
+ <PaletteIcon className="h-5 w-5" />
32
+ </Button>
33
+ </PopoverTrigger>
34
+ <PopoverContent side="bottom" align="end" className="w-56">
35
+ <div className="flex flex-col gap-4">
36
+ <div className="flex flex-col gap-2">
37
+ <label className="text-sm font-medium">{t('settings.language')}</label>
38
+ <Select value={language} onValueChange={handleLanguageChange}>
39
+ <SelectTrigger>
40
+ <SelectValue />
41
+ </SelectTrigger>
42
+ <SelectContent>
43
+ <SelectItem value="en">English</SelectItem>
44
+ <SelectItem value="zh">中文</SelectItem>
45
+ </SelectContent>
46
+ </Select>
47
+ </div>
48
+
49
+ <div className="flex flex-col gap-2">
50
+ <label className="text-sm font-medium">{t('settings.theme')}</label>
51
+ <Select value={theme} onValueChange={handleThemeChange}>
52
+ <SelectTrigger>
53
+ <SelectValue />
54
+ </SelectTrigger>
55
+ <SelectContent>
56
+ <SelectItem value="light">{t('settings.light')}</SelectItem>
57
+ <SelectItem value="dark">{t('settings.dark')}</SelectItem>
58
+ <SelectItem value="system">{t('settings.system')}</SelectItem>
59
+ </SelectContent>
60
+ </Select>
61
+ </div>
62
+ </div>
63
+ </PopoverContent>
64
+ </Popover>
65
+ )
66
+ }
lightrag_webui/src/components/Root.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode, useEffect, useState } from 'react'
2
+ import { initializeI18n } from '@/i18n'
3
+ import App from '@/App'
4
+
5
+ export const Root = () => {
6
+ const [isI18nInitialized, setIsI18nInitialized] = useState(false)
7
+
8
+ useEffect(() => {
9
+ // Initialize i18n immediately with persisted language
10
+ initializeI18n().then(() => {
11
+ setIsI18nInitialized(true)
12
+ })
13
+ }, [])
14
+
15
+ if (!isI18nInitialized) {
16
+ return null // or a loading spinner
17
+ }
18
+
19
+ return (
20
+ <StrictMode>
21
+ <App />
22
+ </StrictMode>
23
+ )
24
+ }
lightrag_webui/src/components/ThemeProvider.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useEffect, useState } from 'react'
2
  import { Theme, useSettingsStore } from '@/stores/settings'
3
 
4
  type ThemeProviderProps = {
@@ -21,30 +21,32 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
21
  * Component that provides the theme state and setter function to its children.
22
  */
23
  export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
24
- const [theme, setTheme] = useState<Theme>(useSettingsStore.getState().theme)
 
25
 
26
  useEffect(() => {
27
  const root = window.document.documentElement
28
  root.classList.remove('light', 'dark')
29
 
30
  if (theme === 'system') {
31
- const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
32
- ? 'dark'
33
- : 'light'
34
- root.classList.add(systemTheme)
35
- setTheme(systemTheme)
36
- return
 
 
 
 
 
 
37
  }
38
-
39
- root.classList.add(theme)
40
  }, [theme])
41
 
42
  const value = {
43
  theme,
44
- setTheme: (theme: Theme) => {
45
- useSettingsStore.getState().setTheme(theme)
46
- setTheme(theme)
47
- }
48
  }
49
 
50
  return (
 
1
+ import { createContext, useEffect } from 'react'
2
  import { Theme, useSettingsStore } from '@/stores/settings'
3
 
4
  type ThemeProviderProps = {
 
21
  * Component that provides the theme state and setter function to its children.
22
  */
23
  export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
24
+ const theme = useSettingsStore.use.theme()
25
+ const setTheme = useSettingsStore.use.setTheme()
26
 
27
  useEffect(() => {
28
  const root = window.document.documentElement
29
  root.classList.remove('light', 'dark')
30
 
31
  if (theme === 'system') {
32
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
33
+ const handleChange = (e: MediaQueryListEvent) => {
34
+ root.classList.remove('light', 'dark')
35
+ root.classList.add(e.matches ? 'dark' : 'light')
36
+ }
37
+
38
+ root.classList.add(mediaQuery.matches ? 'dark' : 'light')
39
+ mediaQuery.addEventListener('change', handleChange)
40
+
41
+ return () => mediaQuery.removeEventListener('change', handleChange)
42
+ } else {
43
+ root.classList.add(theme)
44
  }
 
 
45
  }, [theme])
46
 
47
  const value = {
48
  theme,
49
+ setTheme
 
 
 
50
  }
51
 
52
  return (
lightrag_webui/src/components/graph/FocusOnNode.tsx CHANGED
@@ -13,15 +13,24 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
13
  * When the selected item changes, highlighted the node and center the camera on it.
14
  */
15
  useEffect(() => {
16
- if (!node) return
17
- sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
18
  if (move) {
19
- gotoNode(node)
 
 
 
 
 
 
 
20
  useGraphStore.getState().setMoveToSelectedNode(false)
 
 
21
  }
22
 
23
  return () => {
24
- sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
 
 
25
  }
26
  }, [node, move, sigma, gotoNode])
27
 
 
13
  * When the selected item changes, highlighted the node and center the camera on it.
14
  */
15
  useEffect(() => {
 
 
16
  if (move) {
17
+ if (node) {
18
+ sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
19
+ gotoNode(node)
20
+ } else {
21
+ // If no node is selected but move is true, reset to default view
22
+ sigma.setCustomBBox(null)
23
+ sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 })
24
+ }
25
  useGraphStore.getState().setMoveToSelectedNode(false)
26
+ } else if (node) {
27
+ sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
28
  }
29
 
30
  return () => {
31
+ if (node) {
32
+ sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
33
+ }
34
  }
35
  }, [node, move, sigma, gotoNode])
36
 
lightrag_webui/src/components/graph/GraphControl.tsx CHANGED
@@ -1,10 +1,11 @@
1
  import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
 
2
  // import { useLayoutCircular } from '@react-sigma/layout-circular'
3
  import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
4
  import { useEffect } from 'react'
5
 
6
  // import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
7
- import useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph'
8
  import useTheme from '@/hooks/useTheme'
9
  import * as Constants from '@/lib/constants'
10
 
@@ -21,7 +22,6 @@ const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
21
  }
22
 
23
  const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
24
- const { lightrageGraph } = useLightragGraph()
25
  const sigma = useSigma<NodeType, EdgeType>()
26
  const registerEvents = useRegisterEvents<NodeType, EdgeType>()
27
  const setSettings = useSetSettings<NodeType, EdgeType>()
@@ -34,21 +34,25 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
34
 
35
  const { theme } = useTheme()
36
  const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
 
 
 
37
  const selectedNode = useGraphStore.use.selectedNode()
38
  const focusedNode = useGraphStore.use.focusedNode()
39
  const selectedEdge = useGraphStore.use.selectedEdge()
40
  const focusedEdge = useGraphStore.use.focusedEdge()
 
41
 
42
  /**
43
  * When component mount or maxIterations changes
44
  * => load the graph and apply layout
45
  */
46
  useEffect(() => {
47
- // Create & load the graph
48
- const graph = lightrageGraph()
49
- loadGraph(graph)
50
- assignLayout()
51
- }, [assignLayout, loadGraph, lightrageGraph, maxIterations])
52
 
53
  /**
54
  * When component mount
@@ -58,39 +62,52 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
58
  const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
59
  useGraphStore.getState()
60
 
61
- // Register the events
62
- registerEvents({
63
- enterNode: (event) => {
 
 
 
 
64
  if (!isButtonPressed(event.event.original)) {
65
  setFocusedNode(event.node)
66
  }
67
  },
68
- leaveNode: (event) => {
69
  if (!isButtonPressed(event.event.original)) {
70
  setFocusedNode(null)
71
  }
72
  },
73
- clickNode: (event) => {
74
  setSelectedNode(event.node)
75
  setSelectedEdge(null)
76
  },
77
- clickEdge: (event) => {
 
 
 
 
 
78
  setSelectedEdge(event.edge)
79
  setSelectedNode(null)
80
- },
81
- enterEdge: (event) => {
 
82
  if (!isButtonPressed(event.event.original)) {
83
  setFocusedEdge(event.edge)
84
  }
85
- },
86
- leaveEdge: (event) => {
 
87
  if (!isButtonPressed(event.event.original)) {
88
  setFocusedEdge(null)
89
  }
90
- },
91
- clickStage: () => clearSelection()
92
- })
93
- }, [registerEvents])
 
 
94
 
95
  /**
96
  * When component mount or hovered node change
@@ -101,7 +118,14 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
101
  const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined
102
  const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined
103
 
 
104
  setSettings({
 
 
 
 
 
 
105
  nodeReducer: (node, data) => {
106
  const graph = sigma.getGraph()
107
  const newData: NodeType & {
@@ -140,6 +164,8 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
140
  }
141
  return newData
142
  },
 
 
143
  edgeReducer: (edge, data) => {
144
  const graph = sigma.getGraph()
145
  const newData = { ...data, hidden: false, labelColor, color: edgeColor }
@@ -181,7 +207,10 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
181
  sigma,
182
  disableHoverEffect,
183
  theme,
184
- hideUnselectedEdges
 
 
 
185
  ])
186
 
187
  return null
 
1
  import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
2
+ import Graph from 'graphology'
3
  // import { useLayoutCircular } from '@react-sigma/layout-circular'
4
  import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
5
  import { useEffect } from 'react'
6
 
7
  // import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
8
+ import { EdgeType, NodeType } from '@/hooks/useLightragGraph'
9
  import useTheme from '@/hooks/useTheme'
10
  import * as Constants from '@/lib/constants'
11
 
 
22
  }
23
 
24
  const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
 
25
  const sigma = useSigma<NodeType, EdgeType>()
26
  const registerEvents = useRegisterEvents<NodeType, EdgeType>()
27
  const setSettings = useSetSettings<NodeType, EdgeType>()
 
34
 
35
  const { theme } = useTheme()
36
  const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
37
+ const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
38
+ const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
39
+ const renderLabels = useSettingsStore.use.showNodeLabel()
40
  const selectedNode = useGraphStore.use.selectedNode()
41
  const focusedNode = useGraphStore.use.focusedNode()
42
  const selectedEdge = useGraphStore.use.selectedEdge()
43
  const focusedEdge = useGraphStore.use.focusedEdge()
44
+ const sigmaGraph = useGraphStore.use.sigmaGraph()
45
 
46
  /**
47
  * When component mount or maxIterations changes
48
  * => load the graph and apply layout
49
  */
50
  useEffect(() => {
51
+ if (sigmaGraph) {
52
+ loadGraph(sigmaGraph as unknown as Graph<NodeType, EdgeType>)
53
+ assignLayout()
54
+ }
55
+ }, [assignLayout, loadGraph, sigmaGraph, maxIterations])
56
 
57
  /**
58
  * When component mount
 
62
  const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
63
  useGraphStore.getState()
64
 
65
+ // Define event types
66
+ type NodeEvent = { node: string; event: { original: MouseEvent | TouchEvent } }
67
+ type EdgeEvent = { edge: string; event: { original: MouseEvent | TouchEvent } }
68
+
69
+ // Register all events, but edge events will only be processed if enableEdgeEvents is true
70
+ const events: Record<string, any> = {
71
+ enterNode: (event: NodeEvent) => {
72
  if (!isButtonPressed(event.event.original)) {
73
  setFocusedNode(event.node)
74
  }
75
  },
76
+ leaveNode: (event: NodeEvent) => {
77
  if (!isButtonPressed(event.event.original)) {
78
  setFocusedNode(null)
79
  }
80
  },
81
+ clickNode: (event: NodeEvent) => {
82
  setSelectedNode(event.node)
83
  setSelectedEdge(null)
84
  },
85
+ clickStage: () => clearSelection()
86
+ }
87
+
88
+ // Only add edge event handlers if enableEdgeEvents is true
89
+ if (enableEdgeEvents) {
90
+ events.clickEdge = (event: EdgeEvent) => {
91
  setSelectedEdge(event.edge)
92
  setSelectedNode(null)
93
+ }
94
+
95
+ events.enterEdge = (event: EdgeEvent) => {
96
  if (!isButtonPressed(event.event.original)) {
97
  setFocusedEdge(event.edge)
98
  }
99
+ }
100
+
101
+ events.leaveEdge = (event: EdgeEvent) => {
102
  if (!isButtonPressed(event.event.original)) {
103
  setFocusedEdge(null)
104
  }
105
+ }
106
+ }
107
+
108
+ // Register the events
109
+ registerEvents(events)
110
+ }, [registerEvents, enableEdgeEvents])
111
 
112
  /**
113
  * When component mount or hovered node change
 
118
  const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined
119
  const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined
120
 
121
+ // Update all dynamic settings directly without recreating the sigma container
122
  setSettings({
123
+ // Update display settings
124
+ enableEdgeEvents,
125
+ renderEdgeLabels,
126
+ renderLabels,
127
+
128
+ // Node reducer for node appearance
129
  nodeReducer: (node, data) => {
130
  const graph = sigma.getGraph()
131
  const newData: NodeType & {
 
164
  }
165
  return newData
166
  },
167
+
168
+ // Edge reducer for edge appearance
169
  edgeReducer: (edge, data) => {
170
  const graph = sigma.getGraph()
171
  const newData = { ...data, hidden: false, labelColor, color: edgeColor }
 
207
  sigma,
208
  disableHoverEffect,
209
  theme,
210
+ hideUnselectedEdges,
211
+ enableEdgeEvents,
212
+ renderEdgeLabels,
213
+ renderLabels
214
  ])
215
 
216
  return null
lightrag_webui/src/components/graph/GraphLabels.tsx CHANGED
@@ -1,37 +1,48 @@
1
- import { useCallback } from 'react'
2
  import { AsyncSelect } from '@/components/ui/AsyncSelect'
3
- import { getGraphLabels } from '@/api/lightrag'
4
  import { useSettingsStore } from '@/stores/settings'
5
  import { useGraphStore } from '@/stores/graph'
6
  import { labelListLimit } from '@/lib/constants'
7
  import MiniSearch from 'minisearch'
8
  import { useTranslation } from 'react-i18next'
9
 
10
- const lastGraph: any = {
11
- graph: null,
12
- searchEngine: null,
13
- labels: []
14
- }
15
-
16
  const GraphLabels = () => {
17
  const { t } = useTranslation()
18
  const label = useSettingsStore.use.queryLabel()
19
- const graph = useGraphStore.use.sigmaGraph()
 
20
 
21
- const getSearchEngine = useCallback(async () => {
22
- if (lastGraph.graph == graph) {
23
- return {
24
- labels: lastGraph.labels,
25
- searchEngine: lastGraph.searchEngine
26
- }
27
- }
28
- const labels = ['*'].concat(await getGraphLabels())
29
 
30
- // Ensure query label exists
31
- if (!labels.includes(useSettingsStore.getState().queryLabel)) {
32
- useSettingsStore.getState().setQueryLabel(labels[0])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
 
34
 
 
35
  // Create search engine
36
  const searchEngine = new MiniSearch({
37
  idField: 'id',
@@ -46,41 +57,32 @@ const GraphLabels = () => {
46
  })
47
 
48
  // Add documents
49
- const documents = labels.map((str, index) => ({ id: index, value: str }))
50
  searchEngine.addAll(documents)
51
 
52
- lastGraph.graph = graph
53
- lastGraph.searchEngine = searchEngine
54
- lastGraph.labels = labels
55
-
56
  return {
57
- labels,
58
  searchEngine
59
  }
60
- }, [graph])
61
 
62
  const fetchData = useCallback(
63
  async (query?: string): Promise<string[]> => {
64
- const { labels, searchEngine } = await getSearchEngine()
65
 
66
  let result: string[] = labels
67
  if (query) {
68
  // Search labels
69
- result = searchEngine.search(query).map((r) => labels[r.id])
70
  }
71
 
72
  return result.length <= labelListLimit
73
  ? result
74
- : [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
75
  },
76
  [getSearchEngine]
77
  )
78
 
79
- const setQueryLabel = useCallback((label: string) => {
80
- if (label.startsWith('And ') && label.endsWith(' others')) return
81
- useSettingsStore.getState().setQueryLabel(label)
82
- }, [])
83
-
84
  return (
85
  <AsyncSelect<string>
86
  className="ml-2"
@@ -94,8 +96,38 @@ const GraphLabels = () => {
94
  notFound={<div className="py-6 text-center text-sm">No labels found</div>}
95
  label={t('graphPanel.graphLabels.label')}
96
  placeholder={t('graphPanel.graphLabels.placeholder')}
97
- value={label !== null ? label : ''}
98
- onChange={setQueryLabel}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  />
100
  )
101
  }
 
1
+ import { useCallback, useEffect, useRef } from 'react'
2
  import { AsyncSelect } from '@/components/ui/AsyncSelect'
 
3
  import { useSettingsStore } from '@/stores/settings'
4
  import { useGraphStore } from '@/stores/graph'
5
  import { labelListLimit } from '@/lib/constants'
6
  import MiniSearch from 'minisearch'
7
  import { useTranslation } from 'react-i18next'
8
 
 
 
 
 
 
 
9
  const GraphLabels = () => {
10
  const { t } = useTranslation()
11
  const label = useSettingsStore.use.queryLabel()
12
+ const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
13
+ const labelsLoadedRef = useRef(false)
14
 
15
+ // Track if a fetch is in progress to prevent multiple simultaneous fetches
16
+ const fetchInProgressRef = useRef(false)
17
+
18
+ // Fetch labels once on component mount, using global flag to prevent duplicates
19
+ useEffect(() => {
20
+ // Check if we've already attempted to fetch labels in this session
21
+ const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
 
22
 
23
+ // Only fetch if we haven't attempted in this session and no fetch is in progress
24
+ if (!labelsFetchAttempted && !fetchInProgressRef.current) {
25
+ fetchInProgressRef.current = true
26
+ // Set global flag to indicate we've attempted to fetch in this session
27
+ useGraphStore.getState().setLabelsFetchAttempted(true)
28
+
29
+ console.log('Fetching graph labels (once per session)...')
30
+
31
+ useGraphStore.getState().fetchAllDatabaseLabels()
32
+ .then(() => {
33
+ labelsLoadedRef.current = true
34
+ fetchInProgressRef.current = false
35
+ })
36
+ .catch((error) => {
37
+ console.error('Failed to fetch labels:', error)
38
+ fetchInProgressRef.current = false
39
+ // Reset global flag to allow retry
40
+ useGraphStore.getState().setLabelsFetchAttempted(false)
41
+ })
42
  }
43
+ }, []) // Empty dependency array ensures this only runs once on mount
44
 
45
+ const getSearchEngine = useCallback(() => {
46
  // Create search engine
47
  const searchEngine = new MiniSearch({
48
  idField: 'id',
 
57
  })
58
 
59
  // Add documents
60
+ const documents = allDatabaseLabels.map((str, index) => ({ id: index, value: str }))
61
  searchEngine.addAll(documents)
62
 
 
 
 
 
63
  return {
64
+ labels: allDatabaseLabels,
65
  searchEngine
66
  }
67
+ }, [allDatabaseLabels])
68
 
69
  const fetchData = useCallback(
70
  async (query?: string): Promise<string[]> => {
71
+ const { labels, searchEngine } = getSearchEngine()
72
 
73
  let result: string[] = labels
74
  if (query) {
75
  // Search labels
76
+ result = searchEngine.search(query).map((r: { id: number }) => labels[r.id])
77
  }
78
 
79
  return result.length <= labelListLimit
80
  ? result
81
+ : [...result.slice(0, labelListLimit), '...']
82
  },
83
  [getSearchEngine]
84
  )
85
 
 
 
 
 
 
86
  return (
87
  <AsyncSelect<string>
88
  className="ml-2"
 
96
  notFound={<div className="py-6 text-center text-sm">No labels found</div>}
97
  label={t('graphPanel.graphLabels.label')}
98
  placeholder={t('graphPanel.graphLabels.placeholder')}
99
+ value={label !== null ? label : '*'}
100
+ onChange={(newLabel) => {
101
+ const currentLabel = useSettingsStore.getState().queryLabel
102
+
103
+ // select the last item means query all
104
+ if (newLabel === '...') {
105
+ newLabel = '*'
106
+ }
107
+
108
+ // Reset the fetch attempted flag to force a new data fetch
109
+ useGraphStore.getState().setGraphDataFetchAttempted(false)
110
+
111
+ // Clear current graph data to ensure complete reload when label changes
112
+ if (newLabel !== currentLabel) {
113
+ const graphStore = useGraphStore.getState();
114
+ graphStore.clearSelection();
115
+
116
+ // Reset the graph state but preserve the instance
117
+ if (graphStore.sigmaGraph) {
118
+ const nodes = Array.from(graphStore.sigmaGraph.nodes());
119
+ nodes.forEach(node => graphStore.sigmaGraph?.dropNode(node));
120
+ }
121
+ }
122
+
123
+ if (newLabel === currentLabel && newLabel !== '*') {
124
+ // reselect the same itme means qery all
125
+ useSettingsStore.getState().setQueryLabel('*')
126
+ } else {
127
+ useSettingsStore.getState().setQueryLabel(newLabel)
128
+ }
129
+ }}
130
+ clearable={false} // Prevent clearing value on reselect
131
  />
132
  )
133
  }
lightrag_webui/src/components/graph/GraphSearch.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { FC, useCallback, useMemo } from 'react'
2
  import {
3
  EdgeById,
4
  NodeById,
@@ -28,6 +28,7 @@ function OptionComponent(item: OptionItem) {
28
  }
29
 
30
  const messageId = '__message_item'
 
31
  const lastGraph: any = {
32
  graph: null,
33
  searchEngine: null
@@ -48,6 +49,15 @@ export const GraphSearchInput = ({
48
  const { t } = useTranslation()
49
  const graph = useGraphStore.use.sigmaGraph()
50
 
 
 
 
 
 
 
 
 
 
51
  const searchEngine = useMemo(() => {
52
  if (lastGraph.graph == graph) {
53
  return lastGraph.searchEngine
@@ -85,8 +95,19 @@ export const GraphSearchInput = ({
85
  const loadOptions = useCallback(
86
  async (query?: string): Promise<OptionItem[]> => {
87
  if (onFocus) onFocus(null)
88
- if (!query || !searchEngine) return []
89
- const result: OptionItem[] = searchEngine.search(query).map((r) => ({
 
 
 
 
 
 
 
 
 
 
 
90
  id: r.id,
91
  type: 'nodes'
92
  }))
@@ -103,7 +124,7 @@ export const GraphSearchInput = ({
103
  }
104
  ]
105
  },
106
- [searchEngine, onFocus]
107
  )
108
 
109
  return (
 
1
+ import { FC, useCallback, useEffect, useMemo } from 'react'
2
  import {
3
  EdgeById,
4
  NodeById,
 
28
  }
29
 
30
  const messageId = '__message_item'
31
+ // Reset this cache when graph changes to ensure fresh search results
32
  const lastGraph: any = {
33
  graph: null,
34
  searchEngine: null
 
49
  const { t } = useTranslation()
50
  const graph = useGraphStore.use.sigmaGraph()
51
 
52
+ // Force reset the cache when graph changes
53
+ useEffect(() => {
54
+ if (graph) {
55
+ // Reset cache to ensure fresh search results with new graph data
56
+ lastGraph.graph = null;
57
+ lastGraph.searchEngine = null;
58
+ }
59
+ }, [graph]);
60
+
61
  const searchEngine = useMemo(() => {
62
  if (lastGraph.graph == graph) {
63
  return lastGraph.searchEngine
 
95
  const loadOptions = useCallback(
96
  async (query?: string): Promise<OptionItem[]> => {
97
  if (onFocus) onFocus(null)
98
+ if (!graph || !searchEngine) return []
99
+
100
+ // If no query, return first searchResultLimit nodes
101
+ if (!query) {
102
+ const nodeIds = graph.nodes().slice(0, searchResultLimit)
103
+ return nodeIds.map(id => ({
104
+ id,
105
+ type: 'nodes'
106
+ }))
107
+ }
108
+
109
+ // If has query, search nodes
110
+ const result: OptionItem[] = searchEngine.search(query).map((r: { id: string }) => ({
111
  id: r.id,
112
  type: 'nodes'
113
  }))
 
124
  }
125
  ]
126
  },
127
+ [graph, searchEngine, onFocus, t]
128
  )
129
 
130
  return (
lightrag_webui/src/components/graph/PropertiesView.tsx CHANGED
@@ -96,9 +96,9 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
96
  const neighbour = state.rawGraph.getNode(neighbourId)
97
  if (neighbour) {
98
  relationships.push({
99
- type: isTarget ? 'Target' : 'Source',
100
  id: neighbourId,
101
- label: neighbour.labels.join(', ')
102
  })
103
  }
104
  }
@@ -132,14 +132,22 @@ const PropertyRow = ({
132
  onClick?: () => void
133
  tooltip?: string
134
  }) => {
 
 
 
 
 
 
 
 
135
  return (
136
  <div className="flex items-center gap-2">
137
- <label className="text-primary/60 tracking-wide">{name}</label>:
138
  <Text
139
- className="hover:bg-primary/20 rounded p-1 text-ellipsis"
140
  tooltipClassName="max-w-80"
141
  text={value}
142
- tooltip={tooltip || value}
143
  side="left"
144
  onClick={onClick}
145
  />
@@ -174,7 +182,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
174
  {node.relationships.length > 0 && (
175
  <>
176
  <label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
177
- {t('graphPanel.propertiesView.node.relationships')}
178
  </label>
179
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
180
  {node.relationships.map(({ type, id, label }) => {
 
96
  const neighbour = state.rawGraph.getNode(neighbourId)
97
  if (neighbour) {
98
  relationships.push({
99
+ type: 'Neighbour',
100
  id: neighbourId,
101
+ label: neighbour.properties['entity_id'] ? neighbour.properties['entity_id'] : neighbour.labels.join(', ')
102
  })
103
  }
104
  }
 
132
  onClick?: () => void
133
  tooltip?: string
134
  }) => {
135
+ const { t } = useTranslation()
136
+
137
+ const getPropertyNameTranslation = (name: string) => {
138
+ const translationKey = `graphPanel.propertiesView.node.propertyNames.${name}`
139
+ const translation = t(translationKey)
140
+ return translation === translationKey ? name : translation
141
+ }
142
+
143
  return (
144
  <div className="flex items-center gap-2">
145
+ <label className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</label>:
146
  <Text
147
+ className="hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis"
148
  tooltipClassName="max-w-80"
149
  text={value}
150
+ tooltip={tooltip || (typeof value === 'string' ? value : JSON.stringify(value, null, 2))}
151
  side="left"
152
  onClick={onClick}
153
  />
 
182
  {node.relationships.length > 0 && (
183
  <>
184
  <label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
185
+ {t('graphPanel.propertiesView.node.relationships')}
186
  </label>
187
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
188
  {node.relationships.map(({ type, id, label }) => {
lightrag_webui/src/components/graph/Settings.tsx CHANGED
@@ -8,9 +8,10 @@ import Input from '@/components/ui/Input'
8
  import { controlButtonVariant } from '@/lib/constants'
9
  import { useSettingsStore } from '@/stores/settings'
10
  import { useBackendState } from '@/stores/state'
 
11
 
12
- import { SettingsIcon } from 'lucide-react'
13
- import { useTranslation } from "react-i18next";
14
 
15
  /**
16
  * Component that displays a checkbox with a label.
@@ -114,6 +115,7 @@ const LabeledNumberInput = ({
114
  export default function Settings() {
115
  const [opened, setOpened] = useState<boolean>(false)
116
  const [tempApiKey, setTempApiKey] = useState<string>('')
 
117
 
118
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
119
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
@@ -208,116 +210,126 @@ export default function Settings() {
208
  const { t } = useTranslation();
209
 
210
  return (
211
- <Popover open={opened} onOpenChange={setOpened}>
212
- <PopoverTrigger asChild>
213
- <Button variant={controlButtonVariant} tooltip={t("graphPanel.sideBar.settings.settings")} size="icon">
214
- <SettingsIcon />
215
- </Button>
216
- </PopoverTrigger>
217
- <PopoverContent
218
- side="right"
219
- align="start"
220
- className="mb-2 p-2"
221
- onCloseAutoFocus={(e) => e.preventDefault()}
222
  >
223
- <div className="flex flex-col gap-2">
224
- <LabeledCheckBox
225
- checked={enableHealthCheck}
226
- onCheckedChange={setEnableHealthCheck}
227
- label={t("graphPanel.sideBar.settings.healthCheck")}
228
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
- <Separator />
231
 
232
- <LabeledCheckBox
233
- checked={showPropertyPanel}
234
- onCheckedChange={setShowPropertyPanel}
235
- label={t("graphPanel.sideBar.settings.showPropertyPanel")}
236
- />
237
- <LabeledCheckBox
238
- checked={showNodeSearchBar}
239
- onCheckedChange={setShowNodeSearchBar}
240
- label={t("graphPanel.sideBar.settings.showSearchBar")}
241
- />
242
 
243
- <Separator />
244
 
245
- <LabeledCheckBox
246
- checked={showNodeLabel}
247
- onCheckedChange={setShowNodeLabel}
248
- label={t("graphPanel.sideBar.settings.showNodeLabel")}
249
- />
250
- <LabeledCheckBox
251
- checked={enableNodeDrag}
252
- onCheckedChange={setEnableNodeDrag}
253
- label={t("graphPanel.sideBar.settings.nodeDraggable")}
254
- />
255
 
256
- <Separator />
257
 
258
- <LabeledCheckBox
259
- checked={showEdgeLabel}
260
- onCheckedChange={setShowEdgeLabel}
261
- label={t("graphPanel.sideBar.settings.showEdgeLabel")}
262
- />
263
- <LabeledCheckBox
264
- checked={enableHideUnselectedEdges}
265
- onCheckedChange={setEnableHideUnselectedEdges}
266
- label={t("graphPanel.sideBar.settings.hideUnselectedEdges")}
267
- />
268
- <LabeledCheckBox
269
- checked={enableEdgeEvents}
270
- onCheckedChange={setEnableEdgeEvents}
271
- label={t("graphPanel.sideBar.settings.edgeEvents")}
272
- />
273
 
274
- <Separator />
275
- <LabeledNumberInput
276
- label={t("graphPanel.sideBar.settings.maxQueryDepth")}
277
- min={1}
278
- value={graphQueryMaxDepth}
279
- onEditFinished={setGraphQueryMaxDepth}
280
- />
281
- <LabeledNumberInput
282
- label={t("graphPanel.sideBar.settings.minDegree")}
283
- min={0}
284
- value={graphMinDegree}
285
- onEditFinished={setGraphMinDegree}
286
- />
287
- <LabeledNumberInput
288
- label={t("graphPanel.sideBar.settings.maxLayoutIterations")}
289
- min={1}
290
- max={20}
291
- value={graphLayoutMaxIterations}
292
- onEditFinished={setGraphLayoutMaxIterations}
293
- />
294
- <Separator />
295
 
296
- <div className="flex flex-col gap-2">
297
- <label className="text-sm font-medium">{t("graphPanel.sideBar.settings.apiKey")}</label>
298
- <form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
299
- <div className="w-0 flex-1">
300
- <Input
301
- type="password"
302
- value={tempApiKey}
303
- onChange={handleTempApiKeyChange}
304
- placeholder={t("graphPanel.sideBar.settings.enterYourAPIkey")}
305
- className="max-h-full w-full min-w-0"
306
- autoComplete="off"
307
- />
308
- </div>
309
- <Button
310
- onClick={setApiKey}
311
- variant="outline"
312
- size="sm"
313
- className="max-h-full shrink-0"
314
- >
315
- {t("graphPanel.sideBar.settings.save")}
316
- </Button>
317
- </form>
 
318
  </div>
319
- </div>
320
- </PopoverContent>
321
- </Popover>
322
  )
323
  }
 
8
  import { controlButtonVariant } from '@/lib/constants'
9
  import { useSettingsStore } from '@/stores/settings'
10
  import { useBackendState } from '@/stores/state'
11
+ import { useGraphStore } from '@/stores/graph'
12
 
13
+ import { SettingsIcon, RefreshCwIcon } from 'lucide-react'
14
+ import { useTranslation } from 'react-i18next';
15
 
16
  /**
17
  * Component that displays a checkbox with a label.
 
115
  export default function Settings() {
116
  const [opened, setOpened] = useState<boolean>(false)
117
  const [tempApiKey, setTempApiKey] = useState<string>('')
118
+ const refreshLayout = useGraphStore.use.refreshLayout()
119
 
120
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
121
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
 
210
  const { t } = useTranslation();
211
 
212
  return (
213
+ <>
214
+ <Button
215
+ variant={controlButtonVariant}
216
+ tooltip={t('graphPanel.sideBar.settings.refreshLayout')}
217
+ size="icon"
218
+ onClick={refreshLayout}
 
 
 
 
 
219
  >
220
+ <RefreshCwIcon />
221
+ </Button>
222
+ <Popover open={opened} onOpenChange={setOpened}>
223
+ <PopoverTrigger asChild>
224
+ <Button variant={controlButtonVariant} tooltip={t('graphPanel.sideBar.settings.settings')} size="icon">
225
+ <SettingsIcon />
226
+ </Button>
227
+ </PopoverTrigger>
228
+ <PopoverContent
229
+ side="right"
230
+ align="start"
231
+ className="mb-2 p-2"
232
+ onCloseAutoFocus={(e) => e.preventDefault()}
233
+ >
234
+ <div className="flex flex-col gap-2">
235
+ <LabeledCheckBox
236
+ checked={enableHealthCheck}
237
+ onCheckedChange={setEnableHealthCheck}
238
+ label={t('graphPanel.sideBar.settings.healthCheck')}
239
+ />
240
 
241
+ <Separator />
242
 
243
+ <LabeledCheckBox
244
+ checked={showPropertyPanel}
245
+ onCheckedChange={setShowPropertyPanel}
246
+ label={t('graphPanel.sideBar.settings.showPropertyPanel')}
247
+ />
248
+ <LabeledCheckBox
249
+ checked={showNodeSearchBar}
250
+ onCheckedChange={setShowNodeSearchBar}
251
+ label={t('graphPanel.sideBar.settings.showSearchBar')}
252
+ />
253
 
254
+ <Separator />
255
 
256
+ <LabeledCheckBox
257
+ checked={showNodeLabel}
258
+ onCheckedChange={setShowNodeLabel}
259
+ label={t('graphPanel.sideBar.settings.showNodeLabel')}
260
+ />
261
+ <LabeledCheckBox
262
+ checked={enableNodeDrag}
263
+ onCheckedChange={setEnableNodeDrag}
264
+ label={t('graphPanel.sideBar.settings.nodeDraggable')}
265
+ />
266
 
267
+ <Separator />
268
 
269
+ <LabeledCheckBox
270
+ checked={showEdgeLabel}
271
+ onCheckedChange={setShowEdgeLabel}
272
+ label={t('graphPanel.sideBar.settings.showEdgeLabel')}
273
+ />
274
+ <LabeledCheckBox
275
+ checked={enableHideUnselectedEdges}
276
+ onCheckedChange={setEnableHideUnselectedEdges}
277
+ label={t('graphPanel.sideBar.settings.hideUnselectedEdges')}
278
+ />
279
+ <LabeledCheckBox
280
+ checked={enableEdgeEvents}
281
+ onCheckedChange={setEnableEdgeEvents}
282
+ label={t('graphPanel.sideBar.settings.edgeEvents')}
283
+ />
284
 
285
+ <Separator />
286
+ <LabeledNumberInput
287
+ label={t('graphPanel.sideBar.settings.maxQueryDepth')}
288
+ min={1}
289
+ value={graphQueryMaxDepth}
290
+ onEditFinished={setGraphQueryMaxDepth}
291
+ />
292
+ <LabeledNumberInput
293
+ label={t('graphPanel.sideBar.settings.minDegree')}
294
+ min={0}
295
+ value={graphMinDegree}
296
+ onEditFinished={setGraphMinDegree}
297
+ />
298
+ <LabeledNumberInput
299
+ label={t('graphPanel.sideBar.settings.maxLayoutIterations')}
300
+ min={1}
301
+ max={30}
302
+ value={graphLayoutMaxIterations}
303
+ onEditFinished={setGraphLayoutMaxIterations}
304
+ />
305
+ <Separator />
306
 
307
+ <div className="flex flex-col gap-2">
308
+ <label className="text-sm font-medium">{t('graphPanel.sideBar.settings.apiKey')}</label>
309
+ <form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
310
+ <div className="w-0 flex-1">
311
+ <Input
312
+ type="password"
313
+ value={tempApiKey}
314
+ onChange={handleTempApiKeyChange}
315
+ placeholder={t('graphPanel.sideBar.settings.enterYourAPIkey')}
316
+ className="max-h-full w-full min-w-0"
317
+ autoComplete="off"
318
+ />
319
+ </div>
320
+ <Button
321
+ onClick={setApiKey}
322
+ variant="outline"
323
+ size="sm"
324
+ className="max-h-full shrink-0"
325
+ >
326
+ {t('graphPanel.sideBar.settings.save')}
327
+ </Button>
328
+ </form>
329
+ </div>
330
  </div>
331
+ </PopoverContent>
332
+ </Popover>
333
+ </>
334
  )
335
  }
lightrag_webui/src/components/graph/SettingsDisplay.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useSettingsStore } from '@/stores/settings'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ /**
5
+ * Component that displays current values of important graph settings
6
+ * Positioned to the right of the toolbar at the bottom-left corner
7
+ */
8
+ const SettingsDisplay = () => {
9
+ const { t } = useTranslation()
10
+ const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth()
11
+ const graphMinDegree = useSettingsStore.use.graphMinDegree()
12
+
13
+ return (
14
+ <div className="absolute bottom-2 left-[calc(2rem+2.5rem)] flex items-center gap-2 text-xs text-gray-400">
15
+ <div>{t('graphPanel.sideBar.settings.depth')}: {graphQueryMaxDepth}</div>
16
+ <div>{t('graphPanel.sideBar.settings.degree')}: {graphMinDegree}</div>
17
+ </div>
18
+ )
19
+ }
20
+
21
+ export default SettingsDisplay
lightrag_webui/src/components/retrieval/QuerySettings.tsx CHANGED
@@ -25,7 +25,7 @@ export default function QuerySettings() {
25
  }, [])
26
 
27
  return (
28
- <Card className="flex shrink-0 flex-col">
29
  <CardHeader className="px-4 pt-4 pb-2">
30
  <CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
31
  <CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
 
25
  }, [])
26
 
27
  return (
28
+ <Card className="flex shrink-0 flex-col min-w-[180px]">
29
  <CardHeader className="px-4 pt-4 pb-2">
30
  <CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
31
  <CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
lightrag_webui/src/components/ui/AsyncSearch.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback } from 'react'
2
  import { Loader2 } from 'lucide-react'
3
  import { useDebounce } from '@/hooks/useDebounce'
4
 
@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
193
  </div>
194
  )}
195
  </div>
196
- <CommandList hidden={!open || debouncedSearchTerm.length === 0}>
197
  {error && <div className="text-destructive p-4 text-center">{error}</div>}
198
  {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
199
  {!loading &&
@@ -204,7 +204,7 @@ export function AsyncSearch<T>({
204
  ))}
205
  <CommandGroup>
206
  {options.map((option, idx) => (
207
- <>
208
  <CommandItem
209
  key={getOptionValue(option) + `${idx}`}
210
  value={getOptionValue(option)}
@@ -215,9 +215,9 @@ export function AsyncSearch<T>({
215
  {renderOption(option)}
216
  </CommandItem>
217
  {idx !== options.length - 1 && (
218
- <div key={idx} className="bg-foreground/10 h-[1px]" />
219
  )}
220
- </>
221
  ))}
222
  </CommandGroup>
223
  </CommandList>
 
1
+ import React, { useState, useEffect, useCallback } from 'react'
2
  import { Loader2 } from 'lucide-react'
3
  import { useDebounce } from '@/hooks/useDebounce'
4
 
 
193
  </div>
194
  )}
195
  </div>
196
+ <CommandList hidden={!open}>
197
  {error && <div className="text-destructive p-4 text-center">{error}</div>}
198
  {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
199
  {!loading &&
 
204
  ))}
205
  <CommandGroup>
206
  {options.map((option, idx) => (
207
+ <React.Fragment key={getOptionValue(option) + `-fragment-${idx}`}>
208
  <CommandItem
209
  key={getOptionValue(option) + `${idx}`}
210
  value={getOptionValue(option)}
 
215
  {renderOption(option)}
216
  </CommandItem>
217
  {idx !== options.length - 1 && (
218
+ <div key={`divider-${idx}`} className="bg-foreground/10 h-[1px]" />
219
  )}
220
+ </React.Fragment>
221
  ))}
222
  </CommandGroup>
223
  </CommandList>
lightrag_webui/src/components/ui/TabContent.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from 'react';
2
+ import { useTabVisibility } from '@/contexts/useTabVisibility';
3
+
4
+ interface TabContentProps {
5
+ tabId: string;
6
+ children: React.ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * TabContent component that manages visibility based on tab selection
12
+ * Works with the TabVisibilityContext to show/hide content based on active tab
13
+ */
14
+ const TabContent: React.FC<TabContentProps> = ({ tabId, children, className = '' }) => {
15
+ const { isTabVisible, setTabVisibility } = useTabVisibility();
16
+ const isVisible = isTabVisible(tabId);
17
+
18
+ // Register this tab with the context when mounted
19
+ useEffect(() => {
20
+ setTabVisibility(tabId, true);
21
+
22
+ // Cleanup when unmounted
23
+ return () => {
24
+ setTabVisibility(tabId, false);
25
+ };
26
+ }, [tabId, setTabVisibility]);
27
+
28
+ // Use CSS to hide content instead of not rendering it
29
+ // This prevents components from unmounting when tabs are switched
30
+ return (
31
+ <div className={`${className} ${isVisible ? '' : 'hidden'}`}>
32
+ {children}
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export default TabContent;
lightrag_webui/src/components/ui/Tabs.tsx CHANGED
@@ -42,9 +42,13 @@ const TabsContent = React.forwardRef<
42
  <TabsPrimitive.Content
43
  ref={ref}
44
  className={cn(
45
- 'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
 
 
46
  className
47
  )}
 
 
48
  {...props}
49
  />
50
  ))
 
42
  <TabsPrimitive.Content
43
  ref={ref}
44
  className={cn(
45
+ 'ring-offset-background focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
46
+ 'data-[state=inactive]:invisible data-[state=active]:visible',
47
+ 'h-full w-full',
48
  className
49
  )}
50
+ // Force mounting of inactive tabs to preserve WebGL contexts
51
+ forceMount
52
  {...props}
53
  />
54
  ))
lightrag_webui/src/components/ui/Tooltip.tsx CHANGED
@@ -10,30 +10,43 @@ const TooltipTrigger = TooltipPrimitive.Trigger
10
 
11
  const processTooltipContent = (content: string) => {
12
  if (typeof content !== 'string') return content
13
- return content.split('\\n').map((line, i) => (
14
- <React.Fragment key={i}>
15
- {line}
16
- {i < content.split('\\n').length - 1 && <br />}
17
- </React.Fragment>
18
- ))
19
  }
20
 
21
  const TooltipContent = React.forwardRef<
22
  React.ComponentRef<typeof TooltipPrimitive.Content>,
23
- React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
24
- >(({ className, sideOffset = 4, children, ...props }, ref) => (
25
- <TooltipPrimitive.Content
26
- ref={ref}
27
- sideOffset={sideOffset}
28
- className={cn(
29
- 'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1 max-w-sm overflow-hidden rounded-md border px-3 py-2 text-sm shadow-md',
30
- className
31
- )}
32
- {...props}
33
- >
34
- {typeof children === 'string' ? processTooltipContent(children) : children}
35
- </TooltipPrimitive.Content>
36
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  TooltipContent.displayName = TooltipPrimitive.Content.displayName
38
 
39
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
 
10
 
11
  const processTooltipContent = (content: string) => {
12
  if (typeof content !== 'string') return content
13
+ return (
14
+ <div className="relative top-0 pt-1 whitespace-pre-wrap break-words">
15
+ {content}
16
+ </div>
17
+ )
 
18
  }
19
 
20
  const TooltipContent = React.forwardRef<
21
  React.ComponentRef<typeof TooltipPrimitive.Content>,
22
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
23
+ side?: 'top' | 'right' | 'bottom' | 'left'
24
+ align?: 'start' | 'center' | 'end'
25
+ }
26
+ >(({ className, side = 'left', align = 'start', children, ...props }, ref) => {
27
+ const contentRef = React.useRef<HTMLDivElement>(null);
28
+
29
+ React.useEffect(() => {
30
+ if (contentRef.current) {
31
+ contentRef.current.scrollTop = 0;
32
+ }
33
+ }, [children]);
34
+
35
+ return (
36
+ <TooltipPrimitive.Content
37
+ ref={ref}
38
+ side={side}
39
+ align={align}
40
+ className={cn(
41
+ 'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-[60vh] overflow-y-auto whitespace-pre-wrap break-words rounded-md border px-3 py-2 text-sm shadow-md',
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ {typeof children === 'string' ? processTooltipContent(children) : children}
47
+ </TooltipPrimitive.Content>
48
+ );
49
+ })
50
  TooltipContent.displayName = TooltipPrimitive.Content.displayName
51
 
52
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
lightrag_webui/src/contexts/TabVisibilityProvider.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { TabVisibilityContext } from './context';
3
+ import { TabVisibilityContextType } from './types';
4
+ import { useSettingsStore } from '@/stores/settings';
5
+
6
+ interface TabVisibilityProviderProps {
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ /**
11
+ * Provider component for the TabVisibility context
12
+ * Manages the visibility state of tabs throughout the application
13
+ */
14
+ export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ children }) => {
15
+ // Get current tab from settings store
16
+ const currentTab = useSettingsStore.use.currentTab();
17
+
18
+ // Initialize visibility state with current tab as visible
19
+ const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
20
+ [currentTab]: true
21
+ }));
22
+
23
+ // Update visibility when current tab changes
24
+ useEffect(() => {
25
+ setVisibleTabs((prev) => ({
26
+ ...prev,
27
+ [currentTab]: true
28
+ }));
29
+ }, [currentTab]);
30
+
31
+ // Create the context value with memoization to prevent unnecessary re-renders
32
+ const contextValue = useMemo<TabVisibilityContextType>(
33
+ () => ({
34
+ visibleTabs,
35
+ setTabVisibility: (tabId: string, isVisible: boolean) => {
36
+ setVisibleTabs((prev) => ({
37
+ ...prev,
38
+ [tabId]: isVisible,
39
+ }));
40
+ },
41
+ isTabVisible: (tabId: string) => !!visibleTabs[tabId],
42
+ }),
43
+ [visibleTabs]
44
+ );
45
+
46
+ return (
47
+ <TabVisibilityContext.Provider value={contextValue}>
48
+ {children}
49
+ </TabVisibilityContext.Provider>
50
+ );
51
+ };
52
+
53
+ export default TabVisibilityProvider;
lightrag_webui/src/contexts/context.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext } from 'react';
2
+ import { TabVisibilityContextType } from './types';
3
+
4
+ // Default context value
5
+ const defaultContext: TabVisibilityContextType = {
6
+ visibleTabs: {},
7
+ setTabVisibility: () => {},
8
+ isTabVisible: () => false,
9
+ };
10
+
11
+ // Create the context
12
+ export const TabVisibilityContext = createContext<TabVisibilityContextType>(defaultContext);
lightrag_webui/src/contexts/types.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export interface TabVisibilityContextType {
2
+ visibleTabs: Record<string, boolean>;
3
+ setTabVisibility: (tabId: string, isVisible: boolean) => void;
4
+ isTabVisible: (tabId: string) => boolean;
5
+ }
lightrag_webui/src/contexts/useTabVisibility.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useContext } from 'react';
2
+ import { TabVisibilityContext } from './context';
3
+ import { TabVisibilityContextType } from './types';
4
+
5
+ /**
6
+ * Custom hook to access the tab visibility context
7
+ * @returns The tab visibility context
8
+ */
9
+ export const useTabVisibility = (): TabVisibilityContextType => {
10
+ const context = useContext(TabVisibilityContext);
11
+
12
+ if (!context) {
13
+ throw new Error('useTabVisibility must be used within a TabVisibilityProvider');
14
+ }
15
+
16
+ return context;
17
+ };
lightrag_webui/src/features/ApiSite.tsx CHANGED
@@ -1,5 +1,40 @@
 
 
1
  import { backendBaseUrl } from '@/lib/constants'
 
2
 
3
  export default function ApiSite() {
4
- return <iframe src={backendBaseUrl + '/docs'} className="size-full" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  }
 
1
+ import { useState, useEffect } from 'react'
2
+ import { useTabVisibility } from '@/contexts/useTabVisibility'
3
  import { backendBaseUrl } from '@/lib/constants'
4
+ import { useTranslation } from 'react-i18next'
5
 
6
  export default function ApiSite() {
7
+ const { t } = useTranslation()
8
+ const { isTabVisible } = useTabVisibility()
9
+ const isApiTabVisible = isTabVisible('api')
10
+ const [iframeLoaded, setIframeLoaded] = useState(false)
11
+
12
+ // Load the iframe once on component mount
13
+ useEffect(() => {
14
+ if (!iframeLoaded) {
15
+ setIframeLoaded(true)
16
+ }
17
+ }, [iframeLoaded])
18
+
19
+ // Use CSS to hide content when tab is not visible
20
+ return (
21
+ <div className={`size-full ${isApiTabVisible ? '' : 'hidden'}`}>
22
+ {iframeLoaded ? (
23
+ <iframe
24
+ src={backendBaseUrl + '/docs'}
25
+ className="size-full w-full h-full"
26
+ style={{ width: '100%', height: '100%', border: 'none' }}
27
+ // Use key to ensure iframe doesn't reload
28
+ key="api-docs-iframe"
29
+ />
30
+ ) : (
31
+ <div className="flex h-full w-full items-center justify-center bg-background">
32
+ <div className="text-center">
33
+ <div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
34
+ <p>{t('apiSite.loading')}</p>
35
+ </div>
36
+ </div>
37
+ )}
38
+ </div>
39
+ )
40
  }
lightrag_webui/src/features/DocumentManager.tsx CHANGED
@@ -1,5 +1,6 @@
1
- import { useState, useEffect, useCallback } from 'react'
2
  import { useTranslation } from 'react-i18next'
 
3
  import Button from '@/components/ui/Button'
4
  import {
5
  Table,
@@ -26,6 +27,9 @@ export default function DocumentManager() {
26
  const { t } = useTranslation()
27
  const health = useBackendState.use.health()
28
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
 
 
 
29
 
30
  const fetchDocuments = useCallback(async () => {
31
  try {
@@ -48,11 +52,15 @@ export default function DocumentManager() {
48
  } catch (err) {
49
  toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
50
  }
51
- }, [setDocs])
52
 
 
53
  useEffect(() => {
54
- fetchDocuments()
55
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
 
 
 
56
 
57
  const scanDocuments = useCallback(async () => {
58
  try {
@@ -61,21 +69,24 @@ export default function DocumentManager() {
61
  } catch (err) {
62
  toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
63
  }
64
- }, [])
65
 
 
66
  useEffect(() => {
 
 
 
 
67
  const interval = setInterval(async () => {
68
- if (!health) {
69
- return
70
- }
71
  try {
72
  await fetchDocuments()
73
  } catch (err) {
74
  toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
75
  }
76
  }, 5000)
 
77
  return () => clearInterval(interval)
78
- }, [health, fetchDocuments])
79
 
80
  return (
81
  <Card className="!size-full !rounded-none !border-none">
 
1
+ import { useState, useEffect, useCallback, useRef } from 'react'
2
  import { useTranslation } from 'react-i18next'
3
+ import { useTabVisibility } from '@/contexts/useTabVisibility'
4
  import Button from '@/components/ui/Button'
5
  import {
6
  Table,
 
27
  const { t } = useTranslation()
28
  const health = useBackendState.use.health()
29
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
30
+ const { isTabVisible } = useTabVisibility()
31
+ const isDocumentsTabVisible = isTabVisible('documents')
32
+ const initialLoadRef = useRef(false)
33
 
34
  const fetchDocuments = useCallback(async () => {
35
  try {
 
52
  } catch (err) {
53
  toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
54
  }
55
+ }, [setDocs, t])
56
 
57
+ // Only fetch documents when the tab becomes visible for the first time
58
  useEffect(() => {
59
+ if (isDocumentsTabVisible && !initialLoadRef.current) {
60
+ fetchDocuments()
61
+ initialLoadRef.current = true
62
+ }
63
+ }, [isDocumentsTabVisible, fetchDocuments])
64
 
65
  const scanDocuments = useCallback(async () => {
66
  try {
 
69
  } catch (err) {
70
  toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
71
  }
72
+ }, [t])
73
 
74
+ // Only set up polling when the tab is visible and health is good
75
  useEffect(() => {
76
+ if (!isDocumentsTabVisible || !health) {
77
+ return
78
+ }
79
+
80
  const interval = setInterval(async () => {
 
 
 
81
  try {
82
  await fetchDocuments()
83
  } catch (err) {
84
  toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
85
  }
86
  }, 5000)
87
+
88
  return () => clearInterval(interval)
89
+ }, [health, fetchDocuments, t, isDocumentsTabVisible])
90
 
91
  return (
92
  <Card className="!size-full !rounded-none !border-none">
lightrag_webui/src/features/GraphViewer.tsx CHANGED
@@ -1,4 +1,5 @@
1
- import { useEffect, useState, useCallback, useMemo } from 'react'
 
2
  // import { MiniMap } from '@react-sigma/minimap'
3
  import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
4
  import { Settings as SigmaSettings } from 'sigma/settings'
@@ -17,6 +18,7 @@ import Settings from '@/components/graph/Settings'
17
  import GraphSearch from '@/components/graph/GraphSearch'
18
  import GraphLabels from '@/components/graph/GraphLabels'
19
  import PropertiesView from '@/components/graph/PropertiesView'
 
20
 
21
  import { useSettingsStore } from '@/stores/settings'
22
  import { useGraphStore } from '@/stores/graph'
@@ -90,8 +92,12 @@ const GraphEvents = () => {
90
  }
91
  },
92
  // Disable the autoscale at the first down interaction
93
- mousedown: () => {
94
- if (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox())
 
 
 
 
95
  }
96
  })
97
  }, [registerEvents, sigma, draggedNode])
@@ -101,27 +107,46 @@ const GraphEvents = () => {
101
 
102
  const GraphViewer = () => {
103
  const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
 
 
104
 
105
  const selectedNode = useGraphStore.use.selectedNode()
106
  const focusedNode = useGraphStore.use.focusedNode()
107
  const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
 
 
 
 
 
 
108
 
109
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
110
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
111
- const renderLabels = useSettingsStore.use.showNodeLabel()
112
-
113
- const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
114
  const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
115
- const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
116
 
 
117
  useEffect(() => {
118
- setSigmaSettings({
119
- ...defaultSigmaSettings,
120
- enableEdgeEvents,
121
- renderEdgeLabels,
122
- renderLabels
123
- })
124
- }, [renderLabels, enableEdgeEvents, renderEdgeLabels])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
127
  if (value === null) useGraphStore.getState().setFocusedNode(null)
@@ -142,43 +167,73 @@ const GraphViewer = () => {
142
  [selectedNode]
143
  )
144
 
 
 
145
  return (
146
- <SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
147
- <GraphControl />
148
-
149
- {enableNodeDrag && <GraphEvents />}
150
-
151
- <FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
152
-
153
- <div className="absolute top-2 left-2 flex items-start gap-2">
154
- <GraphLabels />
155
- {showNodeSearchBar && (
156
- <GraphSearch
157
- value={searchInitSelectedNode}
158
- onFocus={onSearchFocus}
159
- onChange={onSearchSelect}
160
- />
161
- )}
162
- </div>
163
-
164
- <div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
165
- <Settings />
166
- <ZoomControl />
167
- <LayoutsControl />
168
- <FullScreenControl />
169
- {/* <ThemeToggle /> */}
170
- </div>
171
-
172
- {showPropertyPanel && (
173
- <div className="absolute top-2 right-2">
174
- <PropertiesView />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </div>
176
  )}
177
 
178
- {/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
179
- <MiniMap width="100px" height="100px" />
180
- </div> */}
181
- </SigmaContainer>
 
 
 
 
 
 
182
  )
183
  }
184
 
 
1
+ import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
2
+ import { useTabVisibility } from '@/contexts/useTabVisibility'
3
  // import { MiniMap } from '@react-sigma/minimap'
4
  import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
5
  import { Settings as SigmaSettings } from 'sigma/settings'
 
18
  import GraphSearch from '@/components/graph/GraphSearch'
19
  import GraphLabels from '@/components/graph/GraphLabels'
20
  import PropertiesView from '@/components/graph/PropertiesView'
21
+ import SettingsDisplay from '@/components/graph/SettingsDisplay'
22
 
23
  import { useSettingsStore } from '@/stores/settings'
24
  import { useGraphStore } from '@/stores/graph'
 
92
  }
93
  },
94
  // Disable the autoscale at the first down interaction
95
+ mousedown: (e) => {
96
+ // Only set custom BBox if it's a drag operation (mouse button is pressed)
97
+ const mouseEvent = e.original as MouseEvent;
98
+ if (mouseEvent.buttons !== 0 && !sigma.getCustomBBox()) {
99
+ sigma.setCustomBBox(sigma.getBBox())
100
+ }
101
  }
102
  })
103
  }, [registerEvents, sigma, draggedNode])
 
107
 
108
  const GraphViewer = () => {
109
  const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
110
+ const sigmaRef = useRef<any>(null)
111
+ const initAttemptedRef = useRef(false)
112
 
113
  const selectedNode = useGraphStore.use.selectedNode()
114
  const focusedNode = useGraphStore.use.focusedNode()
115
  const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
116
+ const isFetching = useGraphStore.use.isFetching()
117
+ const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
118
+
119
+ // Get tab visibility
120
+ const { isTabVisible } = useTabVisibility()
121
+ const isGraphTabVisible = isTabVisible('knowledge-graph')
122
 
123
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
124
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
 
 
 
125
  const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
 
126
 
127
+ // Handle component mount/unmount and tab visibility
128
  useEffect(() => {
129
+ // When component mounts or tab becomes visible
130
+ if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) {
131
+ // If tab is visible but graph is not rendering, try to enable rendering
132
+ useGraphStore.getState().setShouldRender(true)
133
+ initAttemptedRef.current = true
134
+ console.log('Graph viewer initialized')
135
+ }
136
+
137
+ // Cleanup function when component unmounts
138
+ return () => {
139
+ // Only log cleanup, don't actually clean up the WebGL context
140
+ // This allows the WebGL context to persist across tab switches
141
+ console.log('Graph viewer cleanup')
142
+ }
143
+ }, [isGraphTabVisible, shouldRender, isFetching])
144
+
145
+ // Initialize sigma settings once on component mount
146
+ // All dynamic settings will be updated in GraphControl using useSetSettings
147
+ useEffect(() => {
148
+ setSigmaSettings(defaultSigmaSettings)
149
+ }, [])
150
 
151
  const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
152
  if (value === null) useGraphStore.getState().setFocusedNode(null)
 
167
  [selectedNode]
168
  )
169
 
170
+ // Since TabsContent now forces mounting of all tabs, we need to conditionally render
171
+ // the SigmaContainer based on visibility to avoid unnecessary rendering
172
  return (
173
+ <div className="relative h-full w-full">
174
+ {/* Only render the SigmaContainer when the tab is visible */}
175
+ {isGraphTabVisible ? (
176
+ <SigmaContainer
177
+ settings={sigmaSettings}
178
+ className="!bg-background !size-full overflow-hidden"
179
+ ref={sigmaRef}
180
+ >
181
+ <GraphControl />
182
+
183
+ {enableNodeDrag && <GraphEvents />}
184
+
185
+ <FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
186
+
187
+ <div className="absolute top-2 left-2 flex items-start gap-2">
188
+ <GraphLabels />
189
+ {showNodeSearchBar && (
190
+ <GraphSearch
191
+ value={searchInitSelectedNode}
192
+ onFocus={onSearchFocus}
193
+ onChange={onSearchSelect}
194
+ />
195
+ )}
196
+ </div>
197
+
198
+ <div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
199
+ <Settings />
200
+ <ZoomControl />
201
+ <LayoutsControl />
202
+ <FullScreenControl />
203
+ {/* <ThemeToggle /> */}
204
+ </div>
205
+
206
+ {showPropertyPanel && (
207
+ <div className="absolute top-2 right-2">
208
+ <PropertiesView />
209
+ </div>
210
+ )}
211
+
212
+ {/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
213
+ <MiniMap width="100px" height="100px" />
214
+ </div> */}
215
+
216
+ <SettingsDisplay />
217
+ </SigmaContainer>
218
+ ) : (
219
+ // Placeholder when tab is not visible
220
+ <div className="flex h-full w-full items-center justify-center">
221
+ <div className="text-center text-muted-foreground">
222
+ {/* Placeholder content */}
223
+ </div>
224
  </div>
225
  )}
226
 
227
+ {/* Loading overlay - shown when data is loading */}
228
+ {isFetching && (
229
+ <div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
230
+ <div className="text-center">
231
+ <div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
232
+ <p>Loading Graph Data...</p>
233
+ </div>
234
+ </div>
235
+ )}
236
+ </div>
237
  )
238
  }
239
 
lightrag_webui/src/features/SiteHeader.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import Button from '@/components/ui/Button'
2
  import { SiteInfo } from '@/lib/constants'
3
- import ThemeToggle from '@/components/ThemeToggle'
4
  import LanguageToggle from '@/components/LanguageToggle'
5
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
6
  import { useSettingsStore } from '@/stores/settings'
@@ -77,23 +77,15 @@ export default function SiteHeader() {
77
  <TabsNavigation />
78
  </div>
79
 
80
- <nav className="flex items-center gap-2">
81
- <Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
82
- <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
83
- <GithubIcon className="size-4" aria-hidden="true" />
84
- </a>
85
- </Button>
86
- <LanguageToggle />
87
- <ThemeToggle />
88
- <Button
89
- variant="ghost"
90
- size="icon"
91
- side="bottom"
92
- tooltip="Log Out"
93
- onClick={handleLogout}
94
- >
95
- <LogOutIcon className="size-4" aria-hidden="true" />
96
- </Button>
97
  </nav>
98
  </header>
99
  )
 
1
  import Button from '@/components/ui/Button'
2
  import { SiteInfo } from '@/lib/constants'
3
+ import AppSettings from '@/components/AppSettings'
4
  import LanguageToggle from '@/components/LanguageToggle'
5
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
6
  import { useSettingsStore } from '@/stores/settings'
 
77
  <TabsNavigation />
78
  </div>
79
 
80
+ <nav className="flex items-center">
81
+ <div className="flex items-center gap-2">
82
+ <Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
83
+ <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
84
+ <GithubIcon className="size-4" aria-hidden="true" />
85
+ </a>
86
+ </Button>
87
+ <AppSettings />
88
+ </div>
 
 
 
 
 
 
 
 
89
  </nav>
90
  </header>
91
  )
lightrag_webui/src/hooks/useLightragGraph.tsx CHANGED
@@ -1,11 +1,12 @@
1
  import Graph, { DirectedGraph } from 'graphology'
2
- import { useCallback, useEffect } from 'react'
3
  import { randomColor, errorMessage } from '@/lib/utils'
4
  import * as Constants from '@/lib/constants'
5
  import { useGraphStore, RawGraph } from '@/stores/graph'
6
  import { queryGraphs } from '@/api/lightrag'
7
  import { useBackendState } from '@/stores/state'
8
  import { useSettingsStore } from '@/stores/settings'
 
9
 
10
  import seedrandom from 'seedrandom'
11
 
@@ -136,15 +137,23 @@ const fetchGraph = async (label: string, maxDepth: number, minDegree: number) =>
136
  return rawGraph
137
  }
138
 
 
139
  const createSigmaGraph = (rawGraph: RawGraph | null) => {
 
140
  const graph = new DirectedGraph()
141
 
 
142
  for (const rawNode of rawGraph?.nodes ?? []) {
 
 
 
 
 
143
  graph.addNode(rawNode.id, {
144
  label: rawNode.labels.join(', '),
145
  color: rawNode.color,
146
- x: rawNode.x,
147
- y: rawNode.y,
148
  size: rawNode.size,
149
  // for node-border
150
  borderColor: Constants.nodeBorderColor,
@@ -152,6 +161,7 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
152
  })
153
  }
154
 
 
155
  for (const rawEdge of rawGraph?.edges ?? []) {
156
  rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
157
  label: rawEdge.type || undefined
@@ -161,14 +171,30 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
161
  return graph
162
  }
163
 
164
- const lastQueryLabel = { label: '', maxQueryDepth: 0, minDegree: 0 }
165
-
166
  const useLightrangeGraph = () => {
167
  const queryLabel = useSettingsStore.use.queryLabel()
168
  const rawGraph = useGraphStore.use.rawGraph()
169
  const sigmaGraph = useGraphStore.use.sigmaGraph()
170
  const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
171
  const minDegree = useSettingsStore.use.graphMinDegree()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  const getNode = useCallback(
174
  (nodeId: string) => {
@@ -184,35 +210,131 @@ const useLightrangeGraph = () => {
184
  [rawGraph]
185
  )
186
 
 
 
 
 
187
  useEffect(() => {
188
- if (queryLabel) {
189
- if (lastQueryLabel.label !== queryLabel ||
190
- lastQueryLabel.maxQueryDepth !== maxQueryDepth ||
191
- lastQueryLabel.minDegree !== minDegree) {
192
- lastQueryLabel.label = queryLabel
193
- lastQueryLabel.maxQueryDepth = maxQueryDepth
194
- lastQueryLabel.minDegree = minDegree
195
 
 
 
 
196
  const state = useGraphStore.getState()
197
  state.reset()
198
- fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
199
- // console.debug('Query label: ' + queryLabel)
200
- state.setSigmaGraph(createSigmaGraph(data))
201
- data?.buildDynamicMap()
202
- state.setRawGraph(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  })
204
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  } else {
206
- const state = useGraphStore.getState()
207
- state.reset()
208
- state.setSigmaGraph(new DirectedGraph())
209
  }
210
- }, [queryLabel, maxQueryDepth, minDegree])
211
 
212
  const lightrageGraph = useCallback(() => {
 
213
  if (sigmaGraph) {
214
  return sigmaGraph as Graph<NodeType, EdgeType>
215
  }
 
 
 
216
  const graph = new DirectedGraph()
217
  useGraphStore.getState().setSigmaGraph(graph)
218
  return graph as Graph<NodeType, EdgeType>
 
1
  import Graph, { DirectedGraph } from 'graphology'
2
+ import { useCallback, useEffect, useRef } from 'react'
3
  import { randomColor, errorMessage } from '@/lib/utils'
4
  import * as Constants from '@/lib/constants'
5
  import { useGraphStore, RawGraph } from '@/stores/graph'
6
  import { queryGraphs } from '@/api/lightrag'
7
  import { useBackendState } from '@/stores/state'
8
  import { useSettingsStore } from '@/stores/settings'
9
+ import { useTabVisibility } from '@/contexts/useTabVisibility'
10
 
11
  import seedrandom from 'seedrandom'
12
 
 
137
  return rawGraph
138
  }
139
 
140
+ // Create a new graph instance with the raw graph data
141
  const createSigmaGraph = (rawGraph: RawGraph | null) => {
142
+ // Always create a new graph instance
143
  const graph = new DirectedGraph()
144
 
145
+ // Add nodes from raw graph data
146
  for (const rawNode of rawGraph?.nodes ?? []) {
147
+ // Ensure we have fresh random positions for nodes
148
+ seedrandom(rawNode.id + Date.now().toString(), { global: true })
149
+ const x = Math.random()
150
+ const y = Math.random()
151
+
152
  graph.addNode(rawNode.id, {
153
  label: rawNode.labels.join(', '),
154
  color: rawNode.color,
155
+ x: x,
156
+ y: y,
157
  size: rawNode.size,
158
  // for node-border
159
  borderColor: Constants.nodeBorderColor,
 
161
  })
162
  }
163
 
164
+ // Add edges from raw graph data
165
  for (const rawEdge of rawGraph?.edges ?? []) {
166
  rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
167
  label: rawEdge.type || undefined
 
171
  return graph
172
  }
173
 
 
 
174
  const useLightrangeGraph = () => {
175
  const queryLabel = useSettingsStore.use.queryLabel()
176
  const rawGraph = useGraphStore.use.rawGraph()
177
  const sigmaGraph = useGraphStore.use.sigmaGraph()
178
  const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
179
  const minDegree = useSettingsStore.use.graphMinDegree()
180
+ const isFetching = useGraphStore.use.isFetching()
181
+
182
+ // Get tab visibility
183
+ const { isTabVisible } = useTabVisibility()
184
+ const isGraphTabVisible = isTabVisible('knowledge-graph')
185
+
186
+ // Track previous parameters to detect actual changes
187
+ const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
188
+
189
+ // Use ref to track if data has been loaded and initial load
190
+ const dataLoadedRef = useRef(false)
191
+ const initialLoadRef = useRef(false)
192
+
193
+ // Check if parameters have changed
194
+ const paramsChanged =
195
+ prevParamsRef.current.queryLabel !== queryLabel ||
196
+ prevParamsRef.current.maxQueryDepth !== maxQueryDepth ||
197
+ prevParamsRef.current.minDegree !== minDegree
198
 
199
  const getNode = useCallback(
200
  (nodeId: string) => {
 
210
  [rawGraph]
211
  )
212
 
213
+ // Track if a fetch is in progress to prevent multiple simultaneous fetches
214
+ const fetchInProgressRef = useRef(false)
215
+
216
+ // Data fetching logic - simplified but preserving TAB visibility check
217
  useEffect(() => {
218
+ // Skip if fetch is already in progress
219
+ if (fetchInProgressRef.current) {
220
+ return
221
+ }
 
 
 
222
 
223
+ // If there's no query label, reset the graph
224
+ if (!queryLabel) {
225
+ if (rawGraph !== null || sigmaGraph !== null) {
226
  const state = useGraphStore.getState()
227
  state.reset()
228
+ state.setGraphDataFetchAttempted(false)
229
+ state.setLabelsFetchAttempted(false)
230
+ }
231
+ dataLoadedRef.current = false
232
+ initialLoadRef.current = false
233
+ return
234
+ }
235
+
236
+ // Check if parameters have changed
237
+ if (!isFetching && !fetchInProgressRef.current &&
238
+ (paramsChanged || !useGraphStore.getState().graphDataFetchAttempted)) {
239
+
240
+ // Only fetch data if the Graph tab is visible
241
+ if (!isGraphTabVisible) {
242
+ console.log('Graph tab not visible, skipping data fetch');
243
+ return;
244
+ }
245
+
246
+ // Set flags
247
+ fetchInProgressRef.current = true
248
+ useGraphStore.getState().setGraphDataFetchAttempted(true)
249
+
250
+ const state = useGraphStore.getState()
251
+ state.setIsFetching(true)
252
+ state.setShouldRender(false) // Disable rendering during data loading
253
+
254
+ // Clear selection and highlighted nodes before fetching new graph
255
+ state.clearSelection()
256
+ if (state.sigmaGraph) {
257
+ state.sigmaGraph.forEachNode((node) => {
258
+ state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false)
259
  })
260
  }
261
+
262
+ // Update parameter reference
263
+ prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree }
264
+
265
+ console.log('Fetching graph data...')
266
+
267
+ // Use a local copy of the parameters
268
+ const currentQueryLabel = queryLabel
269
+ const currentMaxQueryDepth = maxQueryDepth
270
+ const currentMinDegree = minDegree
271
+
272
+ // Fetch graph data
273
+ fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
274
+ const state = useGraphStore.getState()
275
+
276
+ // Reset state
277
+ state.reset()
278
+
279
+ // Create and set new graph directly
280
+ const newSigmaGraph = createSigmaGraph(data)
281
+ data?.buildDynamicMap()
282
+
283
+ // Set new graph data
284
+ state.setSigmaGraph(newSigmaGraph)
285
+ state.setRawGraph(data)
286
+
287
+ // No longer need to extract labels from graph data
288
+
289
+ // Update flags
290
+ dataLoadedRef.current = true
291
+ initialLoadRef.current = true
292
+ fetchInProgressRef.current = false
293
+
294
+ // Reset camera view
295
+ state.setMoveToSelectedNode(true)
296
+
297
+ // Enable rendering if the tab is visible
298
+ state.setShouldRender(isGraphTabVisible)
299
+ state.setIsFetching(false)
300
+ }).catch((error) => {
301
+ console.error('Error fetching graph data:', error)
302
+
303
+ // Reset state on error
304
+ const state = useGraphStore.getState()
305
+ state.setIsFetching(false)
306
+ state.setShouldRender(isGraphTabVisible)
307
+ dataLoadedRef.current = false
308
+ fetchInProgressRef.current = false
309
+ state.setGraphDataFetchAttempted(false)
310
+ })
311
+ }
312
+ }, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph])
313
+
314
+ // Update rendering state and handle tab visibility changes
315
+ useEffect(() => {
316
+ // When tab becomes visible
317
+ if (isGraphTabVisible) {
318
+ // If we have data, enable rendering
319
+ if (rawGraph) {
320
+ useGraphStore.getState().setShouldRender(true)
321
+ }
322
+
323
+ // We no longer reset the fetch attempted flag here to prevent continuous API calls
324
  } else {
325
+ // When tab becomes invisible, disable rendering
326
+ useGraphStore.getState().setShouldRender(false)
 
327
  }
328
+ }, [isGraphTabVisible, rawGraph])
329
 
330
  const lightrageGraph = useCallback(() => {
331
+ // If we already have a graph instance, return it
332
  if (sigmaGraph) {
333
  return sigmaGraph as Graph<NodeType, EdgeType>
334
  }
335
+
336
+ // If no graph exists yet, create a new one and store it
337
+ console.log('Creating new Sigma graph instance')
338
  const graph = new DirectedGraph()
339
  useGraphStore.getState().setSigmaGraph(graph)
340
  return graph as Graph<NodeType, EdgeType>
lightrag_webui/src/i18n.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import i18n from 'i18next'
2
+ import { initReactI18next } from 'react-i18next'
3
+ import { useSettingsStore } from '@/stores/settings'
4
+
5
+ import en from './locales/en.json'
6
+ import zh from './locales/zh.json'
7
+
8
+ // Function to sync i18n with store state
9
+ export const initializeI18n = async (): Promise<typeof i18n> => {
10
+ // Get initial language from store
11
+ const initialLanguage = useSettingsStore.getState().language
12
+
13
+ // Initialize with store language
14
+ await i18n.use(initReactI18next).init({
15
+ resources: {
16
+ en: { translation: en },
17
+ zh: { translation: zh }
18
+ },
19
+ lng: initialLanguage,
20
+ fallbackLng: 'en',
21
+ interpolation: {
22
+ escapeValue: false
23
+ }
24
+ })
25
+
26
+ // Subscribe to language changes
27
+ useSettingsStore.subscribe((state) => {
28
+ const currentLanguage = state.language
29
+ if (i18n.language !== currentLanguage) {
30
+ i18n.changeLanguage(currentLanguage)
31
+ }
32
+ })
33
+
34
+ return i18n
35
+ }
36
+
37
+ export default i18n
lightrag_webui/src/lib/constants.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { ButtonVariantType } from '@/components/ui/Button'
2
 
3
  export const backendBaseUrl = 'http://localhost:9621/'
4
- export const webuiPrefix = '/webui'
5
 
6
  export const controlButtonVariant: ButtonVariantType = 'ghost'
7
 
@@ -16,8 +16,8 @@ export const edgeColorDarkTheme = '#969696'
16
  export const edgeColorSelected = '#F57F17'
17
  export const edgeColorHighlighted = '#B2EBF2'
18
 
19
- export const searchResultLimit = 20
20
- export const labelListLimit = 40
21
 
22
  export const minNodeSize = 4
23
  export const maxNodeSize = 20
 
1
  import { ButtonVariantType } from '@/components/ui/Button'
2
 
3
  export const backendBaseUrl = 'http://localhost:9621/'
4
+ export const webuiPrefix = ''
5
 
6
  export const controlButtonVariant: ButtonVariantType = 'ghost'
7
 
 
16
  export const edgeColorSelected = '#F57F17'
17
  export const edgeColorHighlighted = '#B2EBF2'
18
 
19
+ export const searchResultLimit = 50
20
+ export const labelListLimit = 100
21
 
22
  export const minNodeSize = 4
23
  export const maxNodeSize = 20