samuel-z-chen commited on
Commit
b6db833
·
1 Parent(s): d19a515

Some enhancements:

Browse files

- Enable the llm_cache storage to support get_by_mode_and_id, to improve the performance for using real KV server
- Provide an option for the developers to cache the LLM response when extracting entities for a document. Solving the paint point that sometimes the process failed, the processed chunks we need to call LLM again, money and time wasted. With the new option (by default not enabled) enabling, we can cache that result, can significantly save the time and money for beginners.

README.md CHANGED
@@ -26,6 +26,7 @@ This repository hosts the code of LightRAG. The structure of this code is based
26
  </div>
27
 
28
  ## 🎉 News
 
29
  - [x] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
30
  - [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.
31
  - [x] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
@@ -356,6 +357,11 @@ rag = LightRAG(
356
  ```
357
  see test_neo4j.py for a working example.
358
 
 
 
 
 
 
359
  ### Insert Custom KG
360
 
361
  ```python
@@ -602,33 +608,34 @@ if __name__ == "__main__":
602
 
603
  ### LightRAG init parameters
604
 
605
- | **Parameter** | **Type** | **Explanation** | **Default** |
606
- | --- | --- | --- | --- |
607
- | **working\_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` |
608
- | **kv\_storage** | `str` | Storage type for documents and text chunks. Supported types: `JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` |
609
- | **vector\_storage** | `str` | Storage type for embedding vectors. Supported types: `NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` |
610
- | **graph\_storage** | `str` | Storage type for graph edges and nodes. Supported types: `NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` |
611
- | **log\_level** | | Log level for application runtime | `logging.DEBUG` |
612
- | **chunk\_token\_size** | `int` | Maximum token size per chunk when splitting documents | `1200` |
613
- | **chunk\_overlap\_token\_size** | `int` | Overlap token size between two chunks when splitting documents | `100` |
614
- | **tiktoken\_model\_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` |
615
- | **entity\_extract\_max\_gleaning** | `int` | Number of loops in the entity extraction process, appending history messages | `1` |
616
- | **entity\_summary\_to\_max\_tokens** | `int` | Maximum token size for each entity summary | `500` |
617
- | **node\_embedding\_algorithm** | `str` | Algorithm for node embedding (currently not used) | `node2vec` |
618
- | **node2vec\_params** | `dict` | Parameters for node embedding | `{"dimensions": 1536,"num_walks": 10,"walk_length": 40,"window_size": 2,"iterations": 3,"random_seed": 3,}` |
619
- | **embedding\_func** | `EmbeddingFunc` | Function to generate embedding vectors from text | `openai_embedding` |
620
- | **embedding\_batch\_num** | `int` | Maximum batch size for embedding processes (multiple texts sent per batch) | `32` |
621
- | **embedding\_func\_max\_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
622
- | **llm\_model\_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
623
- | **llm\_model\_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
624
- | **llm\_model\_max\_token\_size** | `int` | Maximum token size for LLM generation (affects entity relation summaries) | `32768` |
625
- | **llm\_model\_max\_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `16` |
626
- | **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
627
- | **vector\_db\_storage\_cls\_kwargs** | `dict` | Additional parameters for vector database (currently not used) | |
628
- | **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
629
- | **addon\_params** | `dict` | Additional parameters, e.g., `{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"], "insert_batch_size": 10}`: sets example limit, output language, and batch size for document processing | `example_number: all examples, language: English, insert_batch_size: 10` |
630
- | **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
631
- | **embedding\_cache\_config** | `dict` | Configuration for question-answer caching. Contains three parameters:<br>- `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers.<br>- `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM.<br>- `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default: `{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
 
632
 
633
  ### Error Handling
634
  <details>
@@ -1206,6 +1213,7 @@ curl "http://localhost:9621/health"
1206
  ```
1207
 
1208
  ## Development
 
1209
 
1210
  ### Running in Development Mode
1211
 
 
26
  </div>
27
 
28
  ## 🎉 News
29
+ - [x] [2025.01.06]🎯📢You can now [use 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.
 
357
  ```
358
  see test_neo4j.py for a working example.
359
 
360
+ ### Using PostgreSQL for Storage
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
 
367
  ```python
 
608
 
609
  ### LightRAG init parameters
610
 
611
+ | **Parameter** | **Type** | **Explanation** | **Default** |
612
+ |----------------------------------------------| --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
613
+ | **working\_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` |
614
+ | **kv\_storage** | `str` | Storage type for documents and text chunks. Supported types: `JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` |
615
+ | **vector\_storage** | `str` | Storage type for embedding vectors. Supported types: `NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` |
616
+ | **graph\_storage** | `str` | Storage type for graph edges and nodes. Supported types: `NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` |
617
+ | **log\_level** | | Log level for application runtime | `logging.DEBUG` |
618
+ | **chunk\_token\_size** | `int` | Maximum token size per chunk when splitting documents | `1200` |
619
+ | **chunk\_overlap\_token\_size** | `int` | Overlap token size between two chunks when splitting documents | `100` |
620
+ | **tiktoken\_model\_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` |
621
+ | **entity\_extract\_max\_gleaning** | `int` | Number of loops in the entity extraction process, appending history messages | `1` |
622
+ | **entity\_summary\_to\_max\_tokens** | `int` | Maximum token size for each entity summary | `500` |
623
+ | **node\_embedding\_algorithm** | `str` | Algorithm for node embedding (currently not used) | `node2vec` |
624
+ | **node2vec\_params** | `dict` | Parameters for node embedding | `{"dimensions": 1536,"num_walks": 10,"walk_length": 40,"window_size": 2,"iterations": 3,"random_seed": 3,}` |
625
+ | **embedding\_func** | `EmbeddingFunc` | Function to generate embedding vectors from text | `openai_embedding` |
626
+ | **embedding\_batch\_num** | `int` | Maximum batch size for embedding processes (multiple texts sent per batch) | `32` |
627
+ | **embedding\_func\_max\_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
628
+ | **llm\_model\_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
629
+ | **llm\_model\_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
630
+ | **llm\_model\_max\_token\_size** | `int` | Maximum token size for LLM generation (affects entity relation summaries) | `32768` |
631
+ | **llm\_model\_max\_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `16` |
632
+ | **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
633
+ | **vector\_db\_storage\_cls\_kwargs** | `dict` | Additional parameters for vector database (currently not used) | |
634
+ | **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
635
+ | **enable\_llm\_cache\_for\_entity\_extract** | `bool` | If `TRUE`, stores LLM results in cache for entity extraction; Good for beginners to debug your application | `FALSE` |
636
+ | **addon\_params** | `dict` | Additional parameters, e.g., `{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"], "insert_batch_size": 10}`: sets example limit, output language, and batch size for document processing | `example_number: all examples, language: English, insert_batch_size: 10` |
637
+ | **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
638
+ | **embedding\_cache\_config** | `dict` | Configuration for question-answer caching. Contains three parameters:<br>- `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers.<br>- `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM.<br>- `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default: `{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
639
 
640
  ### Error Handling
641
  <details>
 
1213
  ```
1214
 
1215
  ## Development
1216
+ Contribute to the project: [Guide](contributor-readme.MD)
1217
 
1218
  ### Running in Development Mode
1219
 
contributor-readme.MD ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Handy Tips for Developers Who Want to Contribute to the Project
2
+ ## Pre-commit Hooks
3
+ Please ensure you have run pre-commit hooks before committing your changes.
4
+ ### Guides
5
+ 1. **Installing Pre-commit Hooks**:
6
+ - Install pre-commit using pip: `pip install pre-commit`
7
+ - Initialize pre-commit in your repository: `pre-commit install`
8
+ - Run pre-commit hooks: `pre-commit run --all-files`
9
+
10
+ 2. **Pre-commit Hooks Configuration**:
11
+ - Create a `.pre-commit-config.yaml` file in the root of your repository.
12
+ - Add your hooks to the `.pre-commit-config.yaml`file.
examples/lightrag_zhipu_postgres_demo.py CHANGED
@@ -43,6 +43,7 @@ async def main():
43
  llm_model_name="glm-4-flashx",
44
  llm_model_max_async=4,
45
  llm_model_max_token_size=32768,
 
46
  embedding_func=EmbeddingFunc(
47
  embedding_dim=768,
48
  max_token_size=8192,
 
43
  llm_model_name="glm-4-flashx",
44
  llm_model_max_async=4,
45
  llm_model_max_token_size=32768,
46
+ enable_llm_cache_for_entity_extract=True,
47
  embedding_func=EmbeddingFunc(
48
  embedding_dim=768,
49
  max_token_size=8192,
lightrag/kg/postgres_impl.py CHANGED
@@ -151,7 +151,10 @@ class PostgreSQLDB:
151
  try:
152
  await conn.execute('SET search_path = ag_catalog, "$user", public')
153
  await conn.execute(f"""select create_graph('{graph_name}')""")
154
- except asyncpg.exceptions.InvalidSchemaNameError:
 
 
 
155
  pass
156
 
157
 
@@ -160,7 +163,6 @@ class PGKVStorage(BaseKVStorage):
160
  db: PostgreSQLDB = None
161
 
162
  def __post_init__(self):
163
- self._data = {}
164
  self._max_batch_size = self.global_config["embedding_batch_num"]
165
 
166
  ################ QUERY METHODS ################
@@ -181,6 +183,19 @@ class PGKVStorage(BaseKVStorage):
181
  else:
182
  return None
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  # Query by id
185
  async def get_by_ids(self, ids: List[str], fields=None) -> Union[List[dict], None]:
186
  """Get doc_chunks data by id"""
@@ -229,33 +244,30 @@ class PGKVStorage(BaseKVStorage):
229
 
230
  ################ INSERT METHODS ################
231
  async def upsert(self, data: Dict[str, dict]):
232
- left_data = {k: v for k, v in data.items() if k not in self._data}
233
- self._data.update(left_data)
234
  if self.namespace == "text_chunks":
235
  pass
236
  elif self.namespace == "full_docs":
237
- for k, v in self._data.items():
238
  upsert_sql = SQL_TEMPLATES["upsert_doc_full"]
239
- data = {
240
  "id": k,
241
  "content": v["content"],
242
  "workspace": self.db.workspace,
243
  }
244
- await self.db.execute(upsert_sql, data)
245
  elif self.namespace == "llm_response_cache":
246
- for mode, items in self._data.items():
247
  for k, v in items.items():
248
  upsert_sql = SQL_TEMPLATES["upsert_llm_response_cache"]
249
- data = {
250
  "workspace": self.db.workspace,
251
  "id": k,
252
  "original_prompt": v["original_prompt"],
253
- "return": v["return"],
254
  "mode": mode,
255
  }
256
- await self.db.execute(upsert_sql, data)
257
 
258
- return left_data
259
 
260
  async def index_done_callback(self):
261
  if self.namespace in ["full_docs", "text_chunks"]:
@@ -977,9 +989,6 @@ class PGGraphStorage(BaseGraphStorage):
977
  source_node_label = source_node_id.strip('"')
978
  target_node_label = target_node_id.strip('"')
979
  edge_properties = edge_data
980
- logger.info(
981
- f"-- inserting edge: {source_node_label} -> {target_node_label}: {edge_data}"
982
- )
983
 
984
  query = """MATCH (source:`{src_label}`)
985
  WITH source
@@ -1028,8 +1037,8 @@ TABLES = {
1028
  doc_name VARCHAR(1024),
1029
  content TEXT,
1030
  meta JSONB,
1031
- createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1032
- updatetime TIMESTAMP,
1033
  CONSTRAINT LIGHTRAG_DOC_FULL_PK PRIMARY KEY (workspace, id)
1034
  )"""
1035
  },
@@ -1042,8 +1051,8 @@ TABLES = {
1042
  tokens INTEGER,
1043
  content TEXT,
1044
  content_vector VECTOR,
1045
- createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1046
- updatetime TIMESTAMP,
1047
  CONSTRAINT LIGHTRAG_DOC_CHUNKS_PK PRIMARY KEY (workspace, id)
1048
  )"""
1049
  },
@@ -1054,8 +1063,8 @@ TABLES = {
1054
  entity_name VARCHAR(255),
1055
  content TEXT,
1056
  content_vector VECTOR,
1057
- createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1058
- updatetime TIMESTAMP,
1059
  CONSTRAINT LIGHTRAG_VDB_ENTITY_PK PRIMARY KEY (workspace, id)
1060
  )"""
1061
  },
@@ -1067,8 +1076,8 @@ TABLES = {
1067
  target_id VARCHAR(256),
1068
  content TEXT,
1069
  content_vector VECTOR,
1070
- createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1071
- updatetime TIMESTAMP,
1072
  CONSTRAINT LIGHTRAG_VDB_RELATION_PK PRIMARY KEY (workspace, id)
1073
  )"""
1074
  },
@@ -1078,10 +1087,10 @@ TABLES = {
1078
  id varchar(255) NOT NULL,
1079
  mode varchar(32) NOT NULL,
1080
  original_prompt TEXT,
1081
- return TEXT,
1082
- createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1083
- updatetime TIMESTAMP,
1084
- CONSTRAINT LIGHTRAG_LLM_CACHE_PK PRIMARY KEY (workspace, id)
1085
  )"""
1086
  },
1087
  "LIGHTRAG_DOC_STATUS": {
@@ -1109,9 +1118,12 @@ SQL_TEMPLATES = {
1109
  chunk_order_index, full_doc_id
1110
  FROM LIGHTRAG_DOC_CHUNKS WHERE workspace=$1 AND id=$2
1111
  """,
1112
- "get_by_id_llm_response_cache": """SELECT id, original_prompt, COALESCE("return", '') as "return", mode
1113
  FROM LIGHTRAG_LLM_CACHE WHERE workspace=$1 AND mode=$2
1114
  """,
 
 
 
1115
  "get_by_ids_full_docs": """SELECT id, COALESCE(content, '') as content
1116
  FROM LIGHTRAG_DOC_FULL WHERE workspace=$1 AND id IN ({ids})
1117
  """,
@@ -1119,22 +1131,22 @@ SQL_TEMPLATES = {
1119
  chunk_order_index, full_doc_id
1120
  FROM LIGHTRAG_DOC_CHUNKS WHERE workspace=$1 AND id IN ({ids})
1121
  """,
1122
- "get_by_ids_llm_response_cache": """SELECT id, original_prompt, COALESCE("return", '') as "return", mode
1123
  FROM LIGHTRAG_LLM_CACHE WHERE workspace=$1 AND mode= IN ({ids})
1124
  """,
1125
  "filter_keys": "SELECT id FROM {table_name} WHERE workspace=$1 AND id IN ({ids})",
1126
  "upsert_doc_full": """INSERT INTO LIGHTRAG_DOC_FULL (id, content, workspace)
1127
  VALUES ($1, $2, $3)
1128
  ON CONFLICT (workspace,id) DO UPDATE
1129
- SET content = $2, updatetime = CURRENT_TIMESTAMP
1130
  """,
1131
- "upsert_llm_response_cache": """INSERT INTO LIGHTRAG_LLM_CACHE(workspace,id,original_prompt,"return",mode)
1132
  VALUES ($1, $2, $3, $4, $5)
1133
- ON CONFLICT (workspace,id) DO UPDATE
1134
  SET original_prompt = EXCLUDED.original_prompt,
1135
- "return"=EXCLUDED."return",
1136
  mode=EXCLUDED.mode,
1137
- updatetime = CURRENT_TIMESTAMP
1138
  """,
1139
  "upsert_chunk": """INSERT INTO LIGHTRAG_DOC_CHUNKS (workspace, id, tokens,
1140
  chunk_order_index, full_doc_id, content, content_vector)
@@ -1145,7 +1157,7 @@ SQL_TEMPLATES = {
1145
  full_doc_id=EXCLUDED.full_doc_id,
1146
  content = EXCLUDED.content,
1147
  content_vector=EXCLUDED.content_vector,
1148
- updatetime = CURRENT_TIMESTAMP
1149
  """,
1150
  "upsert_entity": """INSERT INTO LIGHTRAG_VDB_ENTITY (workspace, id, entity_name, content, content_vector)
1151
  VALUES ($1, $2, $3, $4, $5)
@@ -1153,7 +1165,7 @@ SQL_TEMPLATES = {
1153
  SET entity_name=EXCLUDED.entity_name,
1154
  content=EXCLUDED.content,
1155
  content_vector=EXCLUDED.content_vector,
1156
- updatetime=CURRENT_TIMESTAMP
1157
  """,
1158
  "upsert_relationship": """INSERT INTO LIGHTRAG_VDB_RELATION (workspace, id, source_id,
1159
  target_id, content, content_vector)
@@ -1162,7 +1174,7 @@ SQL_TEMPLATES = {
1162
  SET source_id=EXCLUDED.source_id,
1163
  target_id=EXCLUDED.target_id,
1164
  content=EXCLUDED.content,
1165
- content_vector=EXCLUDED.content_vector, updatetime = CURRENT_TIMESTAMP
1166
  """,
1167
  # SQL for VectorStorage
1168
  "entities": """SELECT entity_name FROM
 
151
  try:
152
  await conn.execute('SET search_path = ag_catalog, "$user", public')
153
  await conn.execute(f"""select create_graph('{graph_name}')""")
154
+ except (
155
+ asyncpg.exceptions.InvalidSchemaNameError,
156
+ asyncpg.exceptions.UniqueViolationError,
157
+ ):
158
  pass
159
 
160
 
 
163
  db: PostgreSQLDB = None
164
 
165
  def __post_init__(self):
 
166
  self._max_batch_size = self.global_config["embedding_batch_num"]
167
 
168
  ################ QUERY METHODS ################
 
183
  else:
184
  return None
185
 
186
+ async def get_by_mode_and_id(self, mode: str, id: str) -> Union[dict, None]:
187
+ """Specifically for llm_response_cache."""
188
+ sql = SQL_TEMPLATES["get_by_mode_id_" + self.namespace]
189
+ params = {"workspace": self.db.workspace, mode: mode, "id": id}
190
+ if "llm_response_cache" == self.namespace:
191
+ array_res = await self.db.query(sql, params, multirows=True)
192
+ res = {}
193
+ for row in array_res:
194
+ res[row["id"]] = row
195
+ return res
196
+ else:
197
+ return None
198
+
199
  # Query by id
200
  async def get_by_ids(self, ids: List[str], fields=None) -> Union[List[dict], None]:
201
  """Get doc_chunks data by id"""
 
244
 
245
  ################ INSERT METHODS ################
246
  async def upsert(self, data: Dict[str, dict]):
 
 
247
  if self.namespace == "text_chunks":
248
  pass
249
  elif self.namespace == "full_docs":
250
+ for k, v in data.items():
251
  upsert_sql = SQL_TEMPLATES["upsert_doc_full"]
252
+ _data = {
253
  "id": k,
254
  "content": v["content"],
255
  "workspace": self.db.workspace,
256
  }
257
+ await self.db.execute(upsert_sql, _data)
258
  elif self.namespace == "llm_response_cache":
259
+ for mode, items in data.items():
260
  for k, v in items.items():
261
  upsert_sql = SQL_TEMPLATES["upsert_llm_response_cache"]
262
+ _data = {
263
  "workspace": self.db.workspace,
264
  "id": k,
265
  "original_prompt": v["original_prompt"],
266
+ "return_value": v["return"],
267
  "mode": mode,
268
  }
 
269
 
270
+ await self.db.execute(upsert_sql, _data)
271
 
272
  async def index_done_callback(self):
273
  if self.namespace in ["full_docs", "text_chunks"]:
 
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
 
1037
  doc_name VARCHAR(1024),
1038
  content TEXT,
1039
  meta JSONB,
1040
+ create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1041
+ update_time TIMESTAMP,
1042
  CONSTRAINT LIGHTRAG_DOC_FULL_PK PRIMARY KEY (workspace, id)
1043
  )"""
1044
  },
 
1051
  tokens INTEGER,
1052
  content TEXT,
1053
  content_vector VECTOR,
1054
+ create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1055
+ update_time TIMESTAMP,
1056
  CONSTRAINT LIGHTRAG_DOC_CHUNKS_PK PRIMARY KEY (workspace, id)
1057
  )"""
1058
  },
 
1063
  entity_name VARCHAR(255),
1064
  content TEXT,
1065
  content_vector VECTOR,
1066
+ create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1067
+ update_time TIMESTAMP,
1068
  CONSTRAINT LIGHTRAG_VDB_ENTITY_PK PRIMARY KEY (workspace, id)
1069
  )"""
1070
  },
 
1076
  target_id VARCHAR(256),
1077
  content TEXT,
1078
  content_vector VECTOR,
1079
+ create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1080
+ update_time TIMESTAMP,
1081
  CONSTRAINT LIGHTRAG_VDB_RELATION_PK PRIMARY KEY (workspace, id)
1082
  )"""
1083
  },
 
1087
  id varchar(255) NOT NULL,
1088
  mode varchar(32) NOT NULL,
1089
  original_prompt TEXT,
1090
+ return_value TEXT,
1091
+ create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1092
+ update_time TIMESTAMP,
1093
+ CONSTRAINT LIGHTRAG_LLM_CACHE_PK PRIMARY KEY (workspace, mode, id)
1094
  )"""
1095
  },
1096
  "LIGHTRAG_DOC_STATUS": {
 
1118
  chunk_order_index, full_doc_id
1119
  FROM LIGHTRAG_DOC_CHUNKS WHERE workspace=$1 AND id=$2
1120
  """,
1121
+ "get_by_id_llm_response_cache": """SELECT id, original_prompt, COALESCE(return_value, '') as "return", mode
1122
  FROM LIGHTRAG_LLM_CACHE WHERE workspace=$1 AND mode=$2
1123
  """,
1124
+ "get_by_mode_id_llm_response_cache": """SELECT id, original_prompt, COALESCE(return_value, '') as "return", mode
1125
+ FROM LIGHTRAG_LLM_CACHE WHERE workspace=$1 AND mode=$2 AND id=$3
1126
+ """,
1127
  "get_by_ids_full_docs": """SELECT id, COALESCE(content, '') as content
1128
  FROM LIGHTRAG_DOC_FULL WHERE workspace=$1 AND id IN ({ids})
1129
  """,
 
1131
  chunk_order_index, full_doc_id
1132
  FROM LIGHTRAG_DOC_CHUNKS WHERE workspace=$1 AND id IN ({ids})
1133
  """,
1134
+ "get_by_ids_llm_response_cache": """SELECT id, original_prompt, COALESCE(return_value, '') as "return", mode
1135
  FROM LIGHTRAG_LLM_CACHE WHERE workspace=$1 AND mode= IN ({ids})
1136
  """,
1137
  "filter_keys": "SELECT id FROM {table_name} WHERE workspace=$1 AND id IN ({ids})",
1138
  "upsert_doc_full": """INSERT INTO LIGHTRAG_DOC_FULL (id, content, workspace)
1139
  VALUES ($1, $2, $3)
1140
  ON CONFLICT (workspace,id) DO UPDATE
1141
+ SET content = $2, update_time = CURRENT_TIMESTAMP
1142
  """,
1143
+ "upsert_llm_response_cache": """INSERT INTO LIGHTRAG_LLM_CACHE(workspace,id,original_prompt,return_value,mode)
1144
  VALUES ($1, $2, $3, $4, $5)
1145
+ ON CONFLICT (workspace,mode,id) DO UPDATE
1146
  SET original_prompt = EXCLUDED.original_prompt,
1147
+ return_value=EXCLUDED.return_value,
1148
  mode=EXCLUDED.mode,
1149
+ update_time = CURRENT_TIMESTAMP
1150
  """,
1151
  "upsert_chunk": """INSERT INTO LIGHTRAG_DOC_CHUNKS (workspace, id, tokens,
1152
  chunk_order_index, full_doc_id, content, content_vector)
 
1157
  full_doc_id=EXCLUDED.full_doc_id,
1158
  content = EXCLUDED.content,
1159
  content_vector=EXCLUDED.content_vector,
1160
+ update_time = CURRENT_TIMESTAMP
1161
  """,
1162
  "upsert_entity": """INSERT INTO LIGHTRAG_VDB_ENTITY (workspace, id, entity_name, content, content_vector)
1163
  VALUES ($1, $2, $3, $4, $5)
 
1165
  SET entity_name=EXCLUDED.entity_name,
1166
  content=EXCLUDED.content,
1167
  content_vector=EXCLUDED.content_vector,
1168
+ update_time=CURRENT_TIMESTAMP
1169
  """,
1170
  "upsert_relationship": """INSERT INTO LIGHTRAG_VDB_RELATION (workspace, id, source_id,
1171
  target_id, content, content_vector)
 
1174
  SET source_id=EXCLUDED.source_id,
1175
  target_id=EXCLUDED.target_id,
1176
  content=EXCLUDED.content,
1177
+ content_vector=EXCLUDED.content_vector, update_time = CURRENT_TIMESTAMP
1178
  """,
1179
  # SQL for VectorStorage
1180
  "entities": """SELECT entity_name FROM
lightrag/lightrag.py CHANGED
@@ -176,6 +176,8 @@ class LightRAG:
176
  vector_db_storage_cls_kwargs: dict = field(default_factory=dict)
177
 
178
  enable_llm_cache: bool = True
 
 
179
 
180
  # extension
181
  addon_params: dict = field(default_factory=dict)
@@ -402,6 +404,7 @@ class LightRAG:
402
  knowledge_graph_inst=self.chunk_entity_relation_graph,
403
  entity_vdb=self.entities_vdb,
404
  relationships_vdb=self.relationships_vdb,
 
405
  global_config=asdict(self),
406
  )
407
 
 
176
  vector_db_storage_cls_kwargs: dict = field(default_factory=dict)
177
 
178
  enable_llm_cache: bool = True
179
+ # Sometimes there are some reason the LLM failed at Extracting Entities, and we want to continue without LLM cost, we can use this flag
180
+ enable_llm_cache_for_entity_extract: bool = False
181
 
182
  # extension
183
  addon_params: dict = field(default_factory=dict)
 
404
  knowledge_graph_inst=self.chunk_entity_relation_graph,
405
  entity_vdb=self.entities_vdb,
406
  relationships_vdb=self.relationships_vdb,
407
+ llm_response_cache=self.llm_response_cache,
408
  global_config=asdict(self),
409
  )
410
 
lightrag/operate.py CHANGED
@@ -253,9 +253,13 @@ async def extract_entities(
253
  entity_vdb: BaseVectorStorage,
254
  relationships_vdb: BaseVectorStorage,
255
  global_config: dict,
 
256
  ) -> Union[BaseGraphStorage, None]:
257
  use_llm_func: callable = global_config["llm_model_func"]
258
  entity_extract_max_gleaning = global_config["entity_extract_max_gleaning"]
 
 
 
259
 
260
  ordered_chunks = list(chunks.items())
261
  # add language and example number params to prompt
@@ -300,6 +304,52 @@ async def extract_entities(
300
  already_entities = 0
301
  already_relations = 0
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  async def _process_single_content(chunk_key_dp: tuple[str, TextChunkSchema]):
304
  nonlocal already_processed, already_entities, already_relations
305
  chunk_key = chunk_key_dp[0]
@@ -310,17 +360,19 @@ async def extract_entities(
310
  **context_base, input_text="{input_text}"
311
  ).format(**context_base, input_text=content)
312
 
313
- final_result = await use_llm_func(hint_prompt)
314
  history = pack_user_ass_to_openai_messages(hint_prompt, final_result)
315
  for now_glean_index in range(entity_extract_max_gleaning):
316
- glean_result = await use_llm_func(continue_prompt, history_messages=history)
 
 
317
 
318
  history += pack_user_ass_to_openai_messages(continue_prompt, glean_result)
319
  final_result += glean_result
320
  if now_glean_index == entity_extract_max_gleaning - 1:
321
  break
322
 
323
- if_loop_result: str = await use_llm_func(
324
  if_loop_prompt, history_messages=history
325
  )
326
  if_loop_result = if_loop_result.strip().strip('"').strip("'").lower()
 
253
  entity_vdb: BaseVectorStorage,
254
  relationships_vdb: BaseVectorStorage,
255
  global_config: dict,
256
+ llm_response_cache: BaseKVStorage = None,
257
  ) -> Union[BaseGraphStorage, None]:
258
  use_llm_func: callable = global_config["llm_model_func"]
259
  entity_extract_max_gleaning = global_config["entity_extract_max_gleaning"]
260
+ enable_llm_cache_for_entity_extract: bool = global_config[
261
+ "enable_llm_cache_for_entity_extract"
262
+ ]
263
 
264
  ordered_chunks = list(chunks.items())
265
  # add language and example number params to prompt
 
304
  already_entities = 0
305
  already_relations = 0
306
 
307
+ async def _user_llm_func_with_cache(
308
+ input_text: str, history_messages: list[dict[str, str]] = None
309
+ ) -> str:
310
+ if enable_llm_cache_for_entity_extract and llm_response_cache:
311
+ need_to_restore = False
312
+ if (
313
+ global_config["embedding_cache_config"]
314
+ and global_config["embedding_cache_config"]["enabled"]
315
+ ):
316
+ new_config = global_config.copy()
317
+ new_config["embedding_cache_config"] = None
318
+ new_config["enable_llm_cache"] = True
319
+ llm_response_cache.global_config = new_config
320
+ need_to_restore = True
321
+ if history_messages:
322
+ history = json.dumps(history_messages)
323
+ _prompt = history + "\n" + input_text
324
+ else:
325
+ _prompt = input_text
326
+
327
+ arg_hash = compute_args_hash(_prompt)
328
+ cached_return, _1, _2, _3 = await handle_cache(
329
+ llm_response_cache, arg_hash, _prompt, "default"
330
+ )
331
+ if need_to_restore:
332
+ llm_response_cache.global_config = global_config
333
+ if cached_return:
334
+ return cached_return
335
+
336
+ if history_messages:
337
+ res: str = await use_llm_func(
338
+ input_text, history_messages=history_messages
339
+ )
340
+ else:
341
+ res: str = await use_llm_func(input_text)
342
+ await save_to_cache(
343
+ llm_response_cache,
344
+ CacheData(args_hash=arg_hash, content=res, prompt=_prompt),
345
+ )
346
+ return res
347
+
348
+ if history_messages:
349
+ return await use_llm_func(input_text, history_messages=history_messages)
350
+ else:
351
+ return await use_llm_func(input_text)
352
+
353
  async def _process_single_content(chunk_key_dp: tuple[str, TextChunkSchema]):
354
  nonlocal already_processed, already_entities, already_relations
355
  chunk_key = chunk_key_dp[0]
 
360
  **context_base, input_text="{input_text}"
361
  ).format(**context_base, input_text=content)
362
 
363
+ final_result = await _user_llm_func_with_cache(hint_prompt)
364
  history = pack_user_ass_to_openai_messages(hint_prompt, final_result)
365
  for now_glean_index in range(entity_extract_max_gleaning):
366
+ glean_result = await _user_llm_func_with_cache(
367
+ continue_prompt, history_messages=history
368
+ )
369
 
370
  history += pack_user_ass_to_openai_messages(continue_prompt, glean_result)
371
  final_result += glean_result
372
  if now_glean_index == entity_extract_max_gleaning - 1:
373
  break
374
 
375
+ if_loop_result: str = await _user_llm_func_with_cache(
376
  if_loop_prompt, history_messages=history
377
  )
378
  if_loop_result = if_loop_result.strip().strip('"').strip("'").lower()
lightrag/utils.py CHANGED
@@ -454,7 +454,10 @@ async def handle_cache(hashing_kv, args_hash, prompt, mode="default"):
454
 
455
  # For naive mode, only use simple cache matching
456
  if mode == "naive":
457
- mode_cache = await hashing_kv.get_by_id(mode) or {}
 
 
 
458
  if args_hash in mode_cache:
459
  return mode_cache[args_hash]["return"], None, None, None
460
  return None, None, None, None
@@ -488,7 +491,10 @@ async def handle_cache(hashing_kv, args_hash, prompt, mode="default"):
488
  return best_cached_response, None, None, None
489
  else:
490
  # Use regular cache
491
- mode_cache = await hashing_kv.get_by_id(mode) or {}
 
 
 
492
  if args_hash in mode_cache:
493
  return mode_cache[args_hash]["return"], None, None, None
494
 
@@ -510,7 +516,13 @@ async def save_to_cache(hashing_kv, cache_data: CacheData):
510
  if hashing_kv is None or hasattr(cache_data.content, "__aiter__"):
511
  return
512
 
513
- mode_cache = await hashing_kv.get_by_id(cache_data.mode) or {}
 
 
 
 
 
 
514
 
515
  mode_cache[cache_data.args_hash] = {
516
  "return": cache_data.content,
@@ -543,3 +555,15 @@ def safe_unicode_decode(content):
543
  )
544
 
545
  return decoded_content
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
  # For naive mode, only use simple cache matching
456
  if mode == "naive":
457
+ if exists_func(hashing_kv, "get_by_mode_and_id"):
458
+ mode_cache = await hashing_kv.get_by_mode_and_id(mode, args_hash) or {}
459
+ else:
460
+ mode_cache = await hashing_kv.get_by_id(mode) or {}
461
  if args_hash in mode_cache:
462
  return mode_cache[args_hash]["return"], None, None, None
463
  return None, None, None, None
 
491
  return best_cached_response, None, None, None
492
  else:
493
  # Use regular cache
494
+ if exists_func(hashing_kv, "get_by_mode_and_id"):
495
+ mode_cache = await hashing_kv.get_by_mode_and_id(mode, args_hash) or {}
496
+ else:
497
+ mode_cache = await hashing_kv.get_by_id(mode) or {}
498
  if args_hash in mode_cache:
499
  return mode_cache[args_hash]["return"], None, None, None
500
 
 
516
  if hashing_kv is None or hasattr(cache_data.content, "__aiter__"):
517
  return
518
 
519
+ if exists_func(hashing_kv, "get_by_mode_and_id"):
520
+ mode_cache = (
521
+ await hashing_kv.get_by_mode_and_id(cache_data.mode, cache_data.args_hash)
522
+ or {}
523
+ )
524
+ else:
525
+ mode_cache = await hashing_kv.get_by_id(cache_data.mode) or {}
526
 
527
  mode_cache[cache_data.args_hash] = {
528
  "return": cache_data.content,
 
555
  )
556
 
557
  return decoded_content
558
+
559
+
560
+ def exists_func(obj, func_name: str) -> bool:
561
+ """Check if a function exists in an object or not.
562
+ :param obj:
563
+ :param func_name:
564
+ :return: True / False
565
+ """
566
+ if callable(getattr(obj, func_name, None)):
567
+ return True
568
+ else:
569
+ return False