jin commited on
Commit
82d36b3
·
unverified ·
2 Parent(s): 0fc2402 735d7be

Merge branch 'HKUDS:main' into main

Browse files
README.md CHANGED
@@ -12,7 +12,7 @@
12
  </p>
13
  <p>
14
  <img src='https://img.shields.io/github/stars/hkuds/lightrag?color=green&style=social' />
15
- <img src="https://img.shields.io/badge/python->=3.10-blue">
16
  <a href="https://pypi.org/project/lightrag-hku/"><img src="https://img.shields.io/pypi/v/lightrag-hku.svg"></a>
17
  <a href="https://pepy.tech/project/lightrag-hku"><img src="https://static.pepy.tech/badge/lightrag-hku/month"></a>
18
  </p>
@@ -26,7 +26,8 @@ This repository hosts the code of LightRAG. The structure of this code is based
26
  </div>
27
 
28
  ## 🎉 News
29
- - [x] [2025.01.06]🎯📢LightRAG now supports [PostgreSQL for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-postgres-for-storage).
 
30
  - [x] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
31
  - [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.
32
  - [x] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
@@ -361,6 +362,18 @@ see test_neo4j.py for a working example.
361
  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).
362
  * 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.
363
  * How to start? Ref to: [examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
  ### Insert Custom KG
366
 
@@ -912,12 +925,14 @@ pip install -e ".[api]"
912
 
913
  ### Prerequisites
914
 
915
- Before running any of the servers, ensure you have the corresponding backend service running:
 
 
916
 
917
  #### For LoLLMs Server
918
  - LoLLMs must be running and accessible
919
  - Default connection: http://localhost:9600
920
- - Configure using --lollms-host if running on a different host/port
921
 
922
  #### For Ollama Server
923
  - Ollama must be running and accessible
@@ -953,113 +968,96 @@ The output of the last command will give you the endpoint and the key for the Op
953
 
954
  Each server has its own specific configuration options:
955
 
956
- #### LoLLMs Server Options
957
 
958
  | Parameter | Default | Description |
959
  |-----------|---------|-------------|
960
- | --host | 0.0.0.0 | RAG server host |
961
- | --port | 9621 | RAG server port |
962
- | --model | mistral-nemo:latest | LLM model name |
 
 
 
 
963
  | --embedding-model | bge-m3:latest | Embedding model name |
964
- | --lollms-host | http://localhost:9600 | LoLLMS backend URL |
965
- | --working-dir | ./rag_storage | Working directory for RAG |
966
  | --max-async | 4 | Maximum async operations |
967
  | --max-tokens | 32768 | Maximum token size |
968
  | --embedding-dim | 1024 | Embedding dimensions |
969
  | --max-embed-tokens | 8192 | Maximum embedding token size |
970
- | --input-file | ./book.txt | Initial input file |
971
- | --log-level | INFO | Logging level |
972
- | --key | none | Access Key to protect the lightrag service |
 
 
 
973
 
974
- #### Ollama Server Options
975
 
976
- | Parameter | Default | Description |
977
- |-----------|---------|-------------|
978
- | --host | 0.0.0.0 | RAG server host |
979
- | --port | 9621 | RAG server port |
980
- | --model | mistral-nemo:latest | LLM model name |
981
- | --embedding-model | bge-m3:latest | Embedding model name |
982
- | --ollama-host | http://localhost:11434 | Ollama backend URL |
983
- | --working-dir | ./rag_storage | Working directory for RAG |
984
- | --max-async | 4 | Maximum async operations |
985
- | --max-tokens | 32768 | Maximum token size |
986
- | --embedding-dim | 1024 | Embedding dimensions |
987
- | --max-embed-tokens | 8192 | Maximum embedding token size |
988
- | --input-file | ./book.txt | Initial input file |
989
- | --log-level | INFO | Logging level |
990
- | --key | none | Access Key to protect the lightrag service |
991
 
992
- #### OpenAI Server Options
 
993
 
994
- | Parameter | Default | Description |
995
- |-----------|---------|-------------|
996
- | --host | 0.0.0.0 | RAG server host |
997
- | --port | 9621 | RAG server port |
998
- | --model | gpt-4 | OpenAI model name |
999
- | --embedding-model | text-embedding-3-large | OpenAI embedding model |
1000
- | --working-dir | ./rag_storage | Working directory for RAG |
1001
- | --max-tokens | 32768 | Maximum token size |
1002
- | --max-embed-tokens | 8192 | Maximum embedding token size |
1003
- | --input-dir | ./inputs | Input directory for documents |
1004
- | --log-level | INFO | Logging level |
1005
- | --key | none | Access Key to protect the lightrag service |
1006
 
1007
- #### OpenAI AZURE Server Options
1008
 
1009
- | Parameter | Default | Description |
1010
- |-----------|---------|-------------|
1011
- | --host | 0.0.0.0 | Server host |
1012
- | --port | 9621 | Server port |
1013
- | --model | gpt-4 | OpenAI model name |
1014
- | --embedding-model | text-embedding-3-large | OpenAI embedding model |
1015
- | --working-dir | ./rag_storage | Working directory for RAG |
1016
- | --max-tokens | 32768 | Maximum token size |
1017
- | --max-embed-tokens | 8192 | Maximum embedding token size |
1018
- | --input-dir | ./inputs | Input directory for documents |
1019
- | --enable-cache | True | Enable response cache |
1020
- | --log-level | INFO | Logging level |
1021
- | --key | none | Access Key to protect the lightrag service |
1022
 
 
 
1023
 
1024
- For protecting the server using an authentication key, you can also use an environment variable named `LIGHTRAG_API_KEY`.
1025
- ### Example Usage
1026
 
1027
- #### LoLLMs RAG Server
 
 
 
 
1028
 
1029
  ```bash
1030
- # Custom configuration with specific model and working directory
1031
- lollms-lightrag-server --model mistral-nemo --port 8080 --working-dir ./custom_rag
1032
 
1033
- # Using specific models (ensure they are installed in your LoLLMs instance)
1034
- lollms-lightrag-server --model mistral-nemo:latest --embedding-model bge-m3 --embedding-dim 1024
1035
 
1036
- # Using specific models and an authentication key
1037
- lollms-lightrag-server --model mistral-nemo:latest --embedding-model bge-m3 --embedding-dim 1024 --key ky-mykey
1038
 
 
 
1039
  ```
1040
 
1041
- #### Ollama RAG Server
 
1042
 
1043
  ```bash
1044
- # Custom configuration with specific model and working directory
1045
- ollama-lightrag-server --model mistral-nemo:latest --port 8080 --working-dir ./custom_rag
 
 
 
1046
 
1047
- # Using specific models (ensure they are installed in your Ollama instance)
1048
- ollama-lightrag-server --model mistral-nemo:latest --embedding-model bge-m3 --embedding-dim 1024
1049
  ```
1050
 
1051
- #### OpenAI RAG Server
1052
 
1053
  ```bash
1054
- # Using GPT-4 with text-embedding-3-large
1055
- openai-lightrag-server --port 9624 --model gpt-4 --embedding-model text-embedding-3-large
1056
- ```
1057
- #### Azure OpenAI RAG Server
1058
- ```bash
1059
- # Using GPT-4 with text-embedding-3-large
1060
- azure-openai-lightrag-server --model gpt-4o --port 8080 --working-dir ./custom_rag --embedding-model text-embedding-3-large
1061
- ```
1062
 
 
 
 
 
 
 
1063
 
1064
  **Important Notes:**
1065
  - For LoLLMs: Make sure the specified models are installed in your LoLLMs instance
@@ -1069,10 +1067,7 @@ azure-openai-lightrag-server --model gpt-4o --port 8080 --working-dir ./custom_r
1069
 
1070
  For help on any server, use the --help flag:
1071
  ```bash
1072
- lollms-lightrag-server --help
1073
- ollama-lightrag-server --help
1074
- openai-lightrag-server --help
1075
- azure-openai-lightrag-server --help
1076
  ```
1077
 
1078
  Note: If you don't need the API functionality, you can install the base package without API support using:
@@ -1092,7 +1087,7 @@ Query the RAG system with options for different search modes.
1092
  ```bash
1093
  curl -X POST "http://localhost:9621/query" \
1094
  -H "Content-Type: application/json" \
1095
- -d '{"query": "Your question here", "mode": "hybrid"}'
1096
  ```
1097
 
1098
  #### POST /query/stream
 
12
  </p>
13
  <p>
14
  <img src='https://img.shields.io/github/stars/hkuds/lightrag?color=green&style=social' />
15
+ <img src="https://img.shields.io/badge/python-3.10-blue">
16
  <a href="https://pypi.org/project/lightrag-hku/"><img src="https://img.shields.io/pypi/v/lightrag-hku.svg"></a>
17
  <a href="https://pepy.tech/project/lightrag-hku"><img src="https://static.pepy.tech/badge/lightrag-hku/month"></a>
18
  </p>
 
26
  </div>
27
 
28
  ## 🎉 News
29
+ - [x] [2025.01.13]🎯📢Our team has released [MiniRAG](https://github.com/HKUDS/MiniRAG) making RAG simpler with small models.
30
+ - [x] [2025.01.06]🎯📢You can now [use PostgreSQL for Storage](#using-postgresql-for-storage).
31
  - [x] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
32
  - [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.
33
  - [x] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
 
362
  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).
363
  * 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.
364
  * How to start? Ref to: [examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
365
+ * Create index for AGE example: (Change below `dickens` to your graph name if necessary)
366
+ ```
367
+ SET search_path = ag_catalog, "$user", public;
368
+ CREATE INDEX idx_entity ON dickens."Entity" USING gin (agtype_access_operator(properties, '"node_id"'));
369
+ ```
370
+ * Known issue of the Apache AGE: The released versions got below issue:
371
+ > You might find that the properties of the nodes/edges are empty.
372
+ > It is a known issue of the release version: https://github.com/apache/age/pull/1721
373
+ >
374
+ > You can Compile the AGE from source code and fix it.
375
+
376
+
377
 
378
  ### Insert Custom KG
379
 
 
925
 
926
  ### Prerequisites
927
 
928
+ Before running any of the servers, ensure you have the corresponding backend service running for both llm and embedding.
929
+ The new api allows you to mix different bindings for llm/embeddings.
930
+ For example, you have the possibility to use ollama for the embedding and openai for the llm.
931
 
932
  #### For LoLLMs Server
933
  - LoLLMs must be running and accessible
934
  - Default connection: http://localhost:9600
935
+ - Configure using --llm-binding-host and/or --embedding-binding-host if running on a different host/port
936
 
937
  #### For Ollama Server
938
  - Ollama must be running and accessible
 
968
 
969
  Each server has its own specific configuration options:
970
 
971
+ #### LightRag Server Options
972
 
973
  | Parameter | Default | Description |
974
  |-----------|---------|-------------|
975
+ | --host | 0.0.0.0 | Server host |
976
+ | --port | 9621 | Server port |
977
+ | --llm-binding | ollama | LLM binding to be used. Supported: lollms, ollama, openai |
978
+ | --llm-binding-host | (dynamic) | LLM server host URL. Defaults based on binding: http://localhost:11434 (ollama), http://localhost:9600 (lollms), https://api.openai.com/v1 (openai) |
979
+ | --llm-model | mistral-nemo:latest | LLM model name |
980
+ | --embedding-binding | ollama | Embedding binding to be used. Supported: lollms, ollama, openai |
981
+ | --embedding-binding-host | (dynamic) | Embedding server host URL. Defaults based on binding: http://localhost:11434 (ollama), http://localhost:9600 (lollms), https://api.openai.com/v1 (openai) |
982
  | --embedding-model | bge-m3:latest | Embedding model name |
983
+ | --working-dir | ./rag_storage | Working directory for RAG storage |
984
+ | --input-dir | ./inputs | Directory containing input documents |
985
  | --max-async | 4 | Maximum async operations |
986
  | --max-tokens | 32768 | Maximum token size |
987
  | --embedding-dim | 1024 | Embedding dimensions |
988
  | --max-embed-tokens | 8192 | Maximum embedding token size |
989
+ | --timeout | None | Timeout in seconds (useful when using slow AI). Use None for infinite timeout |
990
+ | --log-level | INFO | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
991
+ | --key | None | API key for authentication. Protects lightrag server against unauthorized access |
992
+ | --ssl | False | Enable HTTPS |
993
+ | --ssl-certfile | None | Path to SSL certificate file (required if --ssl is enabled) |
994
+ | --ssl-keyfile | None | Path to SSL private key file (required if --ssl is enabled) |
995
 
 
996
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
997
 
998
+ For protecting the server using an authentication key, you can also use an environment variable named `LIGHTRAG_API_KEY`.
999
+ ### Example Usage
1000
 
1001
+ #### Running a Lightrag server with ollama default local server as llm and embedding backends
 
 
 
 
 
 
 
 
 
 
 
1002
 
1003
+ Ollama is the default backend for both llm and embedding, so by default you can run lightrag-server with no parameters and the default ones will be used. Make sure ollama is installed and is running and default models are already installed on ollama.
1004
 
1005
+ ```bash
1006
+ # Run lightrag with ollama, mistral-nemo:latest for llm, and bge-m3:latest for embedding
1007
+ lightrag-server
 
 
 
 
 
 
 
 
 
 
1008
 
1009
+ # Using specific models (ensure they are installed in your ollama instance)
1010
+ lightrag-server --llm-model adrienbrault/nous-hermes2theta-llama3-8b:f16 --embedding-model nomic-embed-text --embedding-dim 1024
1011
 
1012
+ # Using an authentication key
1013
+ lightrag-server --key my-key
1014
 
1015
+ # Using lollms for llm and ollama for embedding
1016
+ lightrag-server --llm-binding lollms
1017
+ ```
1018
+
1019
+ #### Running a Lightrag server with lollms default local server as llm and embedding backends
1020
 
1021
  ```bash
1022
+ # Run lightrag with lollms, mistral-nemo:latest for llm, and bge-m3:latest for embedding, use lollms for both llm and embedding
1023
+ lightrag-server --llm-binding lollms --embedding-binding lollms
1024
 
1025
+ # Using specific models (ensure they are installed in your ollama instance)
1026
+ lightrag-server --llm-binding lollms --llm-model adrienbrault/nous-hermes2theta-llama3-8b:f16 --embedding-binding lollms --embedding-model nomic-embed-text --embedding-dim 1024
1027
 
1028
+ # Using an authentication key
1029
+ lightrag-server --key my-key
1030
 
1031
+ # Using lollms for llm and openai for embedding
1032
+ lightrag-server --llm-binding lollms --embedding-binding openai --embedding-model text-embedding-3-small
1033
  ```
1034
 
1035
+
1036
+ #### Running a Lightrag server with openai server as llm and embedding backends
1037
 
1038
  ```bash
1039
+ # Run lightrag with lollms, GPT-4o-mini for llm, and text-embedding-3-small for embedding, use openai for both llm and embedding
1040
+ lightrag-server --llm-binding openai --llm-model GPT-4o-mini --embedding-binding openai --embedding-model text-embedding-3-small
1041
+
1042
+ # Using an authentication key
1043
+ lightrag-server --llm-binding openai --llm-model GPT-4o-mini --embedding-binding openai --embedding-model text-embedding-3-small --key my-key
1044
 
1045
+ # Using lollms for llm and openai for embedding
1046
+ lightrag-server --llm-binding lollms --embedding-binding openai --embedding-model text-embedding-3-small
1047
  ```
1048
 
1049
+ #### Running a Lightrag server with azure openai server as llm and embedding backends
1050
 
1051
  ```bash
1052
+ # Run lightrag with lollms, GPT-4o-mini for llm, and text-embedding-3-small for embedding, use openai for both llm and embedding
1053
+ lightrag-server --llm-binding azure_openai --llm-model GPT-4o-mini --embedding-binding openai --embedding-model text-embedding-3-small
 
 
 
 
 
 
1054
 
1055
+ # Using an authentication key
1056
+ lightrag-server --llm-binding azure_openai --llm-model GPT-4o-mini --embedding-binding azure_openai --embedding-model text-embedding-3-small --key my-key
1057
+
1058
+ # Using lollms for llm and azure_openai for embedding
1059
+ lightrag-server --llm-binding lollms --embedding-binding azure_openai --embedding-model text-embedding-3-small
1060
+ ```
1061
 
1062
  **Important Notes:**
1063
  - For LoLLMs: Make sure the specified models are installed in your LoLLMs instance
 
1067
 
1068
  For help on any server, use the --help flag:
1069
  ```bash
1070
+ lightrag-server --help
 
 
 
1071
  ```
1072
 
1073
  Note: If you don't need the API functionality, you can install the base package without API support using:
 
1087
  ```bash
1088
  curl -X POST "http://localhost:9621/query" \
1089
  -H "Content-Type: application/json" \
1090
+ -d '{"query": "Your question here", "mode": "hybrid", ""}'
1091
  ```
1092
 
1093
  #### POST /query/stream
contributor-readme.MD → contributor-README.md RENAMED
File without changes
examples/test_split_by_character.ipynb ADDED
@@ -0,0 +1,1296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "4b5690db12e34685",
7
+ "metadata": {
8
+ "ExecuteTime": {
9
+ "end_time": "2025-01-09T03:40:58.307102Z",
10
+ "start_time": "2025-01-09T03:40:51.935233Z"
11
+ }
12
+ },
13
+ "outputs": [],
14
+ "source": [
15
+ "import os\n",
16
+ "import logging\n",
17
+ "import numpy as np\n",
18
+ "from lightrag import LightRAG, QueryParam\n",
19
+ "from lightrag.llm import openai_complete_if_cache, openai_embedding\n",
20
+ "from lightrag.utils import EmbeddingFunc\n",
21
+ "import nest_asyncio"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "markdown",
26
+ "id": "dd17956ec322b361",
27
+ "metadata": {},
28
+ "source": "#### split by character"
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": 3,
33
+ "id": "8c8ee7c061bf9159",
34
+ "metadata": {
35
+ "ExecuteTime": {
36
+ "end_time": "2025-01-09T03:41:13.961167Z",
37
+ "start_time": "2025-01-09T03:41:13.958357Z"
38
+ }
39
+ },
40
+ "outputs": [],
41
+ "source": [
42
+ "nest_asyncio.apply()\n",
43
+ "WORKING_DIR = \"../../llm_rag/paper_db/R000088_test1\"\n",
44
+ "logging.basicConfig(format=\"%(levelname)s:%(message)s\", level=logging.INFO)\n",
45
+ "if not os.path.exists(WORKING_DIR):\n",
46
+ " os.mkdir(WORKING_DIR)\n",
47
+ "API = os.environ.get(\"DOUBAO_API_KEY\")"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "code",
52
+ "execution_count": 4,
53
+ "id": "a5009d16e0851dca",
54
+ "metadata": {
55
+ "ExecuteTime": {
56
+ "end_time": "2025-01-09T03:41:16.862036Z",
57
+ "start_time": "2025-01-09T03:41:16.859306Z"
58
+ }
59
+ },
60
+ "outputs": [],
61
+ "source": [
62
+ "async def llm_model_func(\n",
63
+ " prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs\n",
64
+ ") -> str:\n",
65
+ " return await openai_complete_if_cache(\n",
66
+ " \"ep-20241218114828-2tlww\",\n",
67
+ " prompt,\n",
68
+ " system_prompt=system_prompt,\n",
69
+ " history_messages=history_messages,\n",
70
+ " api_key=API,\n",
71
+ " base_url=\"https://ark.cn-beijing.volces.com/api/v3\",\n",
72
+ " **kwargs,\n",
73
+ " )\n",
74
+ "\n",
75
+ "\n",
76
+ "async def embedding_func(texts: list[str]) -> np.ndarray:\n",
77
+ " return await openai_embedding(\n",
78
+ " texts,\n",
79
+ " model=\"ep-20241231173413-pgjmk\",\n",
80
+ " api_key=API,\n",
81
+ " base_url=\"https://ark.cn-beijing.volces.com/api/v3\",\n",
82
+ " )"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "code",
87
+ "execution_count": 5,
88
+ "id": "397fcad24ce4d0ed",
89
+ "metadata": {
90
+ "ExecuteTime": {
91
+ "end_time": "2025-01-09T03:41:24.950307Z",
92
+ "start_time": "2025-01-09T03:41:24.940353Z"
93
+ }
94
+ },
95
+ "outputs": [
96
+ {
97
+ "name": "stderr",
98
+ "output_type": "stream",
99
+ "text": [
100
+ "INFO:lightrag:Logger initialized for working directory: ../../llm_rag/paper_db/R000088_test1\n",
101
+ "INFO:lightrag:Load KV llm_response_cache with 0 data\n",
102
+ "INFO:lightrag:Load KV full_docs with 0 data\n",
103
+ "INFO:lightrag:Load KV text_chunks with 0 data\n",
104
+ "INFO:nano-vectordb:Init {'embedding_dim': 4096, 'metric': 'cosine', 'storage_file': '../../llm_rag/paper_db/R000088_test1/vdb_entities.json'} 0 data\n",
105
+ "INFO:nano-vectordb:Init {'embedding_dim': 4096, 'metric': 'cosine', 'storage_file': '../../llm_rag/paper_db/R000088_test1/vdb_relationships.json'} 0 data\n",
106
+ "INFO:nano-vectordb:Init {'embedding_dim': 4096, 'metric': 'cosine', 'storage_file': '../../llm_rag/paper_db/R000088_test1/vdb_chunks.json'} 0 data\n",
107
+ "INFO:lightrag:Loaded document status storage with 0 records\n"
108
+ ]
109
+ }
110
+ ],
111
+ "source": [
112
+ "rag = LightRAG(\n",
113
+ " working_dir=WORKING_DIR,\n",
114
+ " llm_model_func=llm_model_func,\n",
115
+ " embedding_func=EmbeddingFunc(\n",
116
+ " embedding_dim=4096, max_token_size=8192, func=embedding_func\n",
117
+ " ),\n",
118
+ " chunk_token_size=512,\n",
119
+ ")"
120
+ ]
121
+ },
122
+ {
123
+ "cell_type": "code",
124
+ "execution_count": 6,
125
+ "id": "1dc3603677f7484d",
126
+ "metadata": {
127
+ "ExecuteTime": {
128
+ "end_time": "2025-01-09T03:41:37.947456Z",
129
+ "start_time": "2025-01-09T03:41:37.941901Z"
130
+ }
131
+ },
132
+ "outputs": [],
133
+ "source": [
134
+ "with open(\n",
135
+ " \"../../llm_rag/example/R000088/auto/R000088_full_txt.md\", \"r\", encoding=\"utf-8\"\n",
136
+ ") as f:\n",
137
+ " content = f.read()\n",
138
+ "\n",
139
+ "\n",
140
+ "async def embedding_func(texts: list[str]) -> np.ndarray:\n",
141
+ " return await openai_embedding(\n",
142
+ " texts,\n",
143
+ " model=\"ep-20241231173413-pgjmk\",\n",
144
+ " api_key=API,\n",
145
+ " base_url=\"https://ark.cn-beijing.volces.com/api/v3\",\n",
146
+ " )\n",
147
+ "\n",
148
+ "\n",
149
+ "async def get_embedding_dim():\n",
150
+ " test_text = [\"This is a test sentence.\"]\n",
151
+ " embedding = await embedding_func(test_text)\n",
152
+ " embedding_dim = embedding.shape[1]\n",
153
+ " return embedding_dim"
154
+ ]
155
+ },
156
+ {
157
+ "cell_type": "code",
158
+ "execution_count": 7,
159
+ "id": "6844202606acfbe5",
160
+ "metadata": {
161
+ "ExecuteTime": {
162
+ "end_time": "2025-01-09T03:41:39.608541Z",
163
+ "start_time": "2025-01-09T03:41:39.165057Z"
164
+ }
165
+ },
166
+ "outputs": [
167
+ {
168
+ "name": "stderr",
169
+ "output_type": "stream",
170
+ "text": [
171
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n"
172
+ ]
173
+ }
174
+ ],
175
+ "source": [
176
+ "embedding_dimension = await get_embedding_dim()"
177
+ ]
178
+ },
179
+ {
180
+ "cell_type": "code",
181
+ "execution_count": 8,
182
+ "id": "d6273839d9681403",
183
+ "metadata": {
184
+ "ExecuteTime": {
185
+ "end_time": "2025-01-09T03:44:34.295345Z",
186
+ "start_time": "2025-01-09T03:41:48.324171Z"
187
+ }
188
+ },
189
+ "outputs": [
190
+ {
191
+ "name": "stderr",
192
+ "output_type": "stream",
193
+ "text": [
194
+ "INFO:lightrag:Processing 1 new unique documents\n",
195
+ "Processing batch 1: 0%| | 0/1 [00:00<?, ?it/s]INFO:lightrag:Inserting 35 vectors to chunks\n",
196
+ "\n",
197
+ "Generating embeddings: 0%| | 0/2 [00:00<?, ?batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
198
+ "\n",
199
+ "Generating embeddings: 50%|█████ | 1/2 [00:00<00:00, 1.36batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
200
+ "\n",
201
+ "Generating embeddings: 100%|██████████| 2/2 [00:04<00:00, 2.25s/batch]\u001b[A\n",
202
+ "\n",
203
+ "Extracting entities from chunks: 0%| | 0/35 [00:00<?, ?chunk/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
204
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
205
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
206
+ ]
207
+ },
208
+ {
209
+ "name": "stdout",
210
+ "output_type": "stream",
211
+ "text": [
212
+ "⠙ Processed 1 chunks, 1 entities(duplicated), 0 relations(duplicated)\r"
213
+ ]
214
+ },
215
+ {
216
+ "name": "stderr",
217
+ "output_type": "stream",
218
+ "text": [
219
+ "\n",
220
+ "Extracting entities from chunks: 3%|▎ | 1/35 [00:04<02:47, 4.93s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
221
+ ]
222
+ },
223
+ {
224
+ "name": "stdout",
225
+ "output_type": "stream",
226
+ "text": [
227
+ "⠹ Processed 2 chunks, 2 entities(duplicated), 0 relations(duplicated)\r"
228
+ ]
229
+ },
230
+ {
231
+ "name": "stderr",
232
+ "output_type": "stream",
233
+ "text": [
234
+ "\n",
235
+ "Extracting entities from chunks: 6%|▌ | 2/35 [00:05<01:18, 2.37s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
236
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
237
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
238
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
239
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
240
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
241
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
242
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
243
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
244
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
245
+ ]
246
+ },
247
+ {
248
+ "name": "stdout",
249
+ "output_type": "stream",
250
+ "text": [
251
+ "⠸ Processed 3 chunks, 9 entities(duplicated), 5 relations(duplicated)\r"
252
+ ]
253
+ },
254
+ {
255
+ "name": "stderr",
256
+ "output_type": "stream",
257
+ "text": [
258
+ "\n",
259
+ "Extracting entities from chunks: 9%|▊ | 3/35 [00:26<05:43, 10.73s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
260
+ ]
261
+ },
262
+ {
263
+ "name": "stdout",
264
+ "output_type": "stream",
265
+ "text": [
266
+ "⠼ Processed 4 chunks, 16 entities(duplicated), 11 relations(duplicated)\r"
267
+ ]
268
+ },
269
+ {
270
+ "name": "stderr",
271
+ "output_type": "stream",
272
+ "text": [
273
+ "\n",
274
+ "Extracting entities from chunks: 11%|█▏ | 4/35 [00:26<03:24, 6.60s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
275
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
276
+ ]
277
+ },
278
+ {
279
+ "name": "stdout",
280
+ "output_type": "stream",
281
+ "text": [
282
+ "⠴ Processed 5 chunks, 24 entities(duplicated), 18 relations(duplicated)\r"
283
+ ]
284
+ },
285
+ {
286
+ "name": "stderr",
287
+ "output_type": "stream",
288
+ "text": [
289
+ "\n",
290
+ "Extracting entities from chunks: 14%|█▍ | 5/35 [00:33<03:24, 6.82s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
291
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
292
+ ]
293
+ },
294
+ {
295
+ "name": "stdout",
296
+ "output_type": "stream",
297
+ "text": [
298
+ "⠦ Processed 6 chunks, 35 entities(duplicated), 28 relations(duplicated)\r"
299
+ ]
300
+ },
301
+ {
302
+ "name": "stderr",
303
+ "output_type": "stream",
304
+ "text": [
305
+ "\n",
306
+ "Extracting entities from chunks: 17%|█▋ | 6/35 [00:42<03:38, 7.53s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
307
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
308
+ ]
309
+ },
310
+ {
311
+ "name": "stdout",
312
+ "output_type": "stream",
313
+ "text": [
314
+ "⠧ Processed 7 chunks, 47 entities(duplicated), 36 relations(duplicated)\r"
315
+ ]
316
+ },
317
+ {
318
+ "name": "stderr",
319
+ "output_type": "stream",
320
+ "text": [
321
+ "\n",
322
+ "Extracting entities from chunks: 20%|██ | 7/35 [00:43<02:28, 5.31s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
323
+ ]
324
+ },
325
+ {
326
+ "name": "stdout",
327
+ "output_type": "stream",
328
+ "text": [
329
+ "⠇ Processed 8 chunks, 61 entities(duplicated), 49 relations(duplicated)\r"
330
+ ]
331
+ },
332
+ {
333
+ "name": "stderr",
334
+ "output_type": "stream",
335
+ "text": [
336
+ "\n",
337
+ "Extracting entities from chunks: 23%|██▎ | 8/35 [00:45<01:52, 4.16s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
338
+ ]
339
+ },
340
+ {
341
+ "name": "stdout",
342
+ "output_type": "stream",
343
+ "text": [
344
+ "⠏ Processed 9 chunks, 81 entities(duplicated), 49 relations(duplicated)\r"
345
+ ]
346
+ },
347
+ {
348
+ "name": "stderr",
349
+ "output_type": "stream",
350
+ "text": [
351
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
352
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
353
+ ]
354
+ },
355
+ {
356
+ "name": "stdout",
357
+ "output_type": "stream",
358
+ "text": [
359
+ "⠋ Processed 10 chunks, 90 entities(duplicated), 62 relations(duplicated)\r"
360
+ ]
361
+ },
362
+ {
363
+ "name": "stderr",
364
+ "output_type": "stream",
365
+ "text": [
366
+ "\n",
367
+ "Extracting entities from chunks: 29%|██▊ | 10/35 [00:46<01:06, 2.64s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
368
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
369
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
370
+ ]
371
+ },
372
+ {
373
+ "name": "stdout",
374
+ "output_type": "stream",
375
+ "text": [
376
+ "⠙ Processed 11 chunks, 101 entities(duplicated), 80 relations(duplicated)\r"
377
+ ]
378
+ },
379
+ {
380
+ "name": "stderr",
381
+ "output_type": "stream",
382
+ "text": [
383
+ "\n",
384
+ "Extracting entities from chunks: 31%|███▏ | 11/35 [00:52<01:19, 3.31s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
385
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
386
+ ]
387
+ },
388
+ {
389
+ "name": "stdout",
390
+ "output_type": "stream",
391
+ "text": [
392
+ "⠹ Processed 12 chunks, 108 entities(duplicated), 85 relations(duplicated)\r"
393
+ ]
394
+ },
395
+ {
396
+ "name": "stderr",
397
+ "output_type": "stream",
398
+ "text": [
399
+ "\n",
400
+ "Extracting entities from chunks: 34%|███▍ | 12/35 [00:54<01:11, 3.12s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
401
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
402
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
403
+ ]
404
+ },
405
+ {
406
+ "name": "stdout",
407
+ "output_type": "stream",
408
+ "text": [
409
+ "⠸ Processed 13 chunks, 120 entities(duplicated), 100 relations(duplicated)\r"
410
+ ]
411
+ },
412
+ {
413
+ "name": "stderr",
414
+ "output_type": "stream",
415
+ "text": [
416
+ "\n",
417
+ "Extracting entities from chunks: 37%|███▋ | 13/35 [00:59<01:18, 3.55s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
418
+ ]
419
+ },
420
+ {
421
+ "name": "stdout",
422
+ "output_type": "stream",
423
+ "text": [
424
+ "⠼ Processed 14 chunks, 131 entities(duplicated), 110 relations(duplicated)\r"
425
+ ]
426
+ },
427
+ {
428
+ "name": "stderr",
429
+ "output_type": "stream",
430
+ "text": [
431
+ "\n",
432
+ "Extracting entities from chunks: 40%|████ | 14/35 [01:00<00:59, 2.82s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
433
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
434
+ ]
435
+ },
436
+ {
437
+ "name": "stdout",
438
+ "output_type": "stream",
439
+ "text": [
440
+ "⠴ Processed 15 chunks, 143 entities(duplicated), 110 relations(duplicated)\r"
441
+ ]
442
+ },
443
+ {
444
+ "name": "stderr",
445
+ "output_type": "stream",
446
+ "text": [
447
+ "\n",
448
+ "Extracting entities from chunks: 43%|████▎ | 15/35 [01:02<00:52, 2.64s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
449
+ ]
450
+ },
451
+ {
452
+ "name": "stdout",
453
+ "output_type": "stream",
454
+ "text": [
455
+ "⠦ Processed 16 chunks, 162 entities(duplicated), 124 relations(duplicated)\r"
456
+ ]
457
+ },
458
+ {
459
+ "name": "stderr",
460
+ "output_type": "stream",
461
+ "text": [
462
+ "\n",
463
+ "Extracting entities from chunks: 46%|████▌ | 16/35 [01:05<00:53, 2.80s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
464
+ ]
465
+ },
466
+ {
467
+ "name": "stdout",
468
+ "output_type": "stream",
469
+ "text": [
470
+ "⠧ Processed 17 chunks, 174 entities(duplicated), 132 relations(duplicated)\r"
471
+ ]
472
+ },
473
+ {
474
+ "name": "stderr",
475
+ "output_type": "stream",
476
+ "text": [
477
+ "\n",
478
+ "Extracting entities from chunks: 49%|████▊ | 17/35 [01:06<00:39, 2.19s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
479
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
480
+ ]
481
+ },
482
+ {
483
+ "name": "stdout",
484
+ "output_type": "stream",
485
+ "text": [
486
+ "⠇ Processed 18 chunks, 185 entities(duplicated), 137 relations(duplicated)\r"
487
+ ]
488
+ },
489
+ {
490
+ "name": "stderr",
491
+ "output_type": "stream",
492
+ "text": [
493
+ "\n",
494
+ "Extracting entities from chunks: 51%|█████▏ | 18/35 [01:12<00:53, 3.15s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
495
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
496
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
497
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
498
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
499
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
500
+ ]
501
+ },
502
+ {
503
+ "name": "stdout",
504
+ "output_type": "stream",
505
+ "text": [
506
+ "⠏ Processed 19 chunks, 193 entities(duplicated), 149 relations(duplicated)\r"
507
+ ]
508
+ },
509
+ {
510
+ "name": "stderr",
511
+ "output_type": "stream",
512
+ "text": [
513
+ "\n",
514
+ "Extracting entities from chunks: 54%|█████▍ | 19/35 [01:18<01:06, 4.14s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
515
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
516
+ ]
517
+ },
518
+ {
519
+ "name": "stdout",
520
+ "output_type": "stream",
521
+ "text": [
522
+ "⠋ Processed 20 chunks, 205 entities(duplicated), 158 relations(duplicated)\r"
523
+ ]
524
+ },
525
+ {
526
+ "name": "stderr",
527
+ "output_type": "stream",
528
+ "text": [
529
+ "\n",
530
+ "Extracting entities from chunks: 57%|█████▋ | 20/35 [01:19<00:50, 3.35s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
531
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
532
+ ]
533
+ },
534
+ {
535
+ "name": "stdout",
536
+ "output_type": "stream",
537
+ "text": [
538
+ "⠙ Processed 21 chunks, 220 entities(duplicated), 187 relations(duplicated)\r"
539
+ ]
540
+ },
541
+ {
542
+ "name": "stderr",
543
+ "output_type": "stream",
544
+ "text": [
545
+ "\n",
546
+ "Extracting entities from chunks: 60%|██████ | 21/35 [01:27<01:02, 4.47s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
547
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
548
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
549
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
550
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
551
+ ]
552
+ },
553
+ {
554
+ "name": "stdout",
555
+ "output_type": "stream",
556
+ "text": [
557
+ "⠹ Processed 22 chunks, 247 entities(duplicated), 216 relations(duplicated)\r"
558
+ ]
559
+ },
560
+ {
561
+ "name": "stderr",
562
+ "output_type": "stream",
563
+ "text": [
564
+ "\n",
565
+ "Extracting entities from chunks: 63%|██████▎ | 22/35 [01:30<00:54, 4.16s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
566
+ ]
567
+ },
568
+ {
569
+ "name": "stdout",
570
+ "output_type": "stream",
571
+ "text": [
572
+ "⠸ Processed 23 chunks, 260 entities(duplicated), 230 relations(duplicated)\r"
573
+ ]
574
+ },
575
+ {
576
+ "name": "stderr",
577
+ "output_type": "stream",
578
+ "text": [
579
+ "\n",
580
+ "Extracting entities from chunks: 66%|██████▌ | 23/35 [01:34<00:48, 4.05s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
581
+ ]
582
+ },
583
+ {
584
+ "name": "stdout",
585
+ "output_type": "stream",
586
+ "text": [
587
+ "⠼ Processed 24 chunks, 291 entities(duplicated), 253 relations(duplicated)\r"
588
+ ]
589
+ },
590
+ {
591
+ "name": "stderr",
592
+ "output_type": "stream",
593
+ "text": [
594
+ "\n",
595
+ "Extracting entities from chunks: 69%|██████▊ | 24/35 [01:38<00:44, 4.03s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
596
+ ]
597
+ },
598
+ {
599
+ "name": "stdout",
600
+ "output_type": "stream",
601
+ "text": [
602
+ "⠴ Processed 25 chunks, 304 entities(duplicated), 262 relations(duplicated)\r"
603
+ ]
604
+ },
605
+ {
606
+ "name": "stderr",
607
+ "output_type": "stream",
608
+ "text": [
609
+ "\n",
610
+ "Extracting entities from chunks: 71%|███████▏ | 25/35 [01:41<00:36, 3.67s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
611
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
612
+ ]
613
+ },
614
+ {
615
+ "name": "stdout",
616
+ "output_type": "stream",
617
+ "text": [
618
+ "⠦ Processed 26 chunks, 313 entities(duplicated), 271 relations(duplicated)\r"
619
+ ]
620
+ },
621
+ {
622
+ "name": "stderr",
623
+ "output_type": "stream",
624
+ "text": [
625
+ "\n",
626
+ "Extracting entities from chunks: 74%|███████▍ | 26/35 [01:41<00:24, 2.76s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
627
+ ]
628
+ },
629
+ {
630
+ "name": "stdout",
631
+ "output_type": "stream",
632
+ "text": [
633
+ "⠧ Processed 27 chunks, 321 entities(duplicated), 283 relations(duplicated)\r"
634
+ ]
635
+ },
636
+ {
637
+ "name": "stderr",
638
+ "output_type": "stream",
639
+ "text": [
640
+ "\n",
641
+ "Extracting entities from chunks: 77%|███████▋ | 27/35 [01:47<00:28, 3.52s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
642
+ ]
643
+ },
644
+ {
645
+ "name": "stdout",
646
+ "output_type": "stream",
647
+ "text": [
648
+ "⠇ Processed 28 chunks, 333 entities(duplicated), 290 relations(duplicated)\r"
649
+ ]
650
+ },
651
+ {
652
+ "name": "stderr",
653
+ "output_type": "stream",
654
+ "text": [
655
+ "\n",
656
+ "Extracting entities from chunks: 80%|████████ | 28/35 [01:52<00:28, 4.08s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
657
+ ]
658
+ },
659
+ {
660
+ "name": "stdout",
661
+ "output_type": "stream",
662
+ "text": [
663
+ "⠏ Processed 29 chunks, 348 entities(duplicated), 307 relations(duplicated)\r"
664
+ ]
665
+ },
666
+ {
667
+ "name": "stderr",
668
+ "output_type": "stream",
669
+ "text": [
670
+ "\n",
671
+ "Extracting entities from chunks: 83%|████████▎ | 29/35 [01:59<00:29, 4.88s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
672
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
673
+ ]
674
+ },
675
+ {
676
+ "name": "stdout",
677
+ "output_type": "stream",
678
+ "text": [
679
+ "⠋ Processed 30 chunks, 362 entities(duplicated), 329 relations(duplicated)\r"
680
+ ]
681
+ },
682
+ {
683
+ "name": "stderr",
684
+ "output_type": "stream",
685
+ "text": [
686
+ "\n",
687
+ "Extracting entities from chunks: 86%|████████▌ | 30/35 [02:02<00:21, 4.29s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
688
+ ]
689
+ },
690
+ {
691
+ "name": "stdout",
692
+ "output_type": "stream",
693
+ "text": [
694
+ "⠙ Processed 31 chunks, 373 entities(duplicated), 337 relations(duplicated)\r"
695
+ ]
696
+ },
697
+ {
698
+ "name": "stderr",
699
+ "output_type": "stream",
700
+ "text": [
701
+ "\n",
702
+ "Extracting entities from chunks: 89%|████████▊ | 31/35 [02:03<00:13, 3.28s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
703
+ ]
704
+ },
705
+ {
706
+ "name": "stdout",
707
+ "output_type": "stream",
708
+ "text": [
709
+ "⠹ Processed 32 chunks, 390 entities(duplicated), 369 relations(duplicated)\r"
710
+ ]
711
+ },
712
+ {
713
+ "name": "stderr",
714
+ "output_type": "stream",
715
+ "text": [
716
+ "\n",
717
+ "Extracting entities from chunks: 91%|█████████▏| 32/35 [02:03<00:07, 2.55s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
718
+ ]
719
+ },
720
+ {
721
+ "name": "stdout",
722
+ "output_type": "stream",
723
+ "text": [
724
+ "⠸ Processed 33 chunks, 405 entities(duplicated), 378 relations(duplicated)\r"
725
+ ]
726
+ },
727
+ {
728
+ "name": "stderr",
729
+ "output_type": "stream",
730
+ "text": [
731
+ "\n",
732
+ "Extracting entities from chunks: 94%|█████████▍| 33/35 [02:07<00:05, 2.84s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
733
+ ]
734
+ },
735
+ {
736
+ "name": "stdout",
737
+ "output_type": "stream",
738
+ "text": [
739
+ "⠼ Processed 34 chunks, 435 entities(duplicated), 395 relations(duplicated)\r"
740
+ ]
741
+ },
742
+ {
743
+ "name": "stderr",
744
+ "output_type": "stream",
745
+ "text": [
746
+ "\n",
747
+ "Extracting entities from chunks: 97%|█████████▋| 34/35 [02:10<00:02, 2.94s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
748
+ ]
749
+ },
750
+ {
751
+ "name": "stdout",
752
+ "output_type": "stream",
753
+ "text": [
754
+ "⠴ Processed 35 chunks, 456 entities(duplicated), 440 relations(duplicated)\r"
755
+ ]
756
+ },
757
+ {
758
+ "name": "stderr",
759
+ "output_type": "stream",
760
+ "text": [
761
+ "\n",
762
+ "Extracting entities from chunks: 100%|██████████| 35/35 [02:23<00:00, 4.10s/chunk]\u001b[A\n",
763
+ "INFO:lightrag:Inserting entities into storage...\n",
764
+ "\n",
765
+ "Inserting entities: 100%|██████████| 324/324 [00:00<00:00, 17456.96entity/s]\n",
766
+ "INFO:lightrag:Inserting relationships into storage...\n",
767
+ "\n",
768
+ "Inserting relationships: 100%|██████████| 427/427 [00:00<00:00, 29956.31relationship/s]\n",
769
+ "INFO:lightrag:Inserting 324 vectors to entities\n",
770
+ "\n",
771
+ "Generating embeddings: 0%| | 0/11 [00:00<?, ?batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
772
+ "\n",
773
+ "Generating embeddings: 9%|▉ | 1/11 [00:00<00:06, 1.48batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
774
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
775
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
776
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
777
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
778
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
779
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
780
+ "\n",
781
+ "Generating embeddings: 18%|█▊ | 2/11 [00:02<00:11, 1.25s/batch]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
782
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
783
+ "\n",
784
+ "Generating embeddings: 27%|██▋ | 3/11 [00:02<00:06, 1.17batch/s]\u001b[A\n",
785
+ "Generating embeddings: 36%|███▋ | 4/11 [00:03<00:04, 1.50batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
786
+ "\n",
787
+ "Generating embeddings: 45%|████▌ | 5/11 [00:03<00:03, 1.78batch/s]\u001b[A\n",
788
+ "Generating embeddings: 55%|█████▍ | 6/11 [00:03<00:02, 2.01batch/s]\u001b[A\n",
789
+ "Generating embeddings: 64%|██████▎ | 7/11 [00:04<00:01, 2.19batch/s]\u001b[A\n",
790
+ "Generating embeddings: 73%|███████▎ | 8/11 [00:04<00:01, 2.31batch/s]\u001b[A\n",
791
+ "Generating embeddings: 82%|████████▏ | 9/11 [00:04<00:00, 2.41batch/s]\u001b[A\n",
792
+ "Generating embeddings: 91%|█████████ | 10/11 [00:05<00:00, 2.48batch/s]\u001b[A\n",
793
+ "Generating embeddings: 100%|██████████| 11/11 [00:05<00:00, 1.91batch/s]\u001b[A\n",
794
+ "INFO:lightrag:Inserting 427 vectors to relationships\n",
795
+ "\n",
796
+ "Generating embeddings: 0%| | 0/14 [00:00<?, ?batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
797
+ "\n",
798
+ "Generating embeddings: 7%|▋ | 1/14 [00:01<00:14, 1.11s/batch]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
799
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
800
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
801
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
802
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
803
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
804
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
805
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
806
+ "\n",
807
+ "Generating embeddings: 14%|█▍ | 2/14 [00:02<00:14, 1.18s/batch]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
808
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
809
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
810
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
811
+ "\n",
812
+ "Generating embeddings: 21%|██▏ | 3/14 [00:02<00:08, 1.23batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
813
+ "\n",
814
+ "Generating embeddings: 29%|██▊ | 4/14 [00:03<00:06, 1.56batch/s]\u001b[A\n",
815
+ "Generating embeddings: 36%|███▌ | 5/14 [00:03<00:04, 1.85batch/s]\u001b[A\n",
816
+ "Generating embeddings: 43%|████▎ | 6/14 [00:03<00:03, 2.05batch/s]\u001b[A\n",
817
+ "Generating embeddings: 50%|█████ | 7/14 [00:04<00:03, 2.23batch/s]\u001b[A\n",
818
+ "Generating embeddings: 57%|█████▋ | 8/14 [00:04<00:02, 2.37batch/s]\u001b[A\n",
819
+ "Generating embeddings: 64%|██████▍ | 9/14 [00:04<00:02, 2.46batch/s]\u001b[A\n",
820
+ "Generating embeddings: 71%|███████▏ | 10/14 [00:05<00:01, 2.54batch/s]\u001b[A\n",
821
+ "Generating embeddings: 79%|███████▊ | 11/14 [00:05<00:01, 2.59batch/s]\u001b[A\n",
822
+ "Generating embeddings: 86%|████████▌ | 12/14 [00:06<00:00, 2.64batch/s]\u001b[A\n",
823
+ "Generating embeddings: 93%|█████████▎| 13/14 [00:06<00:00, 2.65batch/s]\u001b[A\n",
824
+ "Generating embeddings: 100%|██████████| 14/14 [00:06<00:00, 2.05batch/s]\u001b[A\n",
825
+ "INFO:lightrag:Writing graph with 333 nodes, 427 edges\n",
826
+ "Processing batch 1: 100%|██████████| 1/1 [02:45<00:00, 165.90s/it]\n"
827
+ ]
828
+ }
829
+ ],
830
+ "source": [
831
+ "# rag.insert(content)\n",
832
+ "rag.insert(content, split_by_character=\"\\n#\")"
833
+ ]
834
+ },
835
+ {
836
+ "cell_type": "code",
837
+ "execution_count": 9,
838
+ "id": "c4f9ae517151a01d",
839
+ "metadata": {
840
+ "ExecuteTime": {
841
+ "end_time": "2025-01-09T03:45:11.668987Z",
842
+ "start_time": "2025-01-09T03:45:11.664744Z"
843
+ }
844
+ },
845
+ "outputs": [],
846
+ "source": [
847
+ "prompt1 = \"\"\"你是一名经验丰富的论文分析科学家,你的任务是对一篇英文学术研究论文进行关键信息提取并深入分析。\n",
848
+ "请按照以下步骤进行分析:\n",
849
+ "1. 该文献主要研究的问题是什么?\n",
850
+ "2. 该文献采用什么方法进行分析?\n",
851
+ "3. 该文献的主要结论是什么?\n",
852
+ "首先在<分析>标签中,针对每个问题详��分析你的思考过程。然后在<回答>标签中给出所有问题的最终答案。\"\"\""
853
+ ]
854
+ },
855
+ {
856
+ "cell_type": "code",
857
+ "execution_count": 10,
858
+ "id": "7a6491385b050095",
859
+ "metadata": {
860
+ "ExecuteTime": {
861
+ "end_time": "2025-01-09T03:45:40.829111Z",
862
+ "start_time": "2025-01-09T03:45:13.530298Z"
863
+ }
864
+ },
865
+ "outputs": [
866
+ {
867
+ "name": "stderr",
868
+ "output_type": "stream",
869
+ "text": [
870
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
871
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
872
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
873
+ "INFO:lightrag:Local query uses 5 entites, 12 relations, 3 text units\n",
874
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
875
+ "INFO:lightrag:Global query uses 8 entites, 5 relations, 4 text units\n",
876
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
877
+ ]
878
+ },
879
+ {
880
+ "name": "stdout",
881
+ "output_type": "stream",
882
+ "text": [
883
+ "<分析>\n",
884
+ "1. **该文献主要研究的问题是什么?**\n",
885
+ " - 思考过程:通过浏览论文内容,查找作者明确阐述研究目的的部分。文中多处提及“Our study was performed to explore whether folic acid treatment was associated with cancer outcomes and all-cause mortality after extended follow-up”,表明作者旨在探究叶酸治疗与癌症结局及全因死亡率之间的关系,尤其是在经过长期随访后。\n",
886
+ "2. **该文献采用什么方法进行分析?**\n",
887
+ " - 思考过程:寻找描述研究方法和数据分析过程的段落。文中提到“Survival curves were constructed using the Kaplan-Meier method and differences in survival between groups were analyzed using the log-rank test. Estimates of hazard ratios (HRs) with 95% CIs were obtained by using Cox proportional hazards regression models stratified by trial”,可以看出作者使用了Kaplan-Meier法构建生存曲线、log-rank检验分析组间生存差异以及Cox比例风险回归模型估计风险比等方法。\n",
888
+ "3. **该文献的主要结论是什么?**\n",
889
+ " - 思考过程:定位到论文中总结结论的部分,如“Conclusion Treatment with folic acid plus vitamin $\\mathsf{B}_{12}$ was associated with increased cancer outcomes and all-cause mortality in patients with ischemic heart disease in Norway, where there is no folic acid fortification of foods”,可知作者得出叶酸加维生素$\\mathsf{B}_{12}$治疗与癌症结局和全因死亡率增加有关的结论。\n",
890
+ "<回答>\n",
891
+ "1. 该文献主要研究的问题是:叶酸治疗与癌症结局及全因死亡率之间的关系,尤其是在经过长期随访后,叶酸治疗是否与癌症结局和全因死亡率相关。\n",
892
+ "2. 该文献采用的分析方法包括:使用Kaplan-Meier法构建生存曲线、log-rank检验分析组间生存差异、Cox比例风险回归模型估计风险比等。\n",
893
+ "3. 该文献的主要结论是:在挪威没有叶酸强化食品的情况下,叶酸加维生素$\\mathsf{B}_{12}$治疗与缺血性心脏病患者的癌症结局和全因死亡率增加有关。\n",
894
+ "\n",
895
+ "**参考文献**\n",
896
+ "- [VD] In2Norwegianhomocysteine-lowering trialsamongpatientswithischemicheart disease, there was a statistically nonsignificantincreaseincancerincidenceinthe groupsassignedtofolicacidtreatment.15,16 Our study was performed to explore whetherfolicacidtreatmentwasassociatedwithcanceroutcomesandall-cause mortality after extended follow-up.\n",
897
+ "- [VD] Survivalcurveswereconstructedusing theKaplan-Meiermethodanddifferences insurvivalbetweengroupswereanalyzed usingthelog-ranktest.Estimatesofhazard ratios (HRs) with $95\\%$ CIs were obtainedbyusingCoxproportionalhazards regressionmodelsstratifiedbytrial.\n",
898
+ "- [VD] Conclusion Treatment with folic acid plus vitamin $\\mathsf{B}_{12}$ was associated with increased cancer outcomes and all-cause mortality in patients with ischemic heart disease in Norway, where there is no folic acid fortification of foods.\n"
899
+ ]
900
+ }
901
+ ],
902
+ "source": [
903
+ "resp = rag.query(prompt1, param=QueryParam(mode=\"mix\", top_k=5))\n",
904
+ "print(resp)"
905
+ ]
906
+ },
907
+ {
908
+ "cell_type": "markdown",
909
+ "id": "4e5bfad24cb721a8",
910
+ "metadata": {},
911
+ "source": "#### split by character only"
912
+ },
913
+ {
914
+ "cell_type": "code",
915
+ "execution_count": 11,
916
+ "id": "44e2992dc95f8ce0",
917
+ "metadata": {
918
+ "ExecuteTime": {
919
+ "end_time": "2025-01-09T03:47:40.988796Z",
920
+ "start_time": "2025-01-09T03:47:40.982648Z"
921
+ }
922
+ },
923
+ "outputs": [],
924
+ "source": [
925
+ "WORKING_DIR = \"../../llm_rag/paper_db/R000088_test2\"\n",
926
+ "if not os.path.exists(WORKING_DIR):\n",
927
+ " os.mkdir(WORKING_DIR)"
928
+ ]
929
+ },
930
+ {
931
+ "cell_type": "code",
932
+ "execution_count": 12,
933
+ "id": "62c63385d2d973d5",
934
+ "metadata": {
935
+ "ExecuteTime": {
936
+ "end_time": "2025-01-09T03:51:39.951329Z",
937
+ "start_time": "2025-01-09T03:49:15.218976Z"
938
+ }
939
+ },
940
+ "outputs": [
941
+ {
942
+ "name": "stderr",
943
+ "output_type": "stream",
944
+ "text": [
945
+ "INFO:lightrag:Logger initialized for working directory: ../../llm_rag/paper_db/R000088_test2\n",
946
+ "INFO:lightrag:Load KV llm_response_cache with 0 data\n",
947
+ "INFO:lightrag:Load KV full_docs with 0 data\n",
948
+ "INFO:lightrag:Load KV text_chunks with 0 data\n",
949
+ "INFO:nano-vectordb:Init {'embedding_dim': 4096, 'metric': 'cosine', 'storage_file': '../../llm_rag/paper_db/R000088_test2/vdb_entities.json'} 0 data\n",
950
+ "INFO:nano-vectordb:Init {'embedding_dim': 4096, 'metric': 'cosine', 'storage_file': '../../llm_rag/paper_db/R000088_test2/vdb_relationships.json'} 0 data\n",
951
+ "INFO:nano-vectordb:Init {'embedding_dim': 4096, 'metric': 'cosine', 'storage_file': '../../llm_rag/paper_db/R000088_test2/vdb_chunks.json'} 0 data\n",
952
+ "INFO:lightrag:Loaded document status storage with 0 records\n",
953
+ "INFO:lightrag:Processing 1 new unique documents\n",
954
+ "Processing batch 1: 0%| | 0/1 [00:00<?, ?it/s]INFO:lightrag:Inserting 12 vectors to chunks\n",
955
+ "\n",
956
+ "Generating embeddings: 0%| | 0/1 [00:00<?, ?batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
957
+ "\n",
958
+ "Generating embeddings: 100%|██████████| 1/1 [00:02<00:00, 2.95s/batch]\u001b[A\n",
959
+ "\n",
960
+ "Extracting entities from chunks: 0%| | 0/12 [00:00<?, ?chunk/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
961
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
962
+ ]
963
+ },
964
+ {
965
+ "name": "stdout",
966
+ "output_type": "stream",
967
+ "text": [
968
+ "⠙ Processed 1 chunks, 0 entities(duplicated), 0 relations(duplicated)\r"
969
+ ]
970
+ },
971
+ {
972
+ "name": "stderr",
973
+ "output_type": "stream",
974
+ "text": [
975
+ "\n",
976
+ "Extracting entities from chunks: 8%|▊ | 1/12 [00:03<00:43, 3.93s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
977
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
978
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
979
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
980
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
981
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
982
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
983
+ ]
984
+ },
985
+ {
986
+ "name": "stdout",
987
+ "output_type": "stream",
988
+ "text": [
989
+ "⠹ Processed 2 chunks, 8 entities(duplicated), 8 relations(duplicated)\r"
990
+ ]
991
+ },
992
+ {
993
+ "name": "stderr",
994
+ "output_type": "stream",
995
+ "text": [
996
+ "\n",
997
+ "Extracting entities from chunks: 17%|█▋ | 2/12 [00:29<02:44, 16.46s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
998
+ ]
999
+ },
1000
+ {
1001
+ "name": "stdout",
1002
+ "output_type": "stream",
1003
+ "text": [
1004
+ "⠸ Processed 3 chunks, 17 entities(duplicated), 15 relations(duplicated)\r"
1005
+ ]
1006
+ },
1007
+ {
1008
+ "name": "stderr",
1009
+ "output_type": "stream",
1010
+ "text": [
1011
+ "\n",
1012
+ "Extracting entities from chunks: 25%|██▌ | 3/12 [00:30<01:25, 9.45s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
1013
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1014
+ ]
1015
+ },
1016
+ {
1017
+ "name": "stdout",
1018
+ "output_type": "stream",
1019
+ "text": [
1020
+ "⠼ Processed 4 chunks, 27 entities(duplicated), 22 relations(duplicated)\r"
1021
+ ]
1022
+ },
1023
+ {
1024
+ "name": "stderr",
1025
+ "output_type": "stream",
1026
+ "text": [
1027
+ "\n",
1028
+ "Extracting entities from chunks: 33%|███▎ | 4/12 [00:39<01:16, 9.52s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1029
+ ]
1030
+ },
1031
+ {
1032
+ "name": "stdout",
1033
+ "output_type": "stream",
1034
+ "text": [
1035
+ "⠴ Processed 5 chunks, 36 entities(duplicated), 33 relations(duplicated)\r"
1036
+ ]
1037
+ },
1038
+ {
1039
+ "name": "stderr",
1040
+ "output_type": "stream",
1041
+ "text": [
1042
+ "\n",
1043
+ "Extracting entities from chunks: 42%|████▏ | 5/12 [00:40<00:43, 6.24s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
1044
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
1045
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1046
+ ]
1047
+ },
1048
+ {
1049
+ "name": "stdout",
1050
+ "output_type": "stream",
1051
+ "text": [
1052
+ "⠦ Processed 6 chunks, 49 entities(duplicated), 42 relations(duplicated)\r"
1053
+ ]
1054
+ },
1055
+ {
1056
+ "name": "stderr",
1057
+ "output_type": "stream",
1058
+ "text": [
1059
+ "\n",
1060
+ "Extracting entities from chunks: 50%|█████ | 6/12 [00:49<00:43, 7.33s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1061
+ ]
1062
+ },
1063
+ {
1064
+ "name": "stdout",
1065
+ "output_type": "stream",
1066
+ "text": [
1067
+ "⠧ Processed 7 chunks, 62 entities(duplicated), 65 relations(duplicated)\r"
1068
+ ]
1069
+ },
1070
+ {
1071
+ "name": "stderr",
1072
+ "output_type": "stream",
1073
+ "text": [
1074
+ "\n",
1075
+ "Extracting entities from chunks: 58%|█████▊ | 7/12 [01:05<00:50, 10.05s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
1076
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1077
+ ]
1078
+ },
1079
+ {
1080
+ "name": "stdout",
1081
+ "output_type": "stream",
1082
+ "text": [
1083
+ "⠇ Processed 8 chunks, 81 entities(duplicated), 90 relations(duplicated)\r"
1084
+ ]
1085
+ },
1086
+ {
1087
+ "name": "stderr",
1088
+ "output_type": "stream",
1089
+ "text": [
1090
+ "\n",
1091
+ "Extracting entities from chunks: 67%|██████▋ | 8/12 [01:23<00:50, 12.69s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1092
+ ]
1093
+ },
1094
+ {
1095
+ "name": "stdout",
1096
+ "output_type": "stream",
1097
+ "text": [
1098
+ "⠏ Processed 9 chunks, 99 entities(duplicated), 117 relations(duplicated)\r"
1099
+ ]
1100
+ },
1101
+ {
1102
+ "name": "stderr",
1103
+ "output_type": "stream",
1104
+ "text": [
1105
+ "\n",
1106
+ "Extracting entities from chunks: 75%|███████▌ | 9/12 [01:32<00:34, 11.54s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
1107
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1108
+ ]
1109
+ },
1110
+ {
1111
+ "name": "stdout",
1112
+ "output_type": "stream",
1113
+ "text": [
1114
+ "⠋ Processed 10 chunks, 123 entities(duplicated), 140 relations(duplicated)\r"
1115
+ ]
1116
+ },
1117
+ {
1118
+ "name": "stderr",
1119
+ "output_type": "stream",
1120
+ "text": [
1121
+ "\n",
1122
+ "Extracting entities from chunks: 83%|████████▎ | 10/12 [01:48<00:25, 12.79s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1123
+ ]
1124
+ },
1125
+ {
1126
+ "name": "stdout",
1127
+ "output_type": "stream",
1128
+ "text": [
1129
+ "⠙ Processed 11 chunks, 158 entities(duplicated), 174 relations(duplicated)\r"
1130
+ ]
1131
+ },
1132
+ {
1133
+ "name": "stderr",
1134
+ "output_type": "stream",
1135
+ "text": [
1136
+ "\n",
1137
+ "Extracting entities from chunks: 92%|█████████▏| 11/12 [02:03<00:13, 13.50s/chunk]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1138
+ ]
1139
+ },
1140
+ {
1141
+ "name": "stdout",
1142
+ "output_type": "stream",
1143
+ "text": [
1144
+ "⠹ Processed 12 chunks, 194 entities(duplicated), 221 relations(duplicated)\r"
1145
+ ]
1146
+ },
1147
+ {
1148
+ "name": "stderr",
1149
+ "output_type": "stream",
1150
+ "text": [
1151
+ "\n",
1152
+ "Extracting entities from chunks: 100%|██████████| 12/12 [02:13<00:00, 11.15s/chunk]\u001b[A\n",
1153
+ "INFO:lightrag:Inserting entities into storage...\n",
1154
+ "\n",
1155
+ "Inserting entities: 100%|██████████| 170/170 [00:00<00:00, 11610.25entity/s]\n",
1156
+ "INFO:lightrag:Inserting relationships into storage...\n",
1157
+ "\n",
1158
+ "Inserting relationships: 100%|██████████| 218/218 [00:00<00:00, 15913.51relationship/s]\n",
1159
+ "INFO:lightrag:Inserting 170 vectors to entities\n",
1160
+ "\n",
1161
+ "Generating embeddings: 0%| | 0/6 [00:00<?, ?batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1162
+ "\n",
1163
+ "Generating embeddings: 17%|█▋ | 1/6 [00:01<00:05, 1.10s/batch]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1164
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1165
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1166
+ "\n",
1167
+ "Generating embeddings: 33%|███▎ | 2/6 [00:02<00:04, 1.07s/batch]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1168
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1169
+ "\n",
1170
+ "Generating embeddings: 50%|█████ | 3/6 [00:02<00:02, 1.33batch/s]\u001b[A\n",
1171
+ "Generating embeddings: 67%|██████▋ | 4/6 [00:02<00:01, 1.67batch/s]\u001b[A\n",
1172
+ "Generating embeddings: 83%|████████▎ | 5/6 [00:03<00:00, 1.95batch/s]\u001b[A\n",
1173
+ "Generating embeddings: 100%|██████████| 6/6 [00:03<00:00, 1.66batch/s]\u001b[A\n",
1174
+ "INFO:lightrag:Inserting 218 vectors to relationships\n",
1175
+ "\n",
1176
+ "Generating embeddings: 0%| | 0/7 [00:00<?, ?batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1177
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1178
+ "\n",
1179
+ "Generating embeddings: 14%|█▍ | 1/7 [00:01<00:10, 1.74s/batch]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1180
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1181
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1182
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1183
+ "\n",
1184
+ "Generating embeddings: 29%|██▊ | 2/7 [00:02<00:05, 1.04s/batch]\u001b[A\n",
1185
+ "Generating embeddings: 43%|████▎ | 3/7 [00:02<00:02, 1.35batch/s]\u001b[A\n",
1186
+ "Generating embeddings: 57%|█████▋ | 4/7 [00:03<00:01, 1.69batch/s]\u001b[AINFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1187
+ "\n",
1188
+ "Generating embeddings: 71%|███████▏ | 5/7 [00:03<00:01, 1.96batch/s]\u001b[A\n",
1189
+ "Generating embeddings: 86%|████████▌ | 6/7 [00:03<00:00, 2.17batch/s]\u001b[A\n",
1190
+ "Generating embeddings: 100%|██████████| 7/7 [00:04<00:00, 1.68batch/s]\u001b[A\n",
1191
+ "INFO:lightrag:Writing graph with 174 nodes, 218 edges\n",
1192
+ "Processing batch 1: 100%|██████████| 1/1 [02:24<00:00, 144.69s/it]\n"
1193
+ ]
1194
+ }
1195
+ ],
1196
+ "source": [
1197
+ "rag = LightRAG(\n",
1198
+ " working_dir=WORKING_DIR,\n",
1199
+ " llm_model_func=llm_model_func,\n",
1200
+ " embedding_func=EmbeddingFunc(\n",
1201
+ " embedding_dim=4096, max_token_size=8192, func=embedding_func\n",
1202
+ " ),\n",
1203
+ " chunk_token_size=512,\n",
1204
+ ")\n",
1205
+ "\n",
1206
+ "# rag.insert(content)\n",
1207
+ "rag.insert(content, split_by_character=\"\\n#\", split_by_character_only=True)"
1208
+ ]
1209
+ },
1210
+ {
1211
+ "cell_type": "code",
1212
+ "execution_count": 13,
1213
+ "id": "3c7aa9836d8d43c7",
1214
+ "metadata": {
1215
+ "ExecuteTime": {
1216
+ "end_time": "2025-01-09T03:52:37.000418Z",
1217
+ "start_time": "2025-01-09T03:52:09.933584Z"
1218
+ }
1219
+ },
1220
+ "outputs": [
1221
+ {
1222
+ "name": "stderr",
1223
+ "output_type": "stream",
1224
+ "text": [
1225
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1226
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n",
1227
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1228
+ "INFO:lightrag:Local query uses 5 entites, 3 relations, 2 text units\n",
1229
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/embeddings \"HTTP/1.1 200 OK\"\n",
1230
+ "INFO:lightrag:Global query uses 9 entites, 5 relations, 4 text units\n",
1231
+ "INFO:httpx:HTTP Request: POST https://ark.cn-beijing.volces.com/api/v3/chat/completions \"HTTP/1.1 200 OK\"\n"
1232
+ ]
1233
+ },
1234
+ {
1235
+ "name": "stdout",
1236
+ "output_type": "stream",
1237
+ "text": [
1238
+ "<分析>\n",
1239
+ "- **该文献主要研究的问题是什么?**\n",
1240
+ " - **思考过程**:通过浏览论文的标题、摘要、引言等部分,寻找关于研究目的和问题的描述。论文标题为“Cancer Incidence and Mortality After Treatment With Folic Acid and Vitamin B12”,摘要中的“Objective”部分明确指出研究目的是“To evaluate effects of treatment with B vitamins on cancer outcomes and all-cause mortality in 2 randomized controlled trials”。因此,可以确定该文献主要研究的问题是评估B族维生素治疗对两项随机对照试验中癌症结局和全因死亡率的影响。\n",
1241
+ "- **该文献采用什么方法进行分析?**\n",
1242
+ " - **思考过程**:在论文的“METHODS”部分详细描述了研究方法。文中提到这是一个对两项随机、双盲、安慰剂对照临床试验(Norwegian Vitamin [NORVIT] trial和Western Norway B Vitamin Intervention Trial [WENBIT])数据的联合分析,并进行了观察性的试验后随访。具体包括对参与者进行分组干预(不同剂量的叶酸、维生素B12、维生素B6或安慰剂),收集临床信息和血样,分析循环B族维生素、同型半胱氨酸和可替宁等指标,并进行基因分型等,还涉及到多种统计分析方法,如计算预期癌症发生率、构建生存曲线、进行Cox比例风险回归模型分析等。\n",
1243
+ "- **该文献的主要结论是什么?**\n",
1244
+ " - **思考过程**:在论文的“Results”和“Conclusion”部分寻找主要结论。研究结果表明,在治疗期间,接受叶酸加维生素B12治疗的参与者血清叶酸浓度显著增加,且在后续随访中,该组癌症发病率、癌症死亡率和全因死亡率均有所上升,主要是肺癌发病率增加,而维生素B6治疗未显示出显著影响。结论部分明确指出“Treatment with folic acid plus vitamin $\\mathsf{B}_{12}$ was associated with increased cancer outcomes and all-cause mortality in patients with ischemic heart disease in Norway, where there is no folic acid fortification of foods”。\n",
1245
+ "</分析>\n",
1246
+ "\n",
1247
+ "<回答>\n",
1248
+ "- **主要研究问题**:评估B族维生素治疗对两项随机对照试验中癌症结局和全因死亡率的影响。\n",
1249
+ "- **研究方法**:采用对两项随机、双盲、安慰剂对照临床试验(Norwegian Vitamin [NORVIT] trial和Western Norway B Vitamin Intervention Trial [WENBIT])数据的联合分析,并进行观察性的试验后随访,涉及分组干预、多种指标检测以及多种统计分析方法。\n",
1250
+ "- **主要结论**:在挪威(食品中未添加叶酸),对于缺血性心脏病患者,叶酸加维生素B12治疗与癌症结局和全因死亡率的增加有关,而维生素B6治疗未显示出显著影响。\n",
1251
+ "\n",
1252
+ "**参考文献**\n",
1253
+ "- [VD] Cancer Incidence and Mortality After Treatment With Folic Acid and Vitamin B12\n",
1254
+ "- [VD] METHODS Study Design, Participants, and Study Intervention\n",
1255
+ "- [VD] RESULTS\n",
1256
+ "- [VD] Conclusion\n",
1257
+ "- [VD] Objective To evaluate effects of treatment with B vitamins on cancer outcomes and all-cause mortality in 2 randomized controlled trials.\n"
1258
+ ]
1259
+ }
1260
+ ],
1261
+ "source": [
1262
+ "resp = rag.query(prompt1, param=QueryParam(mode=\"mix\", top_k=5))\n",
1263
+ "print(resp)"
1264
+ ]
1265
+ },
1266
+ {
1267
+ "cell_type": "code",
1268
+ "execution_count": null,
1269
+ "id": "7ba6fa79a2550d10",
1270
+ "metadata": {},
1271
+ "outputs": [],
1272
+ "source": []
1273
+ }
1274
+ ],
1275
+ "metadata": {
1276
+ "kernelspec": {
1277
+ "display_name": "Python 3",
1278
+ "language": "python",
1279
+ "name": "python3"
1280
+ },
1281
+ "language_info": {
1282
+ "codemirror_mode": {
1283
+ "name": "ipython",
1284
+ "version": 2
1285
+ },
1286
+ "file_extension": ".py",
1287
+ "mimetype": "text/x-python",
1288
+ "name": "python",
1289
+ "nbconvert_exporter": "python",
1290
+ "pygments_lexer": "ipython2",
1291
+ "version": "2.7.6"
1292
+ }
1293
+ },
1294
+ "nbformat": 4,
1295
+ "nbformat_minor": 5
1296
+ }
lightrag/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
2
 
3
- __version__ = "1.1.0"
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.1.1"
4
  __author__ = "Zirui Guo"
5
  __url__ = "https://github.com/HKUDS/LightRAG"
lightrag/api/azure_openai_lightrag_server.py DELETED
@@ -1,532 +0,0 @@
1
- from fastapi import FastAPI, HTTPException, File, UploadFile, Form
2
- from pydantic import BaseModel
3
- import asyncio
4
- import logging
5
- import argparse
6
- from lightrag import LightRAG, QueryParam
7
- from lightrag.llm import (
8
- azure_openai_complete_if_cache,
9
- azure_openai_embedding,
10
- )
11
- from lightrag.utils import EmbeddingFunc
12
- from typing import Optional, List
13
- from enum import Enum
14
- from pathlib import Path
15
- import shutil
16
- import aiofiles
17
- from ascii_colors import trace_exception
18
- import os
19
- from dotenv import load_dotenv
20
- import inspect
21
- import json
22
- from fastapi.responses import StreamingResponse
23
-
24
- from fastapi import Depends, Security
25
- from fastapi.security import APIKeyHeader
26
- from fastapi.middleware.cors import CORSMiddleware
27
-
28
- from starlette.status import HTTP_403_FORBIDDEN
29
-
30
- load_dotenv()
31
-
32
- AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION")
33
- AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT")
34
- AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
35
- AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
36
-
37
- AZURE_EMBEDDING_DEPLOYMENT = os.getenv("AZURE_EMBEDDING_DEPLOYMENT")
38
- AZURE_EMBEDDING_API_VERSION = os.getenv("AZURE_EMBEDDING_API_VERSION")
39
-
40
-
41
- def parse_args():
42
- parser = argparse.ArgumentParser(
43
- description="LightRAG FastAPI Server with OpenAI integration"
44
- )
45
-
46
- # Server configuration
47
- parser.add_argument(
48
- "--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
49
- )
50
- parser.add_argument(
51
- "--port", type=int, default=9621, help="Server port (default: 9621)"
52
- )
53
-
54
- # Directory configuration
55
- parser.add_argument(
56
- "--working-dir",
57
- default="./rag_storage",
58
- help="Working directory for RAG storage (default: ./rag_storage)",
59
- )
60
- parser.add_argument(
61
- "--input-dir",
62
- default="./inputs",
63
- help="Directory containing input documents (default: ./inputs)",
64
- )
65
-
66
- # Model configuration
67
- parser.add_argument(
68
- "--model", default="gpt-4o", help="OpenAI model name (default: gpt-4o)"
69
- )
70
- parser.add_argument(
71
- "--embedding-model",
72
- default="text-embedding-3-large",
73
- help="OpenAI embedding model (default: text-embedding-3-large)",
74
- )
75
-
76
- # RAG configuration
77
- parser.add_argument(
78
- "--max-tokens",
79
- type=int,
80
- default=32768,
81
- help="Maximum token size (default: 32768)",
82
- )
83
- parser.add_argument(
84
- "--max-embed-tokens",
85
- type=int,
86
- default=8192,
87
- help="Maximum embedding token size (default: 8192)",
88
- )
89
- parser.add_argument(
90
- "--enable-cache",
91
- default=True,
92
- help="Enable response cache (default: True)",
93
- )
94
- # Logging configuration
95
- parser.add_argument(
96
- "--log-level",
97
- default="INFO",
98
- choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
99
- help="Logging level (default: INFO)",
100
- )
101
-
102
- parser.add_argument(
103
- "--key",
104
- type=str,
105
- help="API key for authentication. This protects lightrag server against unauthorized access",
106
- default=None,
107
- )
108
-
109
- return parser.parse_args()
110
-
111
-
112
- class DocumentManager:
113
- """Handles document operations and tracking"""
114
-
115
- def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
116
- self.input_dir = Path(input_dir)
117
- self.supported_extensions = supported_extensions
118
- self.indexed_files = set()
119
-
120
- # Create input directory if it doesn't exist
121
- self.input_dir.mkdir(parents=True, exist_ok=True)
122
-
123
- def scan_directory(self) -> List[Path]:
124
- """Scan input directory for new files"""
125
- new_files = []
126
- for ext in self.supported_extensions:
127
- for file_path in self.input_dir.rglob(f"*{ext}"):
128
- if file_path not in self.indexed_files:
129
- new_files.append(file_path)
130
- return new_files
131
-
132
- def mark_as_indexed(self, file_path: Path):
133
- """Mark a file as indexed"""
134
- self.indexed_files.add(file_path)
135
-
136
- def is_supported_file(self, filename: str) -> bool:
137
- """Check if file type is supported"""
138
- return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
139
-
140
-
141
- # Pydantic models
142
- class SearchMode(str, Enum):
143
- naive = "naive"
144
- local = "local"
145
- global_ = "global"
146
- hybrid = "hybrid"
147
-
148
-
149
- class QueryRequest(BaseModel):
150
- query: str
151
- mode: SearchMode = SearchMode.hybrid
152
- only_need_context: bool = False
153
- # stream: bool = False
154
-
155
-
156
- class QueryResponse(BaseModel):
157
- response: str
158
-
159
-
160
- class InsertTextRequest(BaseModel):
161
- text: str
162
- description: Optional[str] = None
163
-
164
-
165
- class InsertResponse(BaseModel):
166
- status: str
167
- message: str
168
- document_count: int
169
-
170
-
171
- def get_api_key_dependency(api_key: Optional[str]):
172
- if not api_key:
173
- # If no API key is configured, return a dummy dependency that always succeeds
174
- async def no_auth():
175
- return None
176
-
177
- return no_auth
178
-
179
- # If API key is configured, use proper authentication
180
- api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
181
-
182
- async def api_key_auth(api_key_header_value: str | None = Security(api_key_header)):
183
- if not api_key_header_value:
184
- raise HTTPException(
185
- status_code=HTTP_403_FORBIDDEN, detail="API Key required"
186
- )
187
- if api_key_header_value != api_key:
188
- raise HTTPException(
189
- status_code=HTTP_403_FORBIDDEN, detail="Invalid API Key"
190
- )
191
- return api_key_header_value
192
-
193
- return api_key_auth
194
-
195
-
196
- async def get_embedding_dim(embedding_model: str) -> int:
197
- """Get embedding dimensions for the specified model"""
198
- test_text = ["This is a test sentence."]
199
- embedding = await azure_openai_embedding(test_text, model=embedding_model)
200
- return embedding.shape[1]
201
-
202
-
203
- def create_app(args):
204
- # Setup logging
205
- logging.basicConfig(
206
- format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
207
- )
208
-
209
- # Check if API key is provided either through env var or args
210
- api_key = os.getenv("LIGHTRAG_API_KEY") or args.key
211
-
212
- # Initialize FastAPI
213
- app = FastAPI(
214
- title="LightRAG API",
215
- description="API for querying text using LightRAG with separate storage and input directories"
216
- + "(With authentication)"
217
- if api_key
218
- else "",
219
- version="1.0.0",
220
- openapi_tags=[{"name": "api"}],
221
- )
222
-
223
- # Add CORS middleware
224
- app.add_middleware(
225
- CORSMiddleware,
226
- allow_origins=["*"],
227
- allow_credentials=True,
228
- allow_methods=["*"],
229
- allow_headers=["*"],
230
- )
231
-
232
- # Create the optional API key dependency
233
- optional_api_key = get_api_key_dependency(api_key)
234
-
235
- # Create working directory if it doesn't exist
236
- Path(args.working_dir).mkdir(parents=True, exist_ok=True)
237
-
238
- # Initialize document manager
239
- doc_manager = DocumentManager(args.input_dir)
240
-
241
- # Get embedding dimensions
242
- embedding_dim = asyncio.run(get_embedding_dim(args.embedding_model))
243
-
244
- async def async_openai_complete(
245
- prompt, system_prompt=None, history_messages=[], **kwargs
246
- ):
247
- """Async wrapper for OpenAI completion"""
248
- kwargs.pop("keyword_extraction", None)
249
-
250
- return await azure_openai_complete_if_cache(
251
- args.model,
252
- prompt,
253
- system_prompt=system_prompt,
254
- history_messages=history_messages,
255
- base_url=AZURE_OPENAI_ENDPOINT,
256
- api_key=AZURE_OPENAI_API_KEY,
257
- api_version=AZURE_OPENAI_API_VERSION,
258
- **kwargs,
259
- )
260
-
261
- # Initialize RAG with OpenAI configuration
262
- rag = LightRAG(
263
- enable_llm_cache=args.enable_cache,
264
- working_dir=args.working_dir,
265
- llm_model_func=async_openai_complete,
266
- llm_model_name=args.model,
267
- llm_model_max_token_size=args.max_tokens,
268
- embedding_func=EmbeddingFunc(
269
- embedding_dim=embedding_dim,
270
- max_token_size=args.max_embed_tokens,
271
- func=lambda texts: azure_openai_embedding(
272
- texts, model=args.embedding_model
273
- ),
274
- ),
275
- )
276
-
277
- @app.on_event("startup")
278
- async def startup_event():
279
- """Index all files in input directory during startup"""
280
- try:
281
- new_files = doc_manager.scan_directory()
282
- for file_path in new_files:
283
- try:
284
- # Use async file reading
285
- async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
286
- content = await f.read()
287
- # Use the async version of insert directly
288
- await rag.ainsert(content)
289
- doc_manager.mark_as_indexed(file_path)
290
- logging.info(f"Indexed file: {file_path}")
291
- except Exception as e:
292
- trace_exception(e)
293
- logging.error(f"Error indexing file {file_path}: {str(e)}")
294
-
295
- logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
296
-
297
- except Exception as e:
298
- logging.error(f"Error during startup indexing: {str(e)}")
299
-
300
- @app.post("/documents/scan", dependencies=[Depends(optional_api_key)])
301
- async def scan_for_new_documents():
302
- """Manually trigger scanning for new documents"""
303
- try:
304
- new_files = doc_manager.scan_directory()
305
- indexed_count = 0
306
-
307
- for file_path in new_files:
308
- try:
309
- with open(file_path, "r", encoding="utf-8") as f:
310
- content = f.read()
311
- await rag.ainsert(content)
312
- doc_manager.mark_as_indexed(file_path)
313
- indexed_count += 1
314
- except Exception as e:
315
- logging.error(f"Error indexing file {file_path}: {str(e)}")
316
-
317
- return {
318
- "status": "success",
319
- "indexed_count": indexed_count,
320
- "total_documents": len(doc_manager.indexed_files),
321
- }
322
- except Exception as e:
323
- raise HTTPException(status_code=500, detail=str(e))
324
-
325
- @app.post("/resetcache", dependencies=[Depends(optional_api_key)])
326
- async def reset_cache():
327
- """Manually reset cache"""
328
- try:
329
- cachefile = args.working_dir + "/kv_store_llm_response_cache.json"
330
- if os.path.exists(cachefile):
331
- with open(cachefile, "w") as f:
332
- f.write("{}")
333
- return {"status": "success"}
334
- except Exception as e:
335
- raise HTTPException(status_code=500, detail=str(e))
336
-
337
- @app.post("/documents/upload", dependencies=[Depends(optional_api_key)])
338
- async def upload_to_input_dir(file: UploadFile = File(...)):
339
- """Upload a file to the input directory"""
340
- try:
341
- if not doc_manager.is_supported_file(file.filename):
342
- raise HTTPException(
343
- status_code=400,
344
- detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
345
- )
346
-
347
- file_path = doc_manager.input_dir / file.filename
348
- with open(file_path, "wb") as buffer:
349
- shutil.copyfileobj(file.file, buffer)
350
-
351
- # Immediately index the uploaded file
352
- with open(file_path, "r", encoding="utf-8") as f:
353
- content = f.read()
354
- await rag.ainsert(content)
355
- doc_manager.mark_as_indexed(file_path)
356
-
357
- return {
358
- "status": "success",
359
- "message": f"File uploaded and indexed: {file.filename}",
360
- "total_documents": len(doc_manager.indexed_files),
361
- }
362
- except Exception as e:
363
- raise HTTPException(status_code=500, detail=str(e))
364
-
365
- @app.post(
366
- "/query", response_model=QueryResponse, dependencies=[Depends(optional_api_key)]
367
- )
368
- async def query_text(request: QueryRequest):
369
- try:
370
- response = await rag.aquery(
371
- request.query,
372
- param=QueryParam(
373
- mode=request.mode,
374
- stream=False,
375
- only_need_context=request.only_need_context,
376
- ),
377
- )
378
- return QueryResponse(response=response)
379
- except Exception as e:
380
- raise HTTPException(status_code=500, detail=str(e))
381
-
382
- @app.post("/query/stream", dependencies=[Depends(optional_api_key)])
383
- async def query_text_stream(request: QueryRequest):
384
- try:
385
- response = await rag.aquery(
386
- request.query,
387
- param=QueryParam(
388
- mode=request.mode,
389
- stream=True,
390
- only_need_context=request.only_need_context,
391
- ),
392
- )
393
- if inspect.isasyncgen(response):
394
-
395
- async def stream_generator():
396
- async for chunk in response:
397
- yield json.dumps({"data": chunk}) + "\n"
398
-
399
- return StreamingResponse(
400
- stream_generator(), media_type="application/json"
401
- )
402
- else:
403
- return QueryResponse(response=response)
404
-
405
- except Exception as e:
406
- raise HTTPException(status_code=500, detail=str(e))
407
-
408
- @app.post(
409
- "/documents/text",
410
- response_model=InsertResponse,
411
- dependencies=[Depends(optional_api_key)],
412
- )
413
- async def insert_text(request: InsertTextRequest):
414
- try:
415
- await rag.ainsert(request.text)
416
- return InsertResponse(
417
- status="success",
418
- message="Text successfully inserted",
419
- document_count=1,
420
- )
421
- except Exception as e:
422
- raise HTTPException(status_code=500, detail=str(e))
423
-
424
- @app.post(
425
- "/documents/file",
426
- response_model=InsertResponse,
427
- dependencies=[Depends(optional_api_key)],
428
- )
429
- async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
430
- try:
431
- content = await file.read()
432
-
433
- if file.filename.endswith((".txt", ".md")):
434
- text = content.decode("utf-8")
435
- rag.insert(text)
436
- else:
437
- raise HTTPException(
438
- status_code=400,
439
- detail="Unsupported file type. Only .txt and .md files are supported",
440
- )
441
-
442
- return InsertResponse(
443
- status="success",
444
- message=f"File '{file.filename}' successfully inserted",
445
- document_count=1,
446
- )
447
- except UnicodeDecodeError:
448
- raise HTTPException(status_code=400, detail="File encoding not supported")
449
- except Exception as e:
450
- raise HTTPException(status_code=500, detail=str(e))
451
-
452
- @app.post(
453
- "/documents/batch",
454
- response_model=InsertResponse,
455
- dependencies=[Depends(optional_api_key)],
456
- )
457
- async def insert_batch(files: List[UploadFile] = File(...)):
458
- try:
459
- inserted_count = 0
460
- failed_files = []
461
-
462
- for file in files:
463
- try:
464
- content = await file.read()
465
- if file.filename.endswith((".txt", ".md")):
466
- text = content.decode("utf-8")
467
- rag.insert(text)
468
- inserted_count += 1
469
- else:
470
- failed_files.append(f"{file.filename} (unsupported type)")
471
- except Exception as e:
472
- failed_files.append(f"{file.filename} ({str(e)})")
473
-
474
- status_message = f"Successfully inserted {inserted_count} documents"
475
- if failed_files:
476
- status_message += f". Failed files: {', '.join(failed_files)}"
477
-
478
- return InsertResponse(
479
- status="success" if inserted_count > 0 else "partial_success",
480
- message=status_message,
481
- document_count=len(files),
482
- )
483
- except Exception as e:
484
- raise HTTPException(status_code=500, detail=str(e))
485
-
486
- @app.delete(
487
- "/documents",
488
- response_model=InsertResponse,
489
- dependencies=[Depends(optional_api_key)],
490
- )
491
- async def clear_documents():
492
- try:
493
- rag.text_chunks = []
494
- rag.entities_vdb = None
495
- rag.relationships_vdb = None
496
- return InsertResponse(
497
- status="success",
498
- message="All documents cleared successfully",
499
- document_count=0,
500
- )
501
- except Exception as e:
502
- raise HTTPException(status_code=500, detail=str(e))
503
-
504
- @app.get("/health", dependencies=[Depends(optional_api_key)])
505
- async def get_status():
506
- """Get current system status"""
507
- return {
508
- "status": "healthy",
509
- "working_directory": str(args.working_dir),
510
- "input_directory": str(args.input_dir),
511
- "indexed_files": len(doc_manager.indexed_files),
512
- "configuration": {
513
- "model": args.model,
514
- "embedding_model": args.embedding_model,
515
- "max_tokens": args.max_tokens,
516
- "embedding_dim": embedding_dim,
517
- },
518
- }
519
-
520
- return app
521
-
522
-
523
- def main():
524
- args = parse_args()
525
- import uvicorn
526
-
527
- app = create_app(args)
528
- uvicorn.run(app, host=args.host, port=args.port)
529
-
530
-
531
- if __name__ == "__main__":
532
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lightrag/api/{lollms_lightrag_server.py → lightrag_server.py} RENAMED
@@ -4,8 +4,12 @@ import logging
4
  import argparse
5
  from lightrag import LightRAG, QueryParam
6
  from lightrag.llm import lollms_model_complete, lollms_embed
 
 
 
 
7
  from lightrag.utils import EmbeddingFunc
8
- from typing import Optional, List
9
  from enum import Enum
10
  from pathlib import Path
11
  import shutil
@@ -18,6 +22,19 @@ from fastapi.security import APIKeyHeader
18
  from fastapi.middleware.cors import CORSMiddleware
19
 
20
  from starlette.status import HTTP_403_FORBIDDEN
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  def parse_args():
@@ -25,6 +42,22 @@ def parse_args():
25
  description="LightRAG FastAPI Server with separate working and input directories"
26
  )
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Server configuration
29
  parser.add_argument(
30
  "--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
@@ -45,23 +78,45 @@ def parse_args():
45
  help="Directory containing input documents (default: ./inputs)",
46
  )
47
 
48
- # Model configuration
 
 
 
 
 
 
 
49
  parser.add_argument(
50
- "--model",
51
  default="mistral-nemo:latest",
52
  help="LLM model name (default: mistral-nemo:latest)",
53
  )
 
 
 
 
 
 
 
 
 
54
  parser.add_argument(
55
  "--embedding-model",
56
  default="bge-m3:latest",
57
  help="Embedding model name (default: bge-m3:latest)",
58
  )
 
 
 
 
 
 
59
  parser.add_argument(
60
- "--lollms-host",
61
- default="http://localhost:9600",
62
- help="lollms host URL (default: http://localhost:9600)",
 
63
  )
64
-
65
  # RAG configuration
66
  parser.add_argument(
67
  "--max-async", type=int, default=4, help="Maximum async operations (default: 4)"
@@ -100,13 +155,31 @@ def parse_args():
100
  default=None,
101
  )
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  return parser.parse_args()
104
 
105
 
106
  class DocumentManager:
107
  """Handles document operations and tracking"""
108
 
109
- def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
 
 
 
 
110
  self.input_dir = Path(input_dir)
111
  self.supported_extensions = supported_extensions
112
  self.indexed_files = set()
@@ -188,6 +261,24 @@ def get_api_key_dependency(api_key: Optional[str]):
188
 
189
 
190
  def create_app(args):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  # Setup logging
192
  logging.basicConfig(
193
  format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
@@ -203,7 +294,7 @@ def create_app(args):
203
  + "(With authentication)"
204
  if api_key
205
  else "",
206
- version="1.0.0",
207
  openapi_tags=[{"name": "api"}],
208
  )
209
 
@@ -228,23 +319,122 @@ def create_app(args):
228
  # Initialize RAG
229
  rag = LightRAG(
230
  working_dir=args.working_dir,
231
- llm_model_func=lollms_model_complete,
232
- llm_model_name=args.model,
 
 
 
 
 
 
233
  llm_model_max_async=args.max_async,
234
  llm_model_max_token_size=args.max_tokens,
235
  llm_model_kwargs={
236
- "host": args.lollms_host,
 
237
  "options": {"num_ctx": args.max_tokens},
238
  },
239
  embedding_func=EmbeddingFunc(
240
  embedding_dim=args.embedding_dim,
241
  max_token_size=args.max_embed_tokens,
242
  func=lambda texts: lollms_embed(
243
- texts, embed_model=args.embedding_model, host=args.lollms_host
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  ),
245
  ),
246
  )
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  @app.on_event("startup")
249
  async def startup_event():
250
  """Index all files in input directory during startup"""
@@ -252,13 +442,7 @@ def create_app(args):
252
  new_files = doc_manager.scan_directory()
253
  for file_path in new_files:
254
  try:
255
- # Use async file reading
256
- async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
257
- content = await f.read()
258
- # Use the async version of insert directly
259
- await rag.ainsert(content)
260
- doc_manager.mark_as_indexed(file_path)
261
- logging.info(f"Indexed file: {file_path}")
262
  except Exception as e:
263
  trace_exception(e)
264
  logging.error(f"Error indexing file {file_path}: {str(e)}")
@@ -277,11 +461,8 @@ def create_app(args):
277
 
278
  for file_path in new_files:
279
  try:
280
- with open(file_path, "r", encoding="utf-8") as f:
281
- content = f.read()
282
- await rag.ainsert(content)
283
- doc_manager.mark_as_indexed(file_path)
284
- indexed_count += 1
285
  except Exception as e:
286
  logging.error(f"Error indexing file {file_path}: {str(e)}")
287
 
@@ -308,10 +489,7 @@ def create_app(args):
308
  shutil.copyfileobj(file.file, buffer)
309
 
310
  # Immediately index the uploaded file
311
- with open(file_path, "r", encoding="utf-8") as f:
312
- content = f.read()
313
- await rag.ainsert(content)
314
- doc_manager.mark_as_indexed(file_path)
315
 
316
  return {
317
  "status": "success",
@@ -372,11 +550,11 @@ def create_app(args):
372
  )
373
  async def insert_text(request: InsertTextRequest):
374
  try:
375
- rag.insert(request.text)
376
  return InsertResponse(
377
  status="success",
378
  message="Text successfully inserted",
379
- document_count=len(rag),
380
  )
381
  except Exception as e:
382
  raise HTTPException(status_code=500, detail=str(e))
@@ -387,26 +565,103 @@ def create_app(args):
387
  dependencies=[Depends(optional_api_key)],
388
  )
389
  async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
 
 
 
 
 
 
 
 
 
 
 
 
390
  try:
391
- content = await file.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
- if file.filename.endswith((".txt", ".md")):
394
- text = content.decode("utf-8")
395
- await rag.ainsert(text)
 
 
396
  else:
397
  raise HTTPException(
398
  status_code=400,
399
- detail="Unsupported file type. Only .txt and .md files are supported",
400
  )
401
 
402
- return InsertResponse(
403
- status="success",
404
- message=f"File '{file.filename}' successfully inserted",
405
- document_count=1,
406
- )
407
  except UnicodeDecodeError:
408
  raise HTTPException(status_code=400, detail="File encoding not supported")
409
  except Exception as e:
 
410
  raise HTTPException(status_code=500, detail=str(e))
411
 
412
  @app.post(
@@ -415,32 +670,110 @@ def create_app(args):
415
  dependencies=[Depends(optional_api_key)],
416
  )
417
  async def insert_batch(files: List[UploadFile] = File(...)):
 
 
 
 
 
 
 
 
 
 
 
418
  try:
419
  inserted_count = 0
420
  failed_files = []
421
 
422
  for file in files:
423
  try:
424
- content = await file.read()
425
- if file.filename.endswith((".txt", ".md")):
426
- text = content.decode("utf-8")
427
- await rag.ainsert(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  inserted_count += 1
 
429
  else:
430
- failed_files.append(f"{file.filename} (unsupported type)")
 
 
 
431
  except Exception as e:
432
  failed_files.append(f"{file.filename} ({str(e)})")
433
-
434
- status_message = f"Successfully inserted {inserted_count} documents"
435
- if failed_files:
436
- status_message += f". Failed files: {', '.join(failed_files)}"
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
  return InsertResponse(
439
- status="success" if inserted_count > 0 else "partial_success",
440
  message=status_message,
441
- document_count=len(files),
442
  )
 
443
  except Exception as e:
 
444
  raise HTTPException(status_code=500, detail=str(e))
445
 
446
  @app.delete(
@@ -470,10 +803,15 @@ def create_app(args):
470
  "input_directory": str(args.input_dir),
471
  "indexed_files": len(doc_manager.indexed_files),
472
  "configuration": {
473
- "model": args.model,
 
 
 
 
 
 
474
  "embedding_model": args.embedding_model,
475
  "max_tokens": args.max_tokens,
476
- "lollms_host": args.lollms_host,
477
  },
478
  }
479
 
@@ -485,7 +823,19 @@ def main():
485
  import uvicorn
486
 
487
  app = create_app(args)
488
- uvicorn.run(app, host=args.host, port=args.port)
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
 
491
  if __name__ == "__main__":
 
4
  import argparse
5
  from lightrag import LightRAG, QueryParam
6
  from lightrag.llm import lollms_model_complete, lollms_embed
7
+ from lightrag.llm import ollama_model_complete, ollama_embed
8
+ from lightrag.llm import openai_complete_if_cache, openai_embedding
9
+ from lightrag.llm import azure_openai_complete_if_cache, azure_openai_embedding
10
+
11
  from lightrag.utils import EmbeddingFunc
12
+ from typing import Optional, List, Union
13
  from enum import Enum
14
  from pathlib import Path
15
  import shutil
 
22
  from fastapi.middleware.cors import CORSMiddleware
23
 
24
  from starlette.status import HTTP_403_FORBIDDEN
25
+ import pipmaster as pm
26
+
27
+
28
+ def get_default_host(binding_type: str) -> str:
29
+ default_hosts = {
30
+ "ollama": "http://localhost:11434",
31
+ "lollms": "http://localhost:9600",
32
+ "azure_openai": "https://api.openai.com/v1",
33
+ "openai": "https://api.openai.com/v1",
34
+ }
35
+ return default_hosts.get(
36
+ binding_type, "http://localhost:11434"
37
+ ) # fallback to ollama if unknown
38
 
39
 
40
  def parse_args():
 
42
  description="LightRAG FastAPI Server with separate working and input directories"
43
  )
44
 
45
+ # Start by the bindings
46
+ parser.add_argument(
47
+ "--llm-binding",
48
+ default="ollama",
49
+ help="LLM binding to be used. Supported: lollms, ollama, openai (default: ollama)",
50
+ )
51
+ parser.add_argument(
52
+ "--embedding-binding",
53
+ default="ollama",
54
+ help="Embedding binding to be used. Supported: lollms, ollama, openai (default: ollama)",
55
+ )
56
+
57
+ # Parse just these arguments first
58
+ temp_args, _ = parser.parse_known_args()
59
+
60
+ # Add remaining arguments with dynamic defaults for hosts
61
  # Server configuration
62
  parser.add_argument(
63
  "--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
 
78
  help="Directory containing input documents (default: ./inputs)",
79
  )
80
 
81
+ # LLM Model configuration
82
+ default_llm_host = get_default_host(temp_args.llm_binding)
83
+ parser.add_argument(
84
+ "--llm-binding-host",
85
+ default=default_llm_host,
86
+ help=f"llm server host URL (default: {default_llm_host})",
87
+ )
88
+
89
  parser.add_argument(
90
+ "--llm-model",
91
  default="mistral-nemo:latest",
92
  help="LLM model name (default: mistral-nemo:latest)",
93
  )
94
+
95
+ # Embedding model configuration
96
+ default_embedding_host = get_default_host(temp_args.embedding_binding)
97
+ parser.add_argument(
98
+ "--embedding-binding-host",
99
+ default=default_embedding_host,
100
+ help=f"embedding server host URL (default: {default_embedding_host})",
101
+ )
102
+
103
  parser.add_argument(
104
  "--embedding-model",
105
  default="bge-m3:latest",
106
  help="Embedding model name (default: bge-m3:latest)",
107
  )
108
+
109
+ def timeout_type(value):
110
+ if value is None or value == "None":
111
+ return None
112
+ return int(value)
113
+
114
  parser.add_argument(
115
+ "--timeout",
116
+ default=None,
117
+ type=timeout_type,
118
+ help="Timeout in seconds (useful when using slow AI). Use None for infinite timeout",
119
  )
 
120
  # RAG configuration
121
  parser.add_argument(
122
  "--max-async", type=int, default=4, help="Maximum async operations (default: 4)"
 
155
  default=None,
156
  )
157
 
158
+ # Optional https parameters
159
+ parser.add_argument(
160
+ "--ssl", action="store_true", help="Enable HTTPS (default: False)"
161
+ )
162
+ parser.add_argument(
163
+ "--ssl-certfile",
164
+ default=None,
165
+ help="Path to SSL certificate file (required if --ssl is enabled)",
166
+ )
167
+ parser.add_argument(
168
+ "--ssl-keyfile",
169
+ default=None,
170
+ help="Path to SSL private key file (required if --ssl is enabled)",
171
+ )
172
  return parser.parse_args()
173
 
174
 
175
  class DocumentManager:
176
  """Handles document operations and tracking"""
177
 
178
+ def __init__(
179
+ self,
180
+ input_dir: str,
181
+ supported_extensions: tuple = (".txt", ".md", ".pdf", ".docx", ".pptx"),
182
+ ):
183
  self.input_dir = Path(input_dir)
184
  self.supported_extensions = supported_extensions
185
  self.indexed_files = set()
 
261
 
262
 
263
  def create_app(args):
264
+ # Verify that bindings arer correctly setup
265
+ if args.llm_binding not in ["lollms", "ollama", "openai"]:
266
+ raise Exception("llm binding not supported")
267
+
268
+ if args.embedding_binding not in ["lollms", "ollama", "openai"]:
269
+ raise Exception("embedding binding not supported")
270
+
271
+ # Add SSL validation
272
+ if args.ssl:
273
+ if not args.ssl_certfile or not args.ssl_keyfile:
274
+ raise Exception(
275
+ "SSL certificate and key files must be provided when SSL is enabled"
276
+ )
277
+ if not os.path.exists(args.ssl_certfile):
278
+ raise Exception(f"SSL certificate file not found: {args.ssl_certfile}")
279
+ if not os.path.exists(args.ssl_keyfile):
280
+ raise Exception(f"SSL key file not found: {args.ssl_keyfile}")
281
+
282
  # Setup logging
283
  logging.basicConfig(
284
  format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
 
294
  + "(With authentication)"
295
  if api_key
296
  else "",
297
+ version="1.0.2",
298
  openapi_tags=[{"name": "api"}],
299
  )
300
 
 
319
  # Initialize RAG
320
  rag = LightRAG(
321
  working_dir=args.working_dir,
322
+ llm_model_func=lollms_model_complete
323
+ if args.llm_binding == "lollms"
324
+ else ollama_model_complete
325
+ if args.llm_binding == "ollama"
326
+ else azure_openai_complete_if_cache
327
+ if args.llm_binding == "azure_openai"
328
+ else openai_complete_if_cache,
329
+ llm_model_name=args.llm_model,
330
  llm_model_max_async=args.max_async,
331
  llm_model_max_token_size=args.max_tokens,
332
  llm_model_kwargs={
333
+ "host": args.llm_binding_host,
334
+ "timeout": args.timeout,
335
  "options": {"num_ctx": args.max_tokens},
336
  },
337
  embedding_func=EmbeddingFunc(
338
  embedding_dim=args.embedding_dim,
339
  max_token_size=args.max_embed_tokens,
340
  func=lambda texts: lollms_embed(
341
+ texts,
342
+ embed_model=args.embedding_model,
343
+ host=args.embedding_binding_host,
344
+ )
345
+ if args.llm_binding == "lollms"
346
+ else ollama_embed(
347
+ texts,
348
+ embed_model=args.embedding_model,
349
+ host=args.embedding_binding_host,
350
+ )
351
+ if args.llm_binding == "ollama"
352
+ else azure_openai_embedding(
353
+ texts,
354
+ model=args.embedding_model, # no host is used for openai
355
+ )
356
+ if args.llm_binding == "azure_openai"
357
+ else openai_embedding(
358
+ texts,
359
+ model=args.embedding_model, # no host is used for openai
360
  ),
361
  ),
362
  )
363
 
364
+ async def index_file(file_path: Union[str, Path]) -> None:
365
+ """Index all files inside the folder with support for multiple file formats
366
+
367
+ Args:
368
+ file_path: Path to the file to be indexed (str or Path object)
369
+
370
+ Raises:
371
+ ValueError: If file format is not supported
372
+ FileNotFoundError: If file doesn't exist
373
+ """
374
+ if not pm.is_installed("aiofiles"):
375
+ pm.install("aiofiles")
376
+
377
+ # Convert to Path object if string
378
+ file_path = Path(file_path)
379
+
380
+ # Check if file exists
381
+ if not file_path.exists():
382
+ raise FileNotFoundError(f"File not found: {file_path}")
383
+
384
+ content = ""
385
+ # Get file extension in lowercase
386
+ ext = file_path.suffix.lower()
387
+
388
+ match ext:
389
+ case ".txt" | ".md":
390
+ # Text files handling
391
+ async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
392
+ content = await f.read()
393
+
394
+ case ".pdf":
395
+ if not pm.is_installed("pypdf2"):
396
+ pm.install("pypdf2")
397
+ from pypdf2 import PdfReader
398
+
399
+ # PDF handling
400
+ reader = PdfReader(str(file_path))
401
+ content = ""
402
+ for page in reader.pages:
403
+ content += page.extract_text() + "\n"
404
+
405
+ case ".docx":
406
+ if not pm.is_installed("docx"):
407
+ pm.install("docx")
408
+ from docx import Document
409
+
410
+ # Word document handling
411
+ doc = Document(file_path)
412
+ content = "\n".join([paragraph.text for paragraph in doc.paragraphs])
413
+
414
+ case ".pptx":
415
+ if not pm.is_installed("pptx"):
416
+ pm.install("pptx")
417
+ from pptx import Presentation
418
+
419
+ # PowerPoint handling
420
+ prs = Presentation(file_path)
421
+ content = ""
422
+ for slide in prs.slides:
423
+ for shape in slide.shapes:
424
+ if hasattr(shape, "text"):
425
+ content += shape.text + "\n"
426
+
427
+ case _:
428
+ raise ValueError(f"Unsupported file format: {ext}")
429
+
430
+ # Insert content into RAG system
431
+ if content:
432
+ await rag.ainsert(content)
433
+ doc_manager.mark_as_indexed(file_path)
434
+ logging.info(f"Successfully indexed file: {file_path}")
435
+ else:
436
+ logging.warning(f"No content extracted from file: {file_path}")
437
+
438
  @app.on_event("startup")
439
  async def startup_event():
440
  """Index all files in input directory during startup"""
 
442
  new_files = doc_manager.scan_directory()
443
  for file_path in new_files:
444
  try:
445
+ await index_file(file_path)
 
 
 
 
 
 
446
  except Exception as e:
447
  trace_exception(e)
448
  logging.error(f"Error indexing file {file_path}: {str(e)}")
 
461
 
462
  for file_path in new_files:
463
  try:
464
+ await index_file(file_path)
465
+ indexed_count += 1
 
 
 
466
  except Exception as e:
467
  logging.error(f"Error indexing file {file_path}: {str(e)}")
468
 
 
489
  shutil.copyfileobj(file.file, buffer)
490
 
491
  # Immediately index the uploaded file
492
+ await index_file(file_path)
 
 
 
493
 
494
  return {
495
  "status": "success",
 
550
  )
551
  async def insert_text(request: InsertTextRequest):
552
  try:
553
+ await rag.ainsert(request.text)
554
  return InsertResponse(
555
  status="success",
556
  message="Text successfully inserted",
557
+ document_count=1,
558
  )
559
  except Exception as e:
560
  raise HTTPException(status_code=500, detail=str(e))
 
565
  dependencies=[Depends(optional_api_key)],
566
  )
567
  async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
568
+ """Insert a file directly into the RAG system
569
+
570
+ Args:
571
+ file: Uploaded file
572
+ description: Optional description of the file
573
+
574
+ Returns:
575
+ InsertResponse: Status of the insertion operation
576
+
577
+ Raises:
578
+ HTTPException: For unsupported file types or processing errors
579
+ """
580
  try:
581
+ content = ""
582
+ # Get file extension in lowercase
583
+ ext = Path(file.filename).suffix.lower()
584
+
585
+ match ext:
586
+ case ".txt" | ".md":
587
+ # Text files handling
588
+ text_content = await file.read()
589
+ content = text_content.decode("utf-8")
590
+
591
+ case ".pdf":
592
+ if not pm.is_installed("pypdf2"):
593
+ pm.install("pypdf2")
594
+ from pypdf2 import PdfReader
595
+ from io import BytesIO
596
+
597
+ # Read PDF from memory
598
+ pdf_content = await file.read()
599
+ pdf_file = BytesIO(pdf_content)
600
+ reader = PdfReader(pdf_file)
601
+ content = ""
602
+ for page in reader.pages:
603
+ content += page.extract_text() + "\n"
604
+
605
+ case ".docx":
606
+ if not pm.is_installed("docx"):
607
+ pm.install("docx")
608
+ from docx import Document
609
+ from io import BytesIO
610
+
611
+ # Read DOCX from memory
612
+ docx_content = await file.read()
613
+ docx_file = BytesIO(docx_content)
614
+ doc = Document(docx_file)
615
+ content = "\n".join(
616
+ [paragraph.text for paragraph in doc.paragraphs]
617
+ )
618
+
619
+ case ".pptx":
620
+ if not pm.is_installed("pptx"):
621
+ pm.install("pptx")
622
+ from pptx import Presentation
623
+ from io import BytesIO
624
+
625
+ # Read PPTX from memory
626
+ pptx_content = await file.read()
627
+ pptx_file = BytesIO(pptx_content)
628
+ prs = Presentation(pptx_file)
629
+ content = ""
630
+ for slide in prs.slides:
631
+ for shape in slide.shapes:
632
+ if hasattr(shape, "text"):
633
+ content += shape.text + "\n"
634
+
635
+ case _:
636
+ raise HTTPException(
637
+ status_code=400,
638
+ detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
639
+ )
640
+
641
+ # Insert content into RAG system
642
+ if content:
643
+ # Add description if provided
644
+ if description:
645
+ content = f"{description}\n\n{content}"
646
+
647
+ await rag.ainsert(content)
648
+ logging.info(f"Successfully indexed file: {file.filename}")
649
 
650
+ return InsertResponse(
651
+ status="success",
652
+ message=f"File '{file.filename}' successfully inserted",
653
+ document_count=1,
654
+ )
655
  else:
656
  raise HTTPException(
657
  status_code=400,
658
+ detail="No content could be extracted from the file",
659
  )
660
 
 
 
 
 
 
661
  except UnicodeDecodeError:
662
  raise HTTPException(status_code=400, detail="File encoding not supported")
663
  except Exception as e:
664
+ logging.error(f"Error processing file {file.filename}: {str(e)}")
665
  raise HTTPException(status_code=500, detail=str(e))
666
 
667
  @app.post(
 
670
  dependencies=[Depends(optional_api_key)],
671
  )
672
  async def insert_batch(files: List[UploadFile] = File(...)):
673
+ """Process multiple files in batch mode
674
+
675
+ Args:
676
+ files: List of files to process
677
+
678
+ Returns:
679
+ InsertResponse: Status of the batch insertion operation
680
+
681
+ Raises:
682
+ HTTPException: For processing errors
683
+ """
684
  try:
685
  inserted_count = 0
686
  failed_files = []
687
 
688
  for file in files:
689
  try:
690
+ content = ""
691
+ ext = Path(file.filename).suffix.lower()
692
+
693
+ match ext:
694
+ case ".txt" | ".md":
695
+ text_content = await file.read()
696
+ content = text_content.decode("utf-8")
697
+
698
+ case ".pdf":
699
+ if not pm.is_installed("pypdf2"):
700
+ pm.install("pypdf2")
701
+ from pypdf2 import PdfReader
702
+ from io import BytesIO
703
+
704
+ pdf_content = await file.read()
705
+ pdf_file = BytesIO(pdf_content)
706
+ reader = PdfReader(pdf_file)
707
+ for page in reader.pages:
708
+ content += page.extract_text() + "\n"
709
+
710
+ case ".docx":
711
+ if not pm.is_installed("docx"):
712
+ pm.install("docx")
713
+ from docx import Document
714
+ from io import BytesIO
715
+
716
+ docx_content = await file.read()
717
+ docx_file = BytesIO(docx_content)
718
+ doc = Document(docx_file)
719
+ content = "\n".join(
720
+ [paragraph.text for paragraph in doc.paragraphs]
721
+ )
722
+
723
+ case ".pptx":
724
+ if not pm.is_installed("pptx"):
725
+ pm.install("pptx")
726
+ from pptx import Presentation
727
+ from io import BytesIO
728
+
729
+ pptx_content = await file.read()
730
+ pptx_file = BytesIO(pptx_content)
731
+ prs = Presentation(pptx_file)
732
+ for slide in prs.slides:
733
+ for shape in slide.shapes:
734
+ if hasattr(shape, "text"):
735
+ content += shape.text + "\n"
736
+
737
+ case _:
738
+ failed_files.append(f"{file.filename} (unsupported type)")
739
+ continue
740
+
741
+ if content:
742
+ await rag.ainsert(content)
743
  inserted_count += 1
744
+ logging.info(f"Successfully indexed file: {file.filename}")
745
  else:
746
+ failed_files.append(f"{file.filename} (no content extracted)")
747
+
748
+ except UnicodeDecodeError:
749
+ failed_files.append(f"{file.filename} (encoding error)")
750
  except Exception as e:
751
  failed_files.append(f"{file.filename} ({str(e)})")
752
+ logging.error(f"Error processing file {file.filename}: {str(e)}")
753
+
754
+ # Prepare status message
755
+ if inserted_count == len(files):
756
+ status = "success"
757
+ status_message = f"Successfully inserted all {inserted_count} documents"
758
+ elif inserted_count > 0:
759
+ status = "partial_success"
760
+ status_message = f"Successfully inserted {inserted_count} out of {len(files)} documents"
761
+ if failed_files:
762
+ status_message += f". Failed files: {', '.join(failed_files)}"
763
+ else:
764
+ status = "failure"
765
+ status_message = "No documents were successfully inserted"
766
+ if failed_files:
767
+ status_message += f". Failed files: {', '.join(failed_files)}"
768
 
769
  return InsertResponse(
770
+ status=status,
771
  message=status_message,
772
+ document_count=inserted_count,
773
  )
774
+
775
  except Exception as e:
776
+ logging.error(f"Batch processing error: {str(e)}")
777
  raise HTTPException(status_code=500, detail=str(e))
778
 
779
  @app.delete(
 
803
  "input_directory": str(args.input_dir),
804
  "indexed_files": len(doc_manager.indexed_files),
805
  "configuration": {
806
+ # LLM configuration binding/host address (if applicable)/model (if applicable)
807
+ "llm_binding": args.llm_binding,
808
+ "llm_binding_host": args.llm_binding_host,
809
+ "llm_model": args.llm_model,
810
+ # embedding model configuration binding/host address (if applicable)/model (if applicable)
811
+ "embedding_binding": args.embedding_binding,
812
+ "embedding_binding_host": args.embedding_binding_host,
813
  "embedding_model": args.embedding_model,
814
  "max_tokens": args.max_tokens,
 
815
  },
816
  }
817
 
 
823
  import uvicorn
824
 
825
  app = create_app(args)
826
+ uvicorn_config = {
827
+ "app": app,
828
+ "host": args.host,
829
+ "port": args.port,
830
+ }
831
+ if args.ssl:
832
+ uvicorn_config.update(
833
+ {
834
+ "ssl_certfile": args.ssl_certfile,
835
+ "ssl_keyfile": args.ssl_keyfile,
836
+ }
837
+ )
838
+ uvicorn.run(**uvicorn_config)
839
 
840
 
841
  if __name__ == "__main__":
lightrag/api/ollama_lightrag_server.py DELETED
@@ -1,491 +0,0 @@
1
- from fastapi import FastAPI, HTTPException, File, UploadFile, Form
2
- from pydantic import BaseModel
3
- import logging
4
- import argparse
5
- from lightrag import LightRAG, QueryParam
6
- from lightrag.llm import ollama_model_complete, ollama_embed
7
- from lightrag.utils import EmbeddingFunc
8
- from typing import Optional, List
9
- from enum import Enum
10
- from pathlib import Path
11
- import shutil
12
- import aiofiles
13
- from ascii_colors import trace_exception
14
- import os
15
-
16
- from fastapi import Depends, Security
17
- from fastapi.security import APIKeyHeader
18
- from fastapi.middleware.cors import CORSMiddleware
19
-
20
- from starlette.status import HTTP_403_FORBIDDEN
21
-
22
-
23
- def parse_args():
24
- parser = argparse.ArgumentParser(
25
- description="LightRAG FastAPI Server with separate working and input directories"
26
- )
27
-
28
- # Server configuration
29
- parser.add_argument(
30
- "--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
31
- )
32
- parser.add_argument(
33
- "--port", type=int, default=9621, help="Server port (default: 9621)"
34
- )
35
-
36
- # Directory configuration
37
- parser.add_argument(
38
- "--working-dir",
39
- default="./rag_storage",
40
- help="Working directory for RAG storage (default: ./rag_storage)",
41
- )
42
- parser.add_argument(
43
- "--input-dir",
44
- default="./inputs",
45
- help="Directory containing input documents (default: ./inputs)",
46
- )
47
-
48
- # Model configuration
49
- parser.add_argument(
50
- "--model",
51
- default="mistral-nemo:latest",
52
- help="LLM model name (default: mistral-nemo:latest)",
53
- )
54
- parser.add_argument(
55
- "--embedding-model",
56
- default="bge-m3:latest",
57
- help="Embedding model name (default: bge-m3:latest)",
58
- )
59
- parser.add_argument(
60
- "--ollama-host",
61
- default="http://localhost:11434",
62
- help="Ollama host URL (default: http://localhost:11434)",
63
- )
64
-
65
- # RAG configuration
66
- parser.add_argument(
67
- "--max-async", type=int, default=4, help="Maximum async operations (default: 4)"
68
- )
69
- parser.add_argument(
70
- "--max-tokens",
71
- type=int,
72
- default=32768,
73
- help="Maximum token size (default: 32768)",
74
- )
75
- parser.add_argument(
76
- "--embedding-dim",
77
- type=int,
78
- default=1024,
79
- help="Embedding dimensions (default: 1024)",
80
- )
81
- parser.add_argument(
82
- "--max-embed-tokens",
83
- type=int,
84
- default=8192,
85
- help="Maximum embedding token size (default: 8192)",
86
- )
87
-
88
- # Logging configuration
89
- parser.add_argument(
90
- "--log-level",
91
- default="INFO",
92
- choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
93
- help="Logging level (default: INFO)",
94
- )
95
- parser.add_argument(
96
- "--key",
97
- type=str,
98
- help="API key for authentication. This protects lightrag server against unauthorized access",
99
- default=None,
100
- )
101
-
102
- return parser.parse_args()
103
-
104
-
105
- class DocumentManager:
106
- """Handles document operations and tracking"""
107
-
108
- def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
109
- self.input_dir = Path(input_dir)
110
- self.supported_extensions = supported_extensions
111
- self.indexed_files = set()
112
-
113
- # Create input directory if it doesn't exist
114
- self.input_dir.mkdir(parents=True, exist_ok=True)
115
-
116
- def scan_directory(self) -> List[Path]:
117
- """Scan input directory for new files"""
118
- new_files = []
119
- for ext in self.supported_extensions:
120
- for file_path in self.input_dir.rglob(f"*{ext}"):
121
- if file_path not in self.indexed_files:
122
- new_files.append(file_path)
123
- return new_files
124
-
125
- def mark_as_indexed(self, file_path: Path):
126
- """Mark a file as indexed"""
127
- self.indexed_files.add(file_path)
128
-
129
- def is_supported_file(self, filename: str) -> bool:
130
- """Check if file type is supported"""
131
- return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
132
-
133
-
134
- # Pydantic models
135
- class SearchMode(str, Enum):
136
- naive = "naive"
137
- local = "local"
138
- global_ = "global"
139
- hybrid = "hybrid"
140
-
141
-
142
- class QueryRequest(BaseModel):
143
- query: str
144
- mode: SearchMode = SearchMode.hybrid
145
- stream: bool = False
146
- only_need_context: bool = False
147
-
148
-
149
- class QueryResponse(BaseModel):
150
- response: str
151
-
152
-
153
- class InsertTextRequest(BaseModel):
154
- text: str
155
- description: Optional[str] = None
156
-
157
-
158
- class InsertResponse(BaseModel):
159
- status: str
160
- message: str
161
- document_count: int
162
-
163
-
164
- def get_api_key_dependency(api_key: Optional[str]):
165
- if not api_key:
166
- # If no API key is configured, return a dummy dependency that always succeeds
167
- async def no_auth():
168
- return None
169
-
170
- return no_auth
171
-
172
- # If API key is configured, use proper authentication
173
- api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
174
-
175
- async def api_key_auth(api_key_header_value: str | None = Security(api_key_header)):
176
- if not api_key_header_value:
177
- raise HTTPException(
178
- status_code=HTTP_403_FORBIDDEN, detail="API Key required"
179
- )
180
- if api_key_header_value != api_key:
181
- raise HTTPException(
182
- status_code=HTTP_403_FORBIDDEN, detail="Invalid API Key"
183
- )
184
- return api_key_header_value
185
-
186
- return api_key_auth
187
-
188
-
189
- def create_app(args):
190
- # Setup logging
191
- logging.basicConfig(
192
- format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
193
- )
194
-
195
- # Check if API key is provided either through env var or args
196
- api_key = os.getenv("LIGHTRAG_API_KEY") or args.key
197
-
198
- # Initialize FastAPI
199
- app = FastAPI(
200
- title="LightRAG API",
201
- description="API for querying text using LightRAG with separate storage and input directories"
202
- + "(With authentication)"
203
- if api_key
204
- else "",
205
- version="1.0.0",
206
- openapi_tags=[{"name": "api"}],
207
- )
208
-
209
- # Add CORS middleware
210
- app.add_middleware(
211
- CORSMiddleware,
212
- allow_origins=["*"],
213
- allow_credentials=True,
214
- allow_methods=["*"],
215
- allow_headers=["*"],
216
- )
217
-
218
- # Create the optional API key dependency
219
- optional_api_key = get_api_key_dependency(api_key)
220
-
221
- # Create working directory if it doesn't exist
222
- Path(args.working_dir).mkdir(parents=True, exist_ok=True)
223
-
224
- # Initialize document manager
225
- doc_manager = DocumentManager(args.input_dir)
226
-
227
- # Initialize RAG
228
- rag = LightRAG(
229
- working_dir=args.working_dir,
230
- llm_model_func=ollama_model_complete,
231
- llm_model_name=args.model,
232
- llm_model_max_async=args.max_async,
233
- llm_model_max_token_size=args.max_tokens,
234
- llm_model_kwargs={
235
- "host": args.ollama_host,
236
- "options": {"num_ctx": args.max_tokens},
237
- },
238
- embedding_func=EmbeddingFunc(
239
- embedding_dim=args.embedding_dim,
240
- max_token_size=args.max_embed_tokens,
241
- func=lambda texts: ollama_embed(
242
- texts, embed_model=args.embedding_model, host=args.ollama_host
243
- ),
244
- ),
245
- )
246
-
247
- @app.on_event("startup")
248
- async def startup_event():
249
- """Index all files in input directory during startup"""
250
- try:
251
- new_files = doc_manager.scan_directory()
252
- for file_path in new_files:
253
- try:
254
- # Use async file reading
255
- async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
256
- content = await f.read()
257
- # Use the async version of insert directly
258
- await rag.ainsert(content)
259
- doc_manager.mark_as_indexed(file_path)
260
- logging.info(f"Indexed file: {file_path}")
261
- except Exception as e:
262
- trace_exception(e)
263
- logging.error(f"Error indexing file {file_path}: {str(e)}")
264
-
265
- logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
266
-
267
- except Exception as e:
268
- logging.error(f"Error during startup indexing: {str(e)}")
269
-
270
- @app.post("/documents/scan", dependencies=[Depends(optional_api_key)])
271
- async def scan_for_new_documents():
272
- """Manually trigger scanning for new documents"""
273
- try:
274
- new_files = doc_manager.scan_directory()
275
- indexed_count = 0
276
-
277
- for file_path in new_files:
278
- try:
279
- with open(file_path, "r", encoding="utf-8") as f:
280
- content = f.read()
281
- await rag.ainsert(content)
282
- doc_manager.mark_as_indexed(file_path)
283
- indexed_count += 1
284
- except Exception as e:
285
- logging.error(f"Error indexing file {file_path}: {str(e)}")
286
-
287
- return {
288
- "status": "success",
289
- "indexed_count": indexed_count,
290
- "total_documents": len(doc_manager.indexed_files),
291
- }
292
- except Exception as e:
293
- raise HTTPException(status_code=500, detail=str(e))
294
-
295
- @app.post("/documents/upload", dependencies=[Depends(optional_api_key)])
296
- async def upload_to_input_dir(file: UploadFile = File(...)):
297
- """Upload a file to the input directory"""
298
- try:
299
- if not doc_manager.is_supported_file(file.filename):
300
- raise HTTPException(
301
- status_code=400,
302
- detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
303
- )
304
-
305
- file_path = doc_manager.input_dir / file.filename
306
- with open(file_path, "wb") as buffer:
307
- shutil.copyfileobj(file.file, buffer)
308
-
309
- # Immediately index the uploaded file
310
- with open(file_path, "r", encoding="utf-8") as f:
311
- content = f.read()
312
- await rag.ainsert(content)
313
- doc_manager.mark_as_indexed(file_path)
314
-
315
- return {
316
- "status": "success",
317
- "message": f"File uploaded and indexed: {file.filename}",
318
- "total_documents": len(doc_manager.indexed_files),
319
- }
320
- except Exception as e:
321
- raise HTTPException(status_code=500, detail=str(e))
322
-
323
- @app.post(
324
- "/query", response_model=QueryResponse, dependencies=[Depends(optional_api_key)]
325
- )
326
- async def query_text(request: QueryRequest):
327
- try:
328
- response = await rag.aquery(
329
- request.query,
330
- param=QueryParam(
331
- mode=request.mode,
332
- stream=request.stream,
333
- only_need_context=request.only_need_context,
334
- ),
335
- )
336
-
337
- if request.stream:
338
- result = ""
339
- async for chunk in response:
340
- result += chunk
341
- return QueryResponse(response=result)
342
- else:
343
- return QueryResponse(response=response)
344
- except Exception as e:
345
- raise HTTPException(status_code=500, detail=str(e))
346
-
347
- @app.post("/query/stream", dependencies=[Depends(optional_api_key)])
348
- async def query_text_stream(request: QueryRequest):
349
- try:
350
- response = rag.query(
351
- request.query,
352
- param=QueryParam(
353
- mode=request.mode,
354
- stream=True,
355
- only_need_context=request.only_need_context,
356
- ),
357
- )
358
-
359
- async def stream_generator():
360
- async for chunk in response:
361
- yield chunk
362
-
363
- return stream_generator()
364
- except Exception as e:
365
- raise HTTPException(status_code=500, detail=str(e))
366
-
367
- @app.post(
368
- "/documents/text",
369
- response_model=InsertResponse,
370
- dependencies=[Depends(optional_api_key)],
371
- )
372
- async def insert_text(request: InsertTextRequest):
373
- try:
374
- await rag.ainsert(request.text)
375
- return InsertResponse(
376
- status="success",
377
- message="Text successfully inserted",
378
- document_count=len(rag),
379
- )
380
- except Exception as e:
381
- raise HTTPException(status_code=500, detail=str(e))
382
-
383
- @app.post(
384
- "/documents/file",
385
- response_model=InsertResponse,
386
- dependencies=[Depends(optional_api_key)],
387
- )
388
- async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
389
- try:
390
- content = await file.read()
391
-
392
- if file.filename.endswith((".txt", ".md")):
393
- text = content.decode("utf-8")
394
- await rag.ainsert(text)
395
- else:
396
- raise HTTPException(
397
- status_code=400,
398
- detail="Unsupported file type. Only .txt and .md files are supported",
399
- )
400
-
401
- return InsertResponse(
402
- status="success",
403
- message=f"File '{file.filename}' successfully inserted",
404
- document_count=1,
405
- )
406
- except UnicodeDecodeError:
407
- raise HTTPException(status_code=400, detail="File encoding not supported")
408
- except Exception as e:
409
- raise HTTPException(status_code=500, detail=str(e))
410
-
411
- @app.post(
412
- "/documents/batch",
413
- response_model=InsertResponse,
414
- dependencies=[Depends(optional_api_key)],
415
- )
416
- async def insert_batch(files: List[UploadFile] = File(...)):
417
- try:
418
- inserted_count = 0
419
- failed_files = []
420
-
421
- for file in files:
422
- try:
423
- content = await file.read()
424
- if file.filename.endswith((".txt", ".md")):
425
- text = content.decode("utf-8")
426
- await rag.ainsert(text)
427
- inserted_count += 1
428
- else:
429
- failed_files.append(f"{file.filename} (unsupported type)")
430
- except Exception as e:
431
- failed_files.append(f"{file.filename} ({str(e)})")
432
-
433
- status_message = f"Successfully inserted {inserted_count} documents"
434
- if failed_files:
435
- status_message += f". Failed files: {', '.join(failed_files)}"
436
-
437
- return InsertResponse(
438
- status="success" if inserted_count > 0 else "partial_success",
439
- message=status_message,
440
- document_count=len(files),
441
- )
442
- except Exception as e:
443
- raise HTTPException(status_code=500, detail=str(e))
444
-
445
- @app.delete(
446
- "/documents",
447
- response_model=InsertResponse,
448
- dependencies=[Depends(optional_api_key)],
449
- )
450
- async def clear_documents():
451
- try:
452
- rag.text_chunks = []
453
- rag.entities_vdb = None
454
- rag.relationships_vdb = None
455
- return InsertResponse(
456
- status="success",
457
- message="All documents cleared successfully",
458
- document_count=0,
459
- )
460
- except Exception as e:
461
- raise HTTPException(status_code=500, detail=str(e))
462
-
463
- @app.get("/health", dependencies=[Depends(optional_api_key)])
464
- async def get_status():
465
- """Get current system status"""
466
- return {
467
- "status": "healthy",
468
- "working_directory": str(args.working_dir),
469
- "input_directory": str(args.input_dir),
470
- "indexed_files": len(doc_manager.indexed_files),
471
- "configuration": {
472
- "model": args.model,
473
- "embedding_model": args.embedding_model,
474
- "max_tokens": args.max_tokens,
475
- "ollama_host": args.ollama_host,
476
- },
477
- }
478
-
479
- return app
480
-
481
-
482
- def main():
483
- args = parse_args()
484
- import uvicorn
485
-
486
- app = create_app(args)
487
- uvicorn.run(app, host=args.host, port=args.port)
488
-
489
-
490
- if __name__ == "__main__":
491
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lightrag/api/openai_lightrag_server.py DELETED
@@ -1,506 +0,0 @@
1
- from fastapi import FastAPI, HTTPException, File, UploadFile, Form
2
- from pydantic import BaseModel
3
- import asyncio
4
- import logging
5
- import argparse
6
- from lightrag import LightRAG, QueryParam
7
- from lightrag.llm import openai_complete_if_cache, openai_embedding
8
- from lightrag.utils import EmbeddingFunc
9
- from typing import Optional, List
10
- from enum import Enum
11
- from pathlib import Path
12
- import shutil
13
- import aiofiles
14
- from ascii_colors import trace_exception
15
- import nest_asyncio
16
-
17
- import os
18
-
19
- from fastapi import Depends, Security
20
- from fastapi.security import APIKeyHeader
21
- from fastapi.middleware.cors import CORSMiddleware
22
-
23
- from starlette.status import HTTP_403_FORBIDDEN
24
-
25
- # Apply nest_asyncio to solve event loop issues
26
- nest_asyncio.apply()
27
-
28
-
29
- def parse_args():
30
- parser = argparse.ArgumentParser(
31
- description="LightRAG FastAPI Server with OpenAI integration"
32
- )
33
-
34
- # Server configuration
35
- parser.add_argument(
36
- "--host", default="0.0.0.0", help="Server host (default: 0.0.0.0)"
37
- )
38
- parser.add_argument(
39
- "--port", type=int, default=9621, help="Server port (default: 9621)"
40
- )
41
-
42
- # Directory configuration
43
- parser.add_argument(
44
- "--working-dir",
45
- default="./rag_storage",
46
- help="Working directory for RAG storage (default: ./rag_storage)",
47
- )
48
- parser.add_argument(
49
- "--input-dir",
50
- default="./inputs",
51
- help="Directory containing input documents (default: ./inputs)",
52
- )
53
-
54
- # Model configuration
55
- parser.add_argument(
56
- "--model", default="gpt-4", help="OpenAI model name (default: gpt-4)"
57
- )
58
- parser.add_argument(
59
- "--embedding-model",
60
- default="text-embedding-3-large",
61
- help="OpenAI embedding model (default: text-embedding-3-large)",
62
- )
63
-
64
- # RAG configuration
65
- parser.add_argument(
66
- "--max-tokens",
67
- type=int,
68
- default=32768,
69
- help="Maximum token size (default: 32768)",
70
- )
71
- parser.add_argument(
72
- "--max-embed-tokens",
73
- type=int,
74
- default=8192,
75
- help="Maximum embedding token size (default: 8192)",
76
- )
77
-
78
- # Logging configuration
79
- parser.add_argument(
80
- "--log-level",
81
- default="INFO",
82
- choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
83
- help="Logging level (default: INFO)",
84
- )
85
-
86
- parser.add_argument(
87
- "--key",
88
- type=str,
89
- help="API key for authentication. This protects lightrag server against unauthorized access",
90
- default=None,
91
- )
92
-
93
- return parser.parse_args()
94
-
95
-
96
- class DocumentManager:
97
- """Handles document operations and tracking"""
98
-
99
- def __init__(self, input_dir: str, supported_extensions: tuple = (".txt", ".md")):
100
- self.input_dir = Path(input_dir)
101
- self.supported_extensions = supported_extensions
102
- self.indexed_files = set()
103
-
104
- # Create input directory if it doesn't exist
105
- self.input_dir.mkdir(parents=True, exist_ok=True)
106
-
107
- def scan_directory(self) -> List[Path]:
108
- """Scan input directory for new files"""
109
- new_files = []
110
- for ext in self.supported_extensions:
111
- for file_path in self.input_dir.rglob(f"*{ext}"):
112
- if file_path not in self.indexed_files:
113
- new_files.append(file_path)
114
- return new_files
115
-
116
- def mark_as_indexed(self, file_path: Path):
117
- """Mark a file as indexed"""
118
- self.indexed_files.add(file_path)
119
-
120
- def is_supported_file(self, filename: str) -> bool:
121
- """Check if file type is supported"""
122
- return any(filename.lower().endswith(ext) for ext in self.supported_extensions)
123
-
124
-
125
- # Pydantic models
126
- class SearchMode(str, Enum):
127
- naive = "naive"
128
- local = "local"
129
- global_ = "global"
130
- hybrid = "hybrid"
131
-
132
-
133
- class QueryRequest(BaseModel):
134
- query: str
135
- mode: SearchMode = SearchMode.hybrid
136
- stream: bool = False
137
- only_need_context: bool = False
138
-
139
-
140
- class QueryResponse(BaseModel):
141
- response: str
142
-
143
-
144
- class InsertTextRequest(BaseModel):
145
- text: str
146
- description: Optional[str] = None
147
-
148
-
149
- class InsertResponse(BaseModel):
150
- status: str
151
- message: str
152
- document_count: int
153
-
154
-
155
- def get_api_key_dependency(api_key: Optional[str]):
156
- if not api_key:
157
- # If no API key is configured, return a dummy dependency that always succeeds
158
- async def no_auth():
159
- return None
160
-
161
- return no_auth
162
-
163
- # If API key is configured, use proper authentication
164
- api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
165
-
166
- async def api_key_auth(api_key_header_value: str | None = Security(api_key_header)):
167
- if not api_key_header_value:
168
- raise HTTPException(
169
- status_code=HTTP_403_FORBIDDEN, detail="API Key required"
170
- )
171
- if api_key_header_value != api_key:
172
- raise HTTPException(
173
- status_code=HTTP_403_FORBIDDEN, detail="Invalid API Key"
174
- )
175
- return api_key_header_value
176
-
177
- return api_key_auth
178
-
179
-
180
- async def get_embedding_dim(embedding_model: str) -> int:
181
- """Get embedding dimensions for the specified model"""
182
- test_text = ["This is a test sentence."]
183
- embedding = await openai_embedding(test_text, model=embedding_model)
184
- return embedding.shape[1]
185
-
186
-
187
- def create_app(args):
188
- # Setup logging
189
- logging.basicConfig(
190
- format="%(levelname)s:%(message)s", level=getattr(logging, args.log_level)
191
- )
192
-
193
- # Check if API key is provided either through env var or args
194
- api_key = os.getenv("LIGHTRAG_API_KEY") or args.key
195
-
196
- # Initialize FastAPI
197
- app = FastAPI(
198
- title="LightRAG API",
199
- description="API for querying text using LightRAG with separate storage and input directories"
200
- + "(With authentication)"
201
- if api_key
202
- else "",
203
- version="1.0.0",
204
- openapi_tags=[{"name": "api"}],
205
- )
206
-
207
- # Add CORS middleware
208
- app.add_middleware(
209
- CORSMiddleware,
210
- allow_origins=["*"],
211
- allow_credentials=True,
212
- allow_methods=["*"],
213
- allow_headers=["*"],
214
- )
215
-
216
- # Create the optional API key dependency
217
- optional_api_key = get_api_key_dependency(api_key)
218
-
219
- # Add CORS middleware
220
- app.add_middleware(
221
- CORSMiddleware,
222
- allow_origins=["*"],
223
- allow_credentials=True,
224
- allow_methods=["*"],
225
- allow_headers=["*"],
226
- )
227
-
228
- # Create working directory if it doesn't exist
229
- Path(args.working_dir).mkdir(parents=True, exist_ok=True)
230
-
231
- # Initialize document manager
232
- doc_manager = DocumentManager(args.input_dir)
233
-
234
- # Get embedding dimensions
235
- embedding_dim = asyncio.run(get_embedding_dim(args.embedding_model))
236
-
237
- async def async_openai_complete(
238
- prompt, system_prompt=None, history_messages=[], **kwargs
239
- ):
240
- """Async wrapper for OpenAI completion"""
241
- return await openai_complete_if_cache(
242
- args.model,
243
- prompt,
244
- system_prompt=system_prompt,
245
- history_messages=history_messages,
246
- **kwargs,
247
- )
248
-
249
- # Initialize RAG with OpenAI configuration
250
- rag = LightRAG(
251
- working_dir=args.working_dir,
252
- llm_model_func=async_openai_complete,
253
- llm_model_name=args.model,
254
- llm_model_max_token_size=args.max_tokens,
255
- embedding_func=EmbeddingFunc(
256
- embedding_dim=embedding_dim,
257
- max_token_size=args.max_embed_tokens,
258
- func=lambda texts: openai_embedding(texts, model=args.embedding_model),
259
- ),
260
- )
261
-
262
- @app.on_event("startup")
263
- async def startup_event():
264
- """Index all files in input directory during startup"""
265
- try:
266
- new_files = doc_manager.scan_directory()
267
- for file_path in new_files:
268
- try:
269
- # Use async file reading
270
- async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
271
- content = await f.read()
272
- # Use the async version of insert directly
273
- await rag.ainsert(content)
274
- doc_manager.mark_as_indexed(file_path)
275
- logging.info(f"Indexed file: {file_path}")
276
- except Exception as e:
277
- trace_exception(e)
278
- logging.error(f"Error indexing file {file_path}: {str(e)}")
279
-
280
- logging.info(f"Indexed {len(new_files)} documents from {args.input_dir}")
281
-
282
- except Exception as e:
283
- logging.error(f"Error during startup indexing: {str(e)}")
284
-
285
- @app.post("/documents/scan", dependencies=[Depends(optional_api_key)])
286
- async def scan_for_new_documents():
287
- """Manually trigger scanning for new documents"""
288
- try:
289
- new_files = doc_manager.scan_directory()
290
- indexed_count = 0
291
-
292
- for file_path in new_files:
293
- try:
294
- with open(file_path, "r", encoding="utf-8") as f:
295
- content = f.read()
296
- rag.insert(content)
297
- doc_manager.mark_as_indexed(file_path)
298
- indexed_count += 1
299
- except Exception as e:
300
- logging.error(f"Error indexing file {file_path}: {str(e)}")
301
-
302
- return {
303
- "status": "success",
304
- "indexed_count": indexed_count,
305
- "total_documents": len(doc_manager.indexed_files),
306
- }
307
- except Exception as e:
308
- raise HTTPException(status_code=500, detail=str(e))
309
-
310
- @app.post("/documents/upload", dependencies=[Depends(optional_api_key)])
311
- async def upload_to_input_dir(file: UploadFile = File(...)):
312
- """Upload a file to the input directory"""
313
- try:
314
- if not doc_manager.is_supported_file(file.filename):
315
- raise HTTPException(
316
- status_code=400,
317
- detail=f"Unsupported file type. Supported types: {doc_manager.supported_extensions}",
318
- )
319
-
320
- file_path = doc_manager.input_dir / file.filename
321
- with open(file_path, "wb") as buffer:
322
- shutil.copyfileobj(file.file, buffer)
323
-
324
- # Immediately index the uploaded file
325
- with open(file_path, "r", encoding="utf-8") as f:
326
- content = f.read()
327
- rag.insert(content)
328
- doc_manager.mark_as_indexed(file_path)
329
-
330
- return {
331
- "status": "success",
332
- "message": f"File uploaded and indexed: {file.filename}",
333
- "total_documents": len(doc_manager.indexed_files),
334
- }
335
- except Exception as e:
336
- raise HTTPException(status_code=500, detail=str(e))
337
-
338
- @app.post(
339
- "/query", response_model=QueryResponse, dependencies=[Depends(optional_api_key)]
340
- )
341
- async def query_text(request: QueryRequest):
342
- try:
343
- response = await rag.aquery(
344
- request.query,
345
- param=QueryParam(
346
- mode=request.mode,
347
- stream=request.stream,
348
- only_need_context=request.only_need_context,
349
- ),
350
- )
351
-
352
- if request.stream:
353
- result = ""
354
- async for chunk in response:
355
- result += chunk
356
- return QueryResponse(response=result)
357
- else:
358
- return QueryResponse(response=response)
359
- except Exception as e:
360
- raise HTTPException(status_code=500, detail=str(e))
361
-
362
- @app.post("/query/stream", dependencies=[Depends(optional_api_key)])
363
- async def query_text_stream(request: QueryRequest):
364
- try:
365
- response = rag.query(
366
- request.query,
367
- param=QueryParam(
368
- mode=request.mode,
369
- stream=True,
370
- only_need_context=request.only_need_context,
371
- ),
372
- )
373
-
374
- async def stream_generator():
375
- async for chunk in response:
376
- yield chunk
377
-
378
- return stream_generator()
379
- except Exception as e:
380
- raise HTTPException(status_code=500, detail=str(e))
381
-
382
- @app.post(
383
- "/documents/text",
384
- response_model=InsertResponse,
385
- dependencies=[Depends(optional_api_key)],
386
- )
387
- async def insert_text(request: InsertTextRequest):
388
- try:
389
- rag.insert(request.text)
390
- return InsertResponse(
391
- status="success",
392
- message="Text successfully inserted",
393
- document_count=len(rag),
394
- )
395
- except Exception as e:
396
- raise HTTPException(status_code=500, detail=str(e))
397
-
398
- @app.post(
399
- "/documents/file",
400
- response_model=InsertResponse,
401
- dependencies=[Depends(optional_api_key)],
402
- )
403
- async def insert_file(file: UploadFile = File(...), description: str = Form(None)):
404
- try:
405
- content = await file.read()
406
-
407
- if file.filename.endswith((".txt", ".md")):
408
- text = content.decode("utf-8")
409
- rag.insert(text)
410
- else:
411
- raise HTTPException(
412
- status_code=400,
413
- detail="Unsupported file type. Only .txt and .md files are supported",
414
- )
415
-
416
- return InsertResponse(
417
- status="success",
418
- message=f"File '{file.filename}' successfully inserted",
419
- document_count=1,
420
- )
421
- except UnicodeDecodeError:
422
- raise HTTPException(status_code=400, detail="File encoding not supported")
423
- except Exception as e:
424
- raise HTTPException(status_code=500, detail=str(e))
425
-
426
- @app.post(
427
- "/documents/batch",
428
- response_model=InsertResponse,
429
- dependencies=[Depends(optional_api_key)],
430
- )
431
- async def insert_batch(files: List[UploadFile] = File(...)):
432
- try:
433
- inserted_count = 0
434
- failed_files = []
435
-
436
- for file in files:
437
- try:
438
- content = await file.read()
439
- if file.filename.endswith((".txt", ".md")):
440
- text = content.decode("utf-8")
441
- rag.insert(text)
442
- inserted_count += 1
443
- else:
444
- failed_files.append(f"{file.filename} (unsupported type)")
445
- except Exception as e:
446
- failed_files.append(f"{file.filename} ({str(e)})")
447
-
448
- status_message = f"Successfully inserted {inserted_count} documents"
449
- if failed_files:
450
- status_message += f". Failed files: {', '.join(failed_files)}"
451
-
452
- return InsertResponse(
453
- status="success" if inserted_count > 0 else "partial_success",
454
- message=status_message,
455
- document_count=len(files),
456
- )
457
- except Exception as e:
458
- raise HTTPException(status_code=500, detail=str(e))
459
-
460
- @app.delete(
461
- "/documents",
462
- response_model=InsertResponse,
463
- dependencies=[Depends(optional_api_key)],
464
- )
465
- async def clear_documents():
466
- try:
467
- rag.text_chunks = []
468
- rag.entities_vdb = None
469
- rag.relationships_vdb = None
470
- return InsertResponse(
471
- status="success",
472
- message="All documents cleared successfully",
473
- document_count=0,
474
- )
475
- except Exception as e:
476
- raise HTTPException(status_code=500, detail=str(e))
477
-
478
- @app.get("/health", dependencies=[Depends(optional_api_key)])
479
- async def get_status():
480
- """Get current system status"""
481
- return {
482
- "status": "healthy",
483
- "working_directory": str(args.working_dir),
484
- "input_directory": str(args.input_dir),
485
- "indexed_files": len(doc_manager.indexed_files),
486
- "configuration": {
487
- "model": args.model,
488
- "embedding_model": args.embedding_model,
489
- "max_tokens": args.max_tokens,
490
- "embedding_dim": embedding_dim,
491
- },
492
- }
493
-
494
- return app
495
-
496
-
497
- def main():
498
- args = parse_args()
499
- import uvicorn
500
-
501
- app = create_app(args)
502
- uvicorn.run(app, host=args.host, port=args.port)
503
-
504
-
505
- if __name__ == "__main__":
506
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lightrag/api/requirements.txt CHANGED
@@ -7,6 +7,7 @@ nest_asyncio
7
  numpy
8
  ollama
9
  openai
 
10
  python-dotenv
11
  python-multipart
12
  tenacity
 
7
  numpy
8
  ollama
9
  openai
10
+ pipmaster
11
  python-dotenv
12
  python-multipart
13
  tenacity
lightrag/kg/mongo_impl.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  from tqdm.asyncio import tqdm as tqdm_async
3
  from dataclasses import dataclass
4
  from pymongo import MongoClient
5
-
6
  from lightrag.utils import logger
7
 
8
  from lightrag.base import BaseKVStorage
@@ -41,11 +41,35 @@ class MongoKVStorage(BaseKVStorage):
41
  return set([s for s in data if s not in existing_ids])
42
 
43
  async def upsert(self, data: dict[str, dict]):
44
- for k, v in tqdm_async(data.items(), desc="Upserting"):
45
- self._data.update_one({"_id": k}, {"$set": v}, upsert=True)
46
- data[k]["_id"] = k
 
 
 
 
 
 
 
 
 
 
 
47
  return data
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  async def drop(self):
50
  """ """
51
  pass
 
2
  from tqdm.asyncio import tqdm as tqdm_async
3
  from dataclasses import dataclass
4
  from pymongo import MongoClient
5
+ from typing import Union
6
  from lightrag.utils import logger
7
 
8
  from lightrag.base import BaseKVStorage
 
41
  return set([s for s in data if s not in existing_ids])
42
 
43
  async def upsert(self, data: dict[str, dict]):
44
+ if self.namespace == "llm_response_cache":
45
+ for mode, items in data.items():
46
+ for k, v in tqdm_async(items.items(), desc="Upserting"):
47
+ key = f"{mode}_{k}"
48
+ result = self._data.update_one(
49
+ {"_id": key}, {"$setOnInsert": v}, upsert=True
50
+ )
51
+ if result.upserted_id:
52
+ logger.debug(f"\nInserted new document with key: {key}")
53
+ data[mode][k]["_id"] = key
54
+ else:
55
+ for k, v in tqdm_async(data.items(), desc="Upserting"):
56
+ self._data.update_one({"_id": k}, {"$set": v}, upsert=True)
57
+ data[k]["_id"] = k
58
  return data
59
 
60
+ async def get_by_mode_and_id(self, mode: str, id: str) -> Union[dict, None]:
61
+ if "llm_response_cache" == self.namespace:
62
+ res = {}
63
+ v = self._data.find_one({"_id": mode + "_" + id})
64
+ if v:
65
+ res[id] = v
66
+ logger.debug(f"llm_response_cache find one by:{id}")
67
+ return res
68
+ else:
69
+ return None
70
+ else:
71
+ return None
72
+
73
  async def drop(self):
74
  """ """
75
  pass
lightrag/kg/neo4j_impl.py CHANGED
@@ -39,6 +39,7 @@ class Neo4JStorage(BaseGraphStorage):
39
  URI = os.environ["NEO4J_URI"]
40
  USERNAME = os.environ["NEO4J_USERNAME"]
41
  PASSWORD = os.environ["NEO4J_PASSWORD"]
 
42
  DATABASE = os.environ.get(
43
  "NEO4J_DATABASE"
44
  ) # If this param is None, the home database will be used. If it is not None, the specified database will be used.
@@ -47,7 +48,11 @@ class Neo4JStorage(BaseGraphStorage):
47
  URI, auth=(USERNAME, PASSWORD)
48
  )
49
  _database_name = "home database" if DATABASE is None else f"database {DATABASE}"
50
- with GraphDatabase.driver(URI, auth=(USERNAME, PASSWORD)) as _sync_driver:
 
 
 
 
51
  try:
52
  with _sync_driver.session(database=DATABASE) as session:
53
  try:
 
39
  URI = os.environ["NEO4J_URI"]
40
  USERNAME = os.environ["NEO4J_USERNAME"]
41
  PASSWORD = os.environ["NEO4J_PASSWORD"]
42
+ MAX_CONNECTION_POOL_SIZE = os.environ.get("NEO4J_MAX_CONNECTION_POOL_SIZE", 800)
43
  DATABASE = os.environ.get(
44
  "NEO4J_DATABASE"
45
  ) # If this param is None, the home database will be used. If it is not None, the specified database will be used.
 
48
  URI, auth=(USERNAME, PASSWORD)
49
  )
50
  _database_name = "home database" if DATABASE is None else f"database {DATABASE}"
51
+ with GraphDatabase.driver(
52
+ URI,
53
+ auth=(USERNAME, PASSWORD),
54
+ max_connection_pool_size=MAX_CONNECTION_POOL_SIZE,
55
+ ) as _sync_driver:
56
  try:
57
  with _sync_driver.session(database=DATABASE) as session:
58
  try:
lightrag/kg/postgres_impl.py CHANGED
@@ -130,6 +130,7 @@ class PostgreSQLDB:
130
  data: Union[list, dict] = None,
131
  for_age: bool = False,
132
  graph_name: str = None,
 
133
  ):
134
  try:
135
  async with self.pool.acquire() as connection:
@@ -140,8 +141,16 @@ class PostgreSQLDB:
140
  await connection.execute(sql)
141
  else:
142
  await connection.execute(sql, *data.values())
 
 
 
 
 
 
 
 
143
  except Exception as e:
144
- logger.error(f"PostgreSQL database error: {e}")
145
  print(sql)
146
  print(data)
147
  raise
@@ -568,10 +577,10 @@ class PGGraphStorage(BaseGraphStorage):
568
 
569
  if dtype == "vertex":
570
  vertex = json.loads(v)
571
- field = json.loads(v).get("properties")
572
  if not field:
573
  field = {}
574
- field["label"] = PGGraphStorage._decode_graph_label(vertex["label"])
575
  d[k] = field
576
  # convert edge from id-label->id by replacing id with node information
577
  # we only do this if the vertex was also returned in the query
@@ -666,73 +675,8 @@ class PGGraphStorage(BaseGraphStorage):
666
  # otherwise return the value stripping out some common special chars
667
  return field.replace("(", "_").replace(")", "")
668
 
669
- @staticmethod
670
- def _wrap_query(query: str, graph_name: str, **params: str) -> str:
671
- """
672
- Convert a cypher query to an Apache Age compatible
673
- sql query by wrapping the cypher query in ag_catalog.cypher,
674
- casting results to agtype and building a select statement
675
-
676
- Args:
677
- query (str): a valid cypher query
678
- graph_name (str): the name of the graph to query
679
- params (dict): parameters for the query
680
-
681
- Returns:
682
- str: an equivalent pgsql query
683
- """
684
-
685
- # pgsql template
686
- template = """SELECT {projection} FROM ag_catalog.cypher('{graph_name}', $$
687
- {query}
688
- $$) AS ({fields})"""
689
-
690
- # if there are any returned fields they must be added to the pgsql query
691
- if "return" in query.lower():
692
- # parse return statement to identify returned fields
693
- fields = (
694
- query.lower()
695
- .split("return")[-1]
696
- .split("distinct")[-1]
697
- .split("order by")[0]
698
- .split("skip")[0]
699
- .split("limit")[0]
700
- .split(",")
701
- )
702
-
703
- # raise exception if RETURN * is found as we can't resolve the fields
704
- if "*" in [x.strip() for x in fields]:
705
- raise ValueError(
706
- "AGE graph does not support 'RETURN *'"
707
- + " statements in Cypher queries"
708
- )
709
-
710
- # get pgsql formatted field names
711
- fields = [
712
- PGGraphStorage._get_col_name(field, idx)
713
- for idx, field in enumerate(fields)
714
- ]
715
-
716
- # build resulting pgsql relation
717
- fields_str = ", ".join(
718
- [field.split(".")[-1] + " agtype" for field in fields]
719
- )
720
-
721
- # if no return statement we still need to return a single field of type agtype
722
- else:
723
- fields_str = "a agtype"
724
-
725
- select_str = "*"
726
-
727
- return template.format(
728
- graph_name=graph_name,
729
- query=query.format(**params),
730
- fields=fields_str,
731
- projection=select_str,
732
- )
733
-
734
  async def _query(
735
- self, query: str, readonly=True, upsert_edge=False, **params: str
736
  ) -> List[Dict[str, Any]]:
737
  """
738
  Query the graph by taking a cypher query, converting it to an
@@ -746,7 +690,7 @@ class PGGraphStorage(BaseGraphStorage):
746
  List[Dict[str, Any]]: a list of dictionaries containing the result set
747
  """
748
  # convert cypher query to pgsql/age query
749
- wrapped_query = self._wrap_query(query, self.graph_name, **params)
750
 
751
  # execute the query, rolling back on an error
752
  try:
@@ -758,22 +702,16 @@ class PGGraphStorage(BaseGraphStorage):
758
  graph_name=self.graph_name,
759
  )
760
  else:
761
- # for upserting edge, need to run the SQL twice, otherwise cannot update the properties. (First time it will try to create the edge, second time is MERGING)
762
- # It is a bug of AGE as of 2025-01-03, hope it can be resolved in the future.
763
- if upsert_edge:
764
- data = await self.db.execute(
765
- f"{wrapped_query};{wrapped_query};",
766
- for_age=True,
767
- graph_name=self.graph_name,
768
- )
769
- else:
770
- data = await self.db.execute(
771
- wrapped_query, for_age=True, graph_name=self.graph_name
772
- )
773
  except Exception as e:
774
  raise PGGraphQueryException(
775
  {
776
- "message": f"Error executing graph query: {query.format(**params)}",
777
  "wrapped": wrapped_query,
778
  "detail": str(e),
779
  }
@@ -788,77 +726,85 @@ class PGGraphStorage(BaseGraphStorage):
788
  return result
789
 
790
  async def has_node(self, node_id: str) -> bool:
791
- entity_name_label = node_id.strip('"')
 
 
 
 
 
792
 
793
- query = """MATCH (n:`{label}`) RETURN count(n) > 0 AS node_exists"""
794
- params = {"label": PGGraphStorage._encode_graph_label(entity_name_label)}
795
- single_result = (await self._query(query, **params))[0]
796
  logger.debug(
797
  "{%s}:query:{%s}:result:{%s}",
798
  inspect.currentframe().f_code.co_name,
799
- query.format(**params),
800
  single_result["node_exists"],
801
  )
802
 
803
  return single_result["node_exists"]
804
 
805
  async def has_edge(self, source_node_id: str, target_node_id: str) -> bool:
806
- entity_name_label_source = source_node_id.strip('"')
807
- entity_name_label_target = target_node_id.strip('"')
 
 
 
 
 
 
 
 
 
808
 
809
- query = """MATCH (a:`{src_label}`)-[r]-(b:`{tgt_label}`)
810
- RETURN COUNT(r) > 0 AS edge_exists"""
811
- params = {
812
- "src_label": PGGraphStorage._encode_graph_label(entity_name_label_source),
813
- "tgt_label": PGGraphStorage._encode_graph_label(entity_name_label_target),
814
- }
815
- single_result = (await self._query(query, **params))[0]
816
  logger.debug(
817
  "{%s}:query:{%s}:result:{%s}",
818
  inspect.currentframe().f_code.co_name,
819
- query.format(**params),
820
  single_result["edge_exists"],
821
  )
822
  return single_result["edge_exists"]
823
 
824
  async def get_node(self, node_id: str) -> Union[dict, None]:
825
- entity_name_label = node_id.strip('"')
826
- query = """MATCH (n:`{label}`) RETURN n"""
827
- params = {"label": PGGraphStorage._encode_graph_label(entity_name_label)}
828
- record = await self._query(query, **params)
 
 
829
  if record:
830
  node = record[0]
831
  node_dict = node["n"]
832
  logger.debug(
833
  "{%s}: query: {%s}, result: {%s}",
834
  inspect.currentframe().f_code.co_name,
835
- query.format(**params),
836
  node_dict,
837
  )
838
  return node_dict
839
  return None
840
 
841
  async def node_degree(self, node_id: str) -> int:
842
- entity_name_label = node_id.strip('"')
843
 
844
- query = """MATCH (n:`{label}`)-[]->(x) RETURN count(x) AS total_edge_count"""
845
- params = {"label": PGGraphStorage._encode_graph_label(entity_name_label)}
846
- record = (await self._query(query, **params))[0]
 
 
847
  if record:
848
  edge_count = int(record["total_edge_count"])
849
  logger.debug(
850
  "{%s}:query:{%s}:result:{%s}",
851
  inspect.currentframe().f_code.co_name,
852
- query.format(**params),
853
  edge_count,
854
  )
855
  return edge_count
856
 
857
  async def edge_degree(self, src_id: str, tgt_id: str) -> int:
858
- entity_name_label_source = src_id.strip('"')
859
- entity_name_label_target = tgt_id.strip('"')
860
- src_degree = await self.node_degree(entity_name_label_source)
861
- trg_degree = await self.node_degree(entity_name_label_target)
862
 
863
  # Convert None to 0 for addition
864
  src_degree = 0 if src_degree is None else src_degree
@@ -885,23 +831,25 @@ class PGGraphStorage(BaseGraphStorage):
885
  Returns:
886
  list: List of all relationships/edges found
887
  """
888
- entity_name_label_source = source_node_id.strip('"')
889
- entity_name_label_target = target_node_id.strip('"')
890
-
891
- query = """MATCH (a:`{src_label}`)-[r]->(b:`{tgt_label}`)
892
- RETURN properties(r) as edge_properties
893
- LIMIT 1"""
894
- params = {
895
- "src_label": PGGraphStorage._encode_graph_label(entity_name_label_source),
896
- "tgt_label": PGGraphStorage._encode_graph_label(entity_name_label_target),
897
- }
898
- record = await self._query(query, **params)
 
 
899
  if record and record[0] and record[0]["edge_properties"]:
900
  result = record[0]["edge_properties"]
901
  logger.debug(
902
  "{%s}:query:{%s}:result:{%s}",
903
  inspect.currentframe().f_code.co_name,
904
- query.format(**params),
905
  result,
906
  )
907
  return result
@@ -911,29 +859,41 @@ class PGGraphStorage(BaseGraphStorage):
911
  Retrieves all edges (relationships) for a particular node identified by its label.
912
  :return: List of dictionaries containing edge information
913
  """
914
- node_label = source_node_id.strip('"')
 
 
 
 
 
 
 
 
 
915
 
916
- query = """MATCH (n:`{label}`)
917
- OPTIONAL MATCH (n)-[r]-(connected)
918
- RETURN n, r, connected"""
919
- params = {"label": PGGraphStorage._encode_graph_label(node_label)}
920
- results = await self._query(query, **params)
921
  edges = []
922
  for record in results:
923
  source_node = record["n"] if record["n"] else None
924
  connected_node = record["connected"] if record["connected"] else None
925
 
926
  source_label = (
927
- source_node["label"] if source_node and source_node["label"] else None
 
 
928
  )
929
  target_label = (
930
- connected_node["label"]
931
- if connected_node and connected_node["label"]
932
  else None
933
  )
934
 
935
  if source_label and target_label:
936
- edges.append((source_label, target_label))
 
 
 
 
 
937
 
938
  return edges
939
 
@@ -950,17 +910,21 @@ class PGGraphStorage(BaseGraphStorage):
950
  node_id: The unique identifier for the node (used as label)
951
  node_data: Dictionary of node properties
952
  """
953
- label = node_id.strip('"')
954
  properties = node_data
955
 
956
- query = """MERGE (n:`{label}`)
957
- SET n += {properties}"""
958
- params = {
959
- "label": PGGraphStorage._encode_graph_label(label),
960
- "properties": PGGraphStorage._format_properties(properties),
961
- }
 
 
 
 
962
  try:
963
- await self._query(query, readonly=False, **params)
964
  logger.debug(
965
  "Upserted node with label '{%s}' and properties: {%s}",
966
  label,
@@ -986,28 +950,30 @@ class PGGraphStorage(BaseGraphStorage):
986
  target_node_id (str): Label of the target node (used as identifier)
987
  edge_data (dict): Dictionary of properties to set on the edge
988
  """
989
- source_node_label = source_node_id.strip('"')
990
- target_node_label = target_node_id.strip('"')
991
  edge_properties = edge_data
992
 
993
- query = """MATCH (source:`{src_label}`)
994
- WITH source
995
- MATCH (target:`{tgt_label}`)
996
- MERGE (source)-[r:DIRECTED]->(target)
997
- SET r += {properties}
998
- RETURN r"""
999
- params = {
1000
- "src_label": PGGraphStorage._encode_graph_label(source_node_label),
1001
- "tgt_label": PGGraphStorage._encode_graph_label(target_node_label),
1002
- "properties": PGGraphStorage._format_properties(edge_properties),
1003
- }
 
 
1004
  # logger.info(f"-- inserting edge after formatted: {params}")
1005
  try:
1006
- await self._query(query, readonly=False, upsert_edge=True, **params)
1007
  logger.debug(
1008
  "Upserted edge from '{%s}' to '{%s}' with properties: {%s}",
1009
- source_node_label,
1010
- target_node_label,
1011
  edge_properties,
1012
  )
1013
  except Exception as e:
 
130
  data: Union[list, dict] = None,
131
  for_age: bool = False,
132
  graph_name: str = None,
133
+ upsert: bool = False,
134
  ):
135
  try:
136
  async with self.pool.acquire() as connection:
 
141
  await connection.execute(sql)
142
  else:
143
  await connection.execute(sql, *data.values())
144
+ except (
145
+ asyncpg.exceptions.UniqueViolationError,
146
+ asyncpg.exceptions.DuplicateTableError,
147
+ ) as e:
148
+ if upsert:
149
+ print("Key value duplicate, but upsert succeeded.")
150
+ else:
151
+ logger.error(f"Upsert error: {e}")
152
  except Exception as e:
153
+ logger.error(f"PostgreSQL database error: {e.__class__} - {e}")
154
  print(sql)
155
  print(data)
156
  raise
 
577
 
578
  if dtype == "vertex":
579
  vertex = json.loads(v)
580
+ field = vertex.get("properties")
581
  if not field:
582
  field = {}
583
+ field["label"] = PGGraphStorage._decode_graph_label(field["node_id"])
584
  d[k] = field
585
  # convert edge from id-label->id by replacing id with node information
586
  # we only do this if the vertex was also returned in the query
 
675
  # otherwise return the value stripping out some common special chars
676
  return field.replace("(", "_").replace(")", "")
677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  async def _query(
679
+ self, query: str, readonly: bool = True, upsert: bool = False
680
  ) -> List[Dict[str, Any]]:
681
  """
682
  Query the graph by taking a cypher query, converting it to an
 
690
  List[Dict[str, Any]]: a list of dictionaries containing the result set
691
  """
692
  # convert cypher query to pgsql/age query
693
+ wrapped_query = query
694
 
695
  # execute the query, rolling back on an error
696
  try:
 
702
  graph_name=self.graph_name,
703
  )
704
  else:
705
+ data = await self.db.execute(
706
+ wrapped_query,
707
+ for_age=True,
708
+ graph_name=self.graph_name,
709
+ upsert=upsert,
710
+ )
 
 
 
 
 
 
711
  except Exception as e:
712
  raise PGGraphQueryException(
713
  {
714
+ "message": f"Error executing graph query: {query}",
715
  "wrapped": wrapped_query,
716
  "detail": str(e),
717
  }
 
726
  return result
727
 
728
  async def has_node(self, node_id: str) -> bool:
729
+ entity_name_label = PGGraphStorage._encode_graph_label(node_id.strip('"'))
730
+
731
+ query = """SELECT * FROM cypher('%s', $$
732
+ MATCH (n:Entity {node_id: "%s"})
733
+ RETURN count(n) > 0 AS node_exists
734
+ $$) AS (node_exists bool)""" % (self.graph_name, entity_name_label)
735
 
736
+ single_result = (await self._query(query))[0]
 
 
737
  logger.debug(
738
  "{%s}:query:{%s}:result:{%s}",
739
  inspect.currentframe().f_code.co_name,
740
+ query,
741
  single_result["node_exists"],
742
  )
743
 
744
  return single_result["node_exists"]
745
 
746
  async def has_edge(self, source_node_id: str, target_node_id: str) -> bool:
747
+ src_label = PGGraphStorage._encode_graph_label(source_node_id.strip('"'))
748
+ tgt_label = PGGraphStorage._encode_graph_label(target_node_id.strip('"'))
749
+
750
+ query = """SELECT * FROM cypher('%s', $$
751
+ MATCH (a:Entity {node_id: "%s"})-[r]-(b:Entity {node_id: "%s"})
752
+ RETURN COUNT(r) > 0 AS edge_exists
753
+ $$) AS (edge_exists bool)""" % (
754
+ self.graph_name,
755
+ src_label,
756
+ tgt_label,
757
+ )
758
 
759
+ single_result = (await self._query(query))[0]
 
 
 
 
 
 
760
  logger.debug(
761
  "{%s}:query:{%s}:result:{%s}",
762
  inspect.currentframe().f_code.co_name,
763
+ query,
764
  single_result["edge_exists"],
765
  )
766
  return single_result["edge_exists"]
767
 
768
  async def get_node(self, node_id: str) -> Union[dict, None]:
769
+ label = PGGraphStorage._encode_graph_label(node_id.strip('"'))
770
+ query = """SELECT * FROM cypher('%s', $$
771
+ MATCH (n:Entity {node_id: "%s"})
772
+ RETURN n
773
+ $$) AS (n agtype)""" % (self.graph_name, label)
774
+ record = await self._query(query)
775
  if record:
776
  node = record[0]
777
  node_dict = node["n"]
778
  logger.debug(
779
  "{%s}: query: {%s}, result: {%s}",
780
  inspect.currentframe().f_code.co_name,
781
+ query,
782
  node_dict,
783
  )
784
  return node_dict
785
  return None
786
 
787
  async def node_degree(self, node_id: str) -> int:
788
+ label = PGGraphStorage._encode_graph_label(node_id.strip('"'))
789
 
790
+ query = """SELECT * FROM cypher('%s', $$
791
+ MATCH (n:Entity {node_id: "%s"})-[]->(x)
792
+ RETURN count(x) AS total_edge_count
793
+ $$) AS (total_edge_count integer)""" % (self.graph_name, label)
794
+ record = (await self._query(query))[0]
795
  if record:
796
  edge_count = int(record["total_edge_count"])
797
  logger.debug(
798
  "{%s}:query:{%s}:result:{%s}",
799
  inspect.currentframe().f_code.co_name,
800
+ query,
801
  edge_count,
802
  )
803
  return edge_count
804
 
805
  async def edge_degree(self, src_id: str, tgt_id: str) -> int:
806
+ src_degree = await self.node_degree(src_id)
807
+ trg_degree = await self.node_degree(tgt_id)
 
 
808
 
809
  # Convert None to 0 for addition
810
  src_degree = 0 if src_degree is None else src_degree
 
831
  Returns:
832
  list: List of all relationships/edges found
833
  """
834
+ src_label = PGGraphStorage._encode_graph_label(source_node_id.strip('"'))
835
+ tgt_label = PGGraphStorage._encode_graph_label(target_node_id.strip('"'))
836
+
837
+ query = """SELECT * FROM cypher('%s', $$
838
+ MATCH (a:Entity {node_id: "%s"})-[r]->(b:Entity {node_id: "%s"})
839
+ RETURN properties(r) as edge_properties
840
+ LIMIT 1
841
+ $$) AS (edge_properties agtype)""" % (
842
+ self.graph_name,
843
+ src_label,
844
+ tgt_label,
845
+ )
846
+ record = await self._query(query)
847
  if record and record[0] and record[0]["edge_properties"]:
848
  result = record[0]["edge_properties"]
849
  logger.debug(
850
  "{%s}:query:{%s}:result:{%s}",
851
  inspect.currentframe().f_code.co_name,
852
+ query,
853
  result,
854
  )
855
  return result
 
859
  Retrieves all edges (relationships) for a particular node identified by its label.
860
  :return: List of dictionaries containing edge information
861
  """
862
+ label = PGGraphStorage._encode_graph_label(source_node_id.strip('"'))
863
+
864
+ query = """SELECT * FROM cypher('%s', $$
865
+ MATCH (n:Entity {node_id: "%s"})
866
+ OPTIONAL MATCH (n)-[r]-(connected)
867
+ RETURN n, r, connected
868
+ $$) AS (n agtype, r agtype, connected agtype)""" % (
869
+ self.graph_name,
870
+ label,
871
+ )
872
 
873
+ results = await self._query(query)
 
 
 
 
874
  edges = []
875
  for record in results:
876
  source_node = record["n"] if record["n"] else None
877
  connected_node = record["connected"] if record["connected"] else None
878
 
879
  source_label = (
880
+ source_node["node_id"]
881
+ if source_node and source_node["node_id"]
882
+ else None
883
  )
884
  target_label = (
885
+ connected_node["node_id"]
886
+ if connected_node and connected_node["node_id"]
887
  else None
888
  )
889
 
890
  if source_label and target_label:
891
+ edges.append(
892
+ (
893
+ PGGraphStorage._decode_graph_label(source_label),
894
+ PGGraphStorage._decode_graph_label(target_label),
895
+ )
896
+ )
897
 
898
  return edges
899
 
 
910
  node_id: The unique identifier for the node (used as label)
911
  node_data: Dictionary of node properties
912
  """
913
+ label = PGGraphStorage._encode_graph_label(node_id.strip('"'))
914
  properties = node_data
915
 
916
+ query = """SELECT * FROM cypher('%s', $$
917
+ MERGE (n:Entity {node_id: "%s"})
918
+ SET n += %s
919
+ RETURN n
920
+ $$) AS (n agtype)""" % (
921
+ self.graph_name,
922
+ label,
923
+ PGGraphStorage._format_properties(properties),
924
+ )
925
+
926
  try:
927
+ await self._query(query, readonly=False, upsert=True)
928
  logger.debug(
929
  "Upserted node with label '{%s}' and properties: {%s}",
930
  label,
 
950
  target_node_id (str): Label of the target node (used as identifier)
951
  edge_data (dict): Dictionary of properties to set on the edge
952
  """
953
+ src_label = PGGraphStorage._encode_graph_label(source_node_id.strip('"'))
954
+ tgt_label = PGGraphStorage._encode_graph_label(target_node_id.strip('"'))
955
  edge_properties = edge_data
956
 
957
+ query = """SELECT * FROM cypher('%s', $$
958
+ MATCH (source:Entity {node_id: "%s"})
959
+ WITH source
960
+ MATCH (target:Entity {node_id: "%s"})
961
+ MERGE (source)-[r:DIRECTED]->(target)
962
+ SET r += %s
963
+ RETURN r
964
+ $$) AS (r agtype)""" % (
965
+ self.graph_name,
966
+ src_label,
967
+ tgt_label,
968
+ PGGraphStorage._format_properties(edge_properties),
969
+ )
970
  # logger.info(f"-- inserting edge after formatted: {params}")
971
  try:
972
+ await self._query(query, readonly=False, upsert=True)
973
  logger.debug(
974
  "Upserted edge from '{%s}' to '{%s}' with properties: {%s}",
975
+ src_label,
976
+ tgt_label,
977
  edge_properties,
978
  )
979
  except Exception as e:
lightrag/kg/postgres_impl_test.py CHANGED
@@ -61,7 +61,7 @@ db = PostgreSQLDB(
61
  "port": 15432,
62
  "user": "rag",
63
  "password": "rag",
64
- "database": "rag",
65
  }
66
  )
67
 
@@ -74,8 +74,12 @@ async def query_with_age():
74
  embedding_func=None,
75
  )
76
  graph.db = db
77
- res = await graph.get_node('"CHRISTMAS-TIME"')
78
  print("Node is: ", res)
 
 
 
 
79
 
80
 
81
  async def create_edge_with_age():
 
61
  "port": 15432,
62
  "user": "rag",
63
  "password": "rag",
64
+ "database": "r1",
65
  }
66
  )
67
 
 
74
  embedding_func=None,
75
  )
76
  graph.db = db
77
+ res = await graph.get_node('"A CHRISTMAS CAROL"')
78
  print("Node is: ", res)
79
+ res = await graph.get_edge('"A CHRISTMAS CAROL"', "PROJECT GUTENBERG")
80
+ print("Edge is: ", res)
81
+ res = await graph.get_node_edges('"SCROOGE"')
82
+ print("Node Edges are: ", res)
83
 
84
 
85
  async def create_edge_with_age():
lightrag/lightrag.py CHANGED
@@ -45,6 +45,7 @@ from .storage import (
45
 
46
  from .prompt import GRAPH_FIELD_SEP
47
 
 
48
  # future KG integrations
49
 
50
  # from .kg.ArangoDB_impl import (
@@ -168,7 +169,7 @@ class LightRAG:
168
 
169
  # LLM
170
  llm_model_func: callable = gpt_4o_mini_complete # hf_model_complete#
171
- llm_model_name: str = "meta-llama/Llama-3.2-1B-Instruct" #'meta-llama/Llama-3.2-1B'#'google/gemma-2-2b-it'
172
  llm_model_max_token_size: int = 32768
173
  llm_model_max_async: int = 16
174
  llm_model_kwargs: dict = field(default_factory=dict)
@@ -187,6 +188,10 @@ class LightRAG:
187
  # Add new field for document status storage type
188
  doc_status_storage: str = field(default="JsonDocStatusStorage")
189
 
 
 
 
 
190
  def __post_init__(self):
191
  log_file = os.path.join("lightrag.log")
192
  set_logger(log_file)
@@ -315,15 +320,25 @@ class LightRAG:
315
  "JsonDocStatusStorage": JsonDocStatusStorage,
316
  }
317
 
318
- def insert(self, string_or_strings):
 
 
319
  loop = always_get_an_event_loop()
320
- return loop.run_until_complete(self.ainsert(string_or_strings))
 
 
321
 
322
- async def ainsert(self, string_or_strings):
 
 
323
  """Insert documents with checkpoint support
324
 
325
  Args:
326
  string_or_strings: Single document string or list of document strings
 
 
 
 
327
  """
328
  if isinstance(string_or_strings, str):
329
  string_or_strings = [string_or_strings]
@@ -360,7 +375,7 @@ class LightRAG:
360
  batch_docs = dict(list(new_docs.items())[i : i + batch_size])
361
 
362
  for doc_id, doc in tqdm_async(
363
- batch_docs.items(), desc=f"Processing batch {i//batch_size + 1}"
364
  ):
365
  try:
366
  # Update status to processing
@@ -379,11 +394,14 @@ class LightRAG:
379
  **dp,
380
  "full_doc_id": doc_id,
381
  }
382
- for dp in chunking_by_token_size(
383
  doc["content"],
 
 
384
  overlap_token_size=self.chunk_overlap_token_size,
385
  max_token_size=self.chunk_token_size,
386
  tiktoken_model=self.tiktoken_model_name,
 
387
  )
388
  }
389
 
@@ -455,6 +473,73 @@ class LightRAG:
455
  # Ensure all indexes are updated after each document
456
  await self._insert_done()
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  async def _insert_done(self):
459
  tasks = []
460
  for storage_inst in [
 
45
 
46
  from .prompt import GRAPH_FIELD_SEP
47
 
48
+
49
  # future KG integrations
50
 
51
  # from .kg.ArangoDB_impl import (
 
169
 
170
  # LLM
171
  llm_model_func: callable = gpt_4o_mini_complete # hf_model_complete#
172
+ llm_model_name: str = "meta-llama/Llama-3.2-1B-Instruct" # 'meta-llama/Llama-3.2-1B'#'google/gemma-2-2b-it'
173
  llm_model_max_token_size: int = 32768
174
  llm_model_max_async: int = 16
175
  llm_model_kwargs: dict = field(default_factory=dict)
 
188
  # Add new field for document status storage type
189
  doc_status_storage: str = field(default="JsonDocStatusStorage")
190
 
191
+ # Custom Chunking Function
192
+ chunking_func: callable = chunking_by_token_size
193
+ chunking_func_kwargs: dict = field(default_factory=dict)
194
+
195
  def __post_init__(self):
196
  log_file = os.path.join("lightrag.log")
197
  set_logger(log_file)
 
320
  "JsonDocStatusStorage": JsonDocStatusStorage,
321
  }
322
 
323
+ def insert(
324
+ self, string_or_strings, split_by_character=None, split_by_character_only=False
325
+ ):
326
  loop = always_get_an_event_loop()
327
+ return loop.run_until_complete(
328
+ self.ainsert(string_or_strings, split_by_character, split_by_character_only)
329
+ )
330
 
331
+ async def ainsert(
332
+ self, string_or_strings, split_by_character=None, split_by_character_only=False
333
+ ):
334
  """Insert documents with checkpoint support
335
 
336
  Args:
337
  string_or_strings: Single document string or list of document strings
338
+ split_by_character: if split_by_character is not None, split the string by character, if chunk longer than
339
+ chunk_size, split the sub chunk by token size.
340
+ split_by_character_only: if split_by_character_only is True, split the string by character only, when
341
+ split_by_character is None, this parameter is ignored.
342
  """
343
  if isinstance(string_or_strings, str):
344
  string_or_strings = [string_or_strings]
 
375
  batch_docs = dict(list(new_docs.items())[i : i + batch_size])
376
 
377
  for doc_id, doc in tqdm_async(
378
+ batch_docs.items(), desc=f"Processing batch {i // batch_size + 1}"
379
  ):
380
  try:
381
  # Update status to processing
 
394
  **dp,
395
  "full_doc_id": doc_id,
396
  }
397
+ for dp in self.chunking_func(
398
  doc["content"],
399
+ split_by_character=split_by_character,
400
+ split_by_character_only=split_by_character_only,
401
  overlap_token_size=self.chunk_overlap_token_size,
402
  max_token_size=self.chunk_token_size,
403
  tiktoken_model=self.tiktoken_model_name,
404
+ **self.chunking_func_kwargs,
405
  )
406
  }
407
 
 
473
  # Ensure all indexes are updated after each document
474
  await self._insert_done()
475
 
476
+ def insert_custom_chunks(self, full_text: str, text_chunks: list[str]):
477
+ loop = always_get_an_event_loop()
478
+ return loop.run_until_complete(
479
+ self.ainsert_custom_chunks(full_text, text_chunks)
480
+ )
481
+
482
+ async def ainsert_custom_chunks(self, full_text: str, text_chunks: list[str]):
483
+ update_storage = False
484
+ try:
485
+ doc_key = compute_mdhash_id(full_text.strip(), prefix="doc-")
486
+ new_docs = {doc_key: {"content": full_text.strip()}}
487
+
488
+ _add_doc_keys = await self.full_docs.filter_keys([doc_key])
489
+ new_docs = {k: v for k, v in new_docs.items() if k in _add_doc_keys}
490
+ if not len(new_docs):
491
+ logger.warning("This document is already in the storage.")
492
+ return
493
+
494
+ update_storage = True
495
+ logger.info(f"[New Docs] inserting {len(new_docs)} docs")
496
+
497
+ inserting_chunks = {}
498
+ for chunk_text in text_chunks:
499
+ chunk_text_stripped = chunk_text.strip()
500
+ chunk_key = compute_mdhash_id(chunk_text_stripped, prefix="chunk-")
501
+
502
+ inserting_chunks[chunk_key] = {
503
+ "content": chunk_text_stripped,
504
+ "full_doc_id": doc_key,
505
+ }
506
+
507
+ _add_chunk_keys = await self.text_chunks.filter_keys(
508
+ list(inserting_chunks.keys())
509
+ )
510
+ inserting_chunks = {
511
+ k: v for k, v in inserting_chunks.items() if k in _add_chunk_keys
512
+ }
513
+ if not len(inserting_chunks):
514
+ logger.warning("All chunks are already in the storage.")
515
+ return
516
+
517
+ logger.info(f"[New Chunks] inserting {len(inserting_chunks)} chunks")
518
+
519
+ await self.chunks_vdb.upsert(inserting_chunks)
520
+
521
+ logger.info("[Entity Extraction]...")
522
+ maybe_new_kg = await extract_entities(
523
+ inserting_chunks,
524
+ knowledge_graph_inst=self.chunk_entity_relation_graph,
525
+ entity_vdb=self.entities_vdb,
526
+ relationships_vdb=self.relationships_vdb,
527
+ global_config=asdict(self),
528
+ )
529
+
530
+ if maybe_new_kg is None:
531
+ logger.warning("No new entities and relationships found")
532
+ return
533
+ else:
534
+ self.chunk_entity_relation_graph = maybe_new_kg
535
+
536
+ await self.full_docs.upsert(new_docs)
537
+ await self.text_chunks.upsert(inserting_chunks)
538
+
539
+ finally:
540
+ if update_storage:
541
+ await self._insert_done()
542
+
543
  async def _insert_done(self):
544
  tasks = []
545
  for storage_inst in [
lightrag/llm.py CHANGED
@@ -406,8 +406,9 @@ async def lollms_model_if_cache(
406
  full_prompt += prompt
407
 
408
  request_data["prompt"] = full_prompt
 
409
 
410
- async with aiohttp.ClientSession() as session:
411
  if stream:
412
 
413
  async def inner():
 
406
  full_prompt += prompt
407
 
408
  request_data["prompt"] = full_prompt
409
+ timeout = aiohttp.ClientTimeout(total=kwargs.get("timeout", None))
410
 
411
+ async with aiohttp.ClientSession(timeout=timeout) as session:
412
  if stream:
413
 
414
  async def inner():
lightrag/operate.py CHANGED
@@ -4,7 +4,6 @@ import re
4
  from tqdm.asyncio import tqdm as tqdm_async
5
  from typing import Union
6
  from collections import Counter, defaultdict
7
- import warnings
8
  from .utils import (
9
  logger,
10
  clean_str,
@@ -34,23 +33,61 @@ import time
34
 
35
 
36
  def chunking_by_token_size(
37
- content: str, overlap_token_size=128, max_token_size=1024, tiktoken_model="gpt-4o"
 
 
 
 
 
 
38
  ):
39
  tokens = encode_string_by_tiktoken(content, model_name=tiktoken_model)
40
  results = []
41
- for index, start in enumerate(
42
- range(0, len(tokens), max_token_size - overlap_token_size)
43
- ):
44
- chunk_content = decode_tokens_by_tiktoken(
45
- tokens[start : start + max_token_size], model_name=tiktoken_model
46
- )
47
- results.append(
48
- {
49
- "tokens": min(max_token_size, len(tokens) - start),
50
- "content": chunk_content.strip(),
51
- "chunk_order_index": index,
52
- }
53
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  return results
55
 
56
 
@@ -582,15 +619,22 @@ async def kg_query(
582
  logger.warning("low_level_keywords and high_level_keywords is empty")
583
  return PROMPTS["fail_response"]
584
  if ll_keywords == [] and query_param.mode in ["local", "hybrid"]:
585
- logger.warning("low_level_keywords is empty")
586
- return PROMPTS["fail_response"]
587
- else:
588
- ll_keywords = ", ".join(ll_keywords)
 
589
  if hl_keywords == [] and query_param.mode in ["global", "hybrid"]:
590
- logger.warning("high_level_keywords is empty")
591
- return PROMPTS["fail_response"]
592
- else:
593
- hl_keywords = ", ".join(hl_keywords)
 
 
 
 
 
 
594
 
595
  # Build context
596
  keywords = [ll_keywords, hl_keywords]
@@ -656,77 +700,51 @@ async def _build_query_context(
656
  # ll_entities_context, ll_relations_context, ll_text_units_context = "", "", ""
657
  # hl_entities_context, hl_relations_context, hl_text_units_context = "", "", ""
658
 
659
- ll_kewwords, hl_keywrds = query[0], query[1]
660
- if query_param.mode in ["local", "hybrid"]:
661
- if ll_kewwords == "":
662
- ll_entities_context, ll_relations_context, ll_text_units_context = (
663
- "",
664
- "",
665
- "",
666
- )
667
- warnings.warn(
668
- "Low Level context is None. Return empty Low entity/relationship/source"
669
- )
670
- query_param.mode = "global"
671
- else:
672
- (
673
- ll_entities_context,
674
- ll_relations_context,
675
- ll_text_units_context,
676
- ) = await _get_node_data(
677
- ll_kewwords,
678
- knowledge_graph_inst,
679
- entities_vdb,
680
- text_chunks_db,
681
- query_param,
682
- )
683
- if query_param.mode in ["global", "hybrid"]:
684
- if hl_keywrds == "":
685
- hl_entities_context, hl_relations_context, hl_text_units_context = (
686
- "",
687
- "",
688
- "",
689
- )
690
- warnings.warn(
691
- "High Level context is None. Return empty High entity/relationship/source"
692
- )
693
- query_param.mode = "local"
694
- else:
695
- (
696
- hl_entities_context,
697
- hl_relations_context,
698
- hl_text_units_context,
699
- ) = await _get_edge_data(
700
- hl_keywrds,
701
- knowledge_graph_inst,
702
- relationships_vdb,
703
- text_chunks_db,
704
- query_param,
705
- )
706
- if (
707
- hl_entities_context == ""
708
- and hl_relations_context == ""
709
- and hl_text_units_context == ""
710
- ):
711
- logger.warn("No high level context found. Switching to local mode.")
712
- query_param.mode = "local"
713
- if query_param.mode == "hybrid":
714
- entities_context, relations_context, text_units_context = combine_contexts(
715
- [hl_entities_context, ll_entities_context],
716
- [hl_relations_context, ll_relations_context],
717
- [hl_text_units_context, ll_text_units_context],
718
  )
719
- elif query_param.mode == "local":
720
- entities_context, relations_context, text_units_context = (
721
  ll_entities_context,
722
  ll_relations_context,
723
  ll_text_units_context,
 
 
 
 
 
 
724
  )
725
- elif query_param.mode == "global":
726
- entities_context, relations_context, text_units_context = (
727
  hl_entities_context,
728
  hl_relations_context,
729
  hl_text_units_context,
 
 
 
 
 
 
 
 
 
 
 
730
  )
731
  return f"""
732
  -----Entities-----
 
4
  from tqdm.asyncio import tqdm as tqdm_async
5
  from typing import Union
6
  from collections import Counter, defaultdict
 
7
  from .utils import (
8
  logger,
9
  clean_str,
 
33
 
34
 
35
  def chunking_by_token_size(
36
+ content: str,
37
+ split_by_character=None,
38
+ split_by_character_only=False,
39
+ overlap_token_size=128,
40
+ max_token_size=1024,
41
+ tiktoken_model="gpt-4o",
42
+ **kwargs,
43
  ):
44
  tokens = encode_string_by_tiktoken(content, model_name=tiktoken_model)
45
  results = []
46
+ if split_by_character:
47
+ raw_chunks = content.split(split_by_character)
48
+ new_chunks = []
49
+ if split_by_character_only:
50
+ for chunk in raw_chunks:
51
+ _tokens = encode_string_by_tiktoken(chunk, model_name=tiktoken_model)
52
+ new_chunks.append((len(_tokens), chunk))
53
+ else:
54
+ for chunk in raw_chunks:
55
+ _tokens = encode_string_by_tiktoken(chunk, model_name=tiktoken_model)
56
+ if len(_tokens) > max_token_size:
57
+ for start in range(
58
+ 0, len(_tokens), max_token_size - overlap_token_size
59
+ ):
60
+ chunk_content = decode_tokens_by_tiktoken(
61
+ _tokens[start : start + max_token_size],
62
+ model_name=tiktoken_model,
63
+ )
64
+ new_chunks.append(
65
+ (min(max_token_size, len(_tokens) - start), chunk_content)
66
+ )
67
+ else:
68
+ new_chunks.append((len(_tokens), chunk))
69
+ for index, (_len, chunk) in enumerate(new_chunks):
70
+ results.append(
71
+ {
72
+ "tokens": _len,
73
+ "content": chunk.strip(),
74
+ "chunk_order_index": index,
75
+ }
76
+ )
77
+ else:
78
+ for index, start in enumerate(
79
+ range(0, len(tokens), max_token_size - overlap_token_size)
80
+ ):
81
+ chunk_content = decode_tokens_by_tiktoken(
82
+ tokens[start : start + max_token_size], model_name=tiktoken_model
83
+ )
84
+ results.append(
85
+ {
86
+ "tokens": min(max_token_size, len(tokens) - start),
87
+ "content": chunk_content.strip(),
88
+ "chunk_order_index": index,
89
+ }
90
+ )
91
  return results
92
 
93
 
 
619
  logger.warning("low_level_keywords and high_level_keywords is empty")
620
  return PROMPTS["fail_response"]
621
  if ll_keywords == [] and query_param.mode in ["local", "hybrid"]:
622
+ logger.warning(
623
+ "low_level_keywords is empty, switching from %s mode to global mode",
624
+ query_param.mode,
625
+ )
626
+ query_param.mode = "global"
627
  if hl_keywords == [] and query_param.mode in ["global", "hybrid"]:
628
+ logger.warning(
629
+ "high_level_keywords is empty, switching from %s mode to local mode",
630
+ query_param.mode,
631
+ )
632
+ query_param.mode = "local"
633
+
634
+ ll_keywords = ", ".join(ll_keywords) if ll_keywords else ""
635
+ hl_keywords = ", ".join(hl_keywords) if hl_keywords else ""
636
+
637
+ logger.info("Using %s mode for query processing", query_param.mode)
638
 
639
  # Build context
640
  keywords = [ll_keywords, hl_keywords]
 
700
  # ll_entities_context, ll_relations_context, ll_text_units_context = "", "", ""
701
  # hl_entities_context, hl_relations_context, hl_text_units_context = "", "", ""
702
 
703
+ ll_keywords, hl_keywords = query[0], query[1]
704
+
705
+ if query_param.mode == "local":
706
+ entities_context, relations_context, text_units_context = await _get_node_data(
707
+ ll_keywords,
708
+ knowledge_graph_inst,
709
+ entities_vdb,
710
+ text_chunks_db,
711
+ query_param,
712
+ )
713
+ elif query_param.mode == "global":
714
+ entities_context, relations_context, text_units_context = await _get_edge_data(
715
+ hl_keywords,
716
+ knowledge_graph_inst,
717
+ relationships_vdb,
718
+ text_chunks_db,
719
+ query_param,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  )
721
+ else: # hybrid mode
722
+ (
723
  ll_entities_context,
724
  ll_relations_context,
725
  ll_text_units_context,
726
+ ) = await _get_node_data(
727
+ ll_keywords,
728
+ knowledge_graph_inst,
729
+ entities_vdb,
730
+ text_chunks_db,
731
+ query_param,
732
  )
733
+ (
 
734
  hl_entities_context,
735
  hl_relations_context,
736
  hl_text_units_context,
737
+ ) = await _get_edge_data(
738
+ hl_keywords,
739
+ knowledge_graph_inst,
740
+ relationships_vdb,
741
+ text_chunks_db,
742
+ query_param,
743
+ )
744
+ entities_context, relations_context, text_units_context = combine_contexts(
745
+ [hl_entities_context, ll_entities_context],
746
+ [hl_relations_context, ll_relations_context],
747
+ [hl_text_units_context, ll_text_units_context],
748
  )
749
  return f"""
750
  -----Entities-----
requirements.txt CHANGED
@@ -1,38 +1,38 @@
1
  accelerate
2
- aioboto3~=13.3.0
3
- aiofiles~=24.1.0
4
- aiohttp~=3.11.11
5
- asyncpg~=0.30.0
6
 
7
  # database packages
8
  graspologic
9
  gremlinpython
10
  hnswlib
11
  nano-vectordb
12
- neo4j~=5.27.0
13
- networkx~=3.2.1
14
 
15
- numpy~=2.2.0
16
- ollama~=0.4.4
17
- openai~=1.58.1
18
  oracledb
19
- psycopg-pool~=3.2.4
20
- psycopg[binary,pool]~=3.2.3
21
- pydantic~=2.10.4
22
  pymilvus
23
  pymongo
24
  pymysql
25
- python-dotenv~=1.0.1
26
- pyvis~=0.3.2
27
- setuptools~=70.0.0
28
  # lmdeploy[all]
29
- sqlalchemy~=2.0.36
30
- tenacity~=9.0.0
31
 
32
 
33
  # LLM packages
34
- tiktoken~=0.8.0
35
- torch~=2.5.1+cu121
36
- tqdm~=4.67.1
37
- transformers~=4.47.1
38
  xxhash
 
1
  accelerate
2
+ aioboto3
3
+ aiofiles
4
+ aiohttp
5
+ asyncpg
6
 
7
  # database packages
8
  graspologic
9
  gremlinpython
10
  hnswlib
11
  nano-vectordb
12
+ neo4j
13
+ networkx
14
 
15
+ numpy
16
+ ollama
17
+ openai
18
  oracledb
19
+ psycopg-pool
20
+ psycopg[binary,pool]
21
+ pydantic
22
  pymilvus
23
  pymongo
24
  pymysql
25
+ python-dotenv
26
+ pyvis
27
+ setuptools
28
  # lmdeploy[all]
29
+ sqlalchemy
30
+ tenacity
31
 
32
 
33
  # LLM packages
34
+ tiktoken
35
+ torch
36
+ tqdm
37
+ transformers
38
  xxhash
setup.py CHANGED
@@ -100,10 +100,7 @@ setuptools.setup(
100
  },
101
  entry_points={
102
  "console_scripts": [
103
- "lollms-lightrag-server=lightrag.api.lollms_lightrag_server:main [api]",
104
- "ollama-lightrag-server=lightrag.api.ollama_lightrag_server:main [api]",
105
- "openai-lightrag-server=lightrag.api.openai_lightrag_server:main [api]",
106
- "azure-openai-lightrag-server=lightrag.api.azure_openai_lightrag_server:main [api]",
107
  ],
108
  },
109
  )
 
100
  },
101
  entry_points={
102
  "console_scripts": [
103
+ "lightrag-server=lightrag.api.lightrag_server:main [api]",
 
 
 
104
  ],
105
  },
106
  )