choizhang commited on
Commit
0a4b242
·
2 Parent(s): 11b4cc9 fa65a80

Merge branch 'main' into loginPage

Browse files
Files changed (39) hide show
  1. README.md +2 -0
  2. lightrag/base.py +30 -1
  3. lightrag/kg/chroma_impl.py +64 -0
  4. lightrag/kg/faiss_impl.py +43 -0
  5. lightrag/kg/milvus_impl.py +54 -0
  6. lightrag/kg/mongo_impl.py +53 -0
  7. lightrag/kg/nano_vector_db_impl.py +30 -0
  8. lightrag/kg/neo4j_impl.py +126 -193
  9. lightrag/kg/oracle_impl.py +74 -0
  10. lightrag/kg/postgres_impl.py +140 -21
  11. lightrag/kg/tidb_impl.py +94 -0
  12. lightrag/lightrag.py +51 -117
  13. lightrag/llm/azure_openai.py +2 -0
  14. lightrag/operate.py +189 -40
  15. lightrag/prompt.py +1 -1
  16. lightrag/utils.py +49 -0
  17. lightrag_webui/bun.lock +10 -0
  18. lightrag_webui/package.json +2 -0
  19. lightrag_webui/src/components/ThemeToggle.tsx +4 -2
  20. lightrag_webui/src/components/documents/ClearDocumentsDialog.tsx +10 -8
  21. lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx +12 -10
  22. lightrag_webui/src/components/graph/FullScreenControl.tsx +4 -2
  23. lightrag_webui/src/components/graph/GraphLabels.tsx +6 -4
  24. lightrag_webui/src/components/graph/GraphSearch.tsx +4 -2
  25. lightrag_webui/src/components/graph/LayoutsControl.tsx +6 -3
  26. lightrag_webui/src/components/graph/PropertiesView.tsx +15 -12
  27. lightrag_webui/src/components/graph/Settings.tsx +18 -16
  28. lightrag_webui/src/components/graph/StatusCard.tsx +20 -18
  29. lightrag_webui/src/components/graph/StatusIndicator.tsx +3 -1
  30. lightrag_webui/src/components/graph/ZoomControl.tsx +5 -3
  31. lightrag_webui/src/components/retrieval/ChatMessage.tsx +5 -2
  32. lightrag_webui/src/components/retrieval/QuerySettings.tsx +43 -41
  33. lightrag_webui/src/features/DocumentManager.tsx +24 -22
  34. lightrag_webui/src/features/RetrievalTesting.tsx +8 -6
  35. lightrag_webui/src/features/SiteHeader.tsx +8 -5
  36. lightrag_webui/src/i18n.js +21 -0
  37. lightrag_webui/src/locales/en.json +234 -0
  38. lightrag_webui/src/locales/zh.json +235 -0
  39. lightrag_webui/src/main.tsx +2 -0
README.md CHANGED
@@ -176,6 +176,8 @@ class QueryParam:
176
  """Maximum number of tokens allocated for relationship descriptions in global retrieval."""
177
  max_token_for_local_context: int = 4000
178
  """Maximum number of tokens allocated for entity descriptions in local retrieval."""
 
 
179
  ...
180
  ```
181
 
 
176
  """Maximum number of tokens allocated for relationship descriptions in global retrieval."""
177
  max_token_for_local_context: int = 4000
178
  """Maximum number of tokens allocated for entity descriptions in local retrieval."""
179
+ ids: list[str] | None = None # ONLY SUPPORTED FOR PG VECTOR DBs
180
+ """List of ids to filter the RAG."""
181
  ...
182
  ```
183
 
lightrag/base.py CHANGED
@@ -81,6 +81,9 @@ class QueryParam:
81
  history_turns: int = 3
82
  """Number of complete conversation turns (user-assistant pairs) to consider in the response context."""
83
 
 
 
 
84
 
85
  @dataclass
86
  class StorageNameSpace(ABC):
@@ -107,7 +110,9 @@ class BaseVectorStorage(StorageNameSpace, ABC):
107
  meta_fields: set[str] = field(default_factory=set)
108
 
109
  @abstractmethod
110
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
111
  """Query the vector storage and retrieve top_k results."""
112
 
113
  @abstractmethod
@@ -122,6 +127,30 @@ class BaseVectorStorage(StorageNameSpace, ABC):
122
  async def delete_entity_relation(self, entity_name: str) -> None:
123
  """Delete relations for a given entity."""
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  @dataclass
127
  class BaseKVStorage(StorageNameSpace, ABC):
 
81
  history_turns: int = 3
82
  """Number of complete conversation turns (user-assistant pairs) to consider in the response context."""
83
 
84
+ ids: list[str] | None = None
85
+ """List of ids to filter the results."""
86
+
87
 
88
  @dataclass
89
  class StorageNameSpace(ABC):
 
110
  meta_fields: set[str] = field(default_factory=set)
111
 
112
  @abstractmethod
113
+ async def query(
114
+ self, query: str, top_k: int, ids: list[str] | None = None
115
+ ) -> list[dict[str, Any]]:
116
  """Query the vector storage and retrieve top_k results."""
117
 
118
  @abstractmethod
 
127
  async def delete_entity_relation(self, entity_name: str) -> None:
128
  """Delete relations for a given entity."""
129
 
130
+ @abstractmethod
131
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
132
+ """Get vector data by its ID
133
+
134
+ Args:
135
+ id: The unique identifier of the vector
136
+
137
+ Returns:
138
+ The vector data if found, or None if not found
139
+ """
140
+ pass
141
+
142
+ @abstractmethod
143
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
144
+ """Get multiple vector data by their IDs
145
+
146
+ Args:
147
+ ids: List of unique identifiers
148
+
149
+ Returns:
150
+ List of vector data objects that were found
151
+ """
152
+ pass
153
+
154
 
155
  @dataclass
156
  class BaseKVStorage(StorageNameSpace, ABC):
lightrag/kg/chroma_impl.py CHANGED
@@ -269,3 +269,67 @@ class ChromaVectorDBStorage(BaseVectorStorage):
269
  except Exception as e:
270
  logger.error(f"Error during prefix search in ChromaDB: {str(e)}")
271
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  except Exception as e:
270
  logger.error(f"Error during prefix search in ChromaDB: {str(e)}")
271
  raise
272
+
273
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
274
+ """Get vector data by its ID
275
+
276
+ Args:
277
+ id: The unique identifier of the vector
278
+
279
+ Returns:
280
+ The vector data if found, or None if not found
281
+ """
282
+ try:
283
+ # Query the collection for a single vector by ID
284
+ result = self._collection.get(
285
+ ids=[id], include=["metadatas", "embeddings", "documents"]
286
+ )
287
+
288
+ if not result or not result["ids"] or len(result["ids"]) == 0:
289
+ return None
290
+
291
+ # Format the result to match the expected structure
292
+ return {
293
+ "id": result["ids"][0],
294
+ "vector": result["embeddings"][0],
295
+ "content": result["documents"][0],
296
+ **result["metadatas"][0],
297
+ }
298
+ except Exception as e:
299
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
300
+ return None
301
+
302
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
303
+ """Get multiple vector data by their IDs
304
+
305
+ Args:
306
+ ids: List of unique identifiers
307
+
308
+ Returns:
309
+ List of vector data objects that were found
310
+ """
311
+ if not ids:
312
+ return []
313
+
314
+ try:
315
+ # Query the collection for multiple vectors by IDs
316
+ result = self._collection.get(
317
+ ids=ids, include=["metadatas", "embeddings", "documents"]
318
+ )
319
+
320
+ if not result or not result["ids"] or len(result["ids"]) == 0:
321
+ return []
322
+
323
+ # Format the results to match the expected structure
324
+ return [
325
+ {
326
+ "id": result["ids"][i],
327
+ "vector": result["embeddings"][i],
328
+ "content": result["documents"][i],
329
+ **result["metadatas"][i],
330
+ }
331
+ for i in range(len(result["ids"]))
332
+ ]
333
+ except Exception as e:
334
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
335
+ return []
lightrag/kg/faiss_impl.py CHANGED
@@ -392,3 +392,46 @@ class FaissVectorDBStorage(BaseVectorStorage):
392
 
393
  logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
394
  return matching_records
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
  logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
394
  return matching_records
395
+
396
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
397
+ """Get vector data by its ID
398
+
399
+ Args:
400
+ id: The unique identifier of the vector
401
+
402
+ Returns:
403
+ The vector data if found, or None if not found
404
+ """
405
+ # Find the Faiss internal ID for the custom ID
406
+ fid = self._find_faiss_id_by_custom_id(id)
407
+ if fid is None:
408
+ return None
409
+
410
+ # Get the metadata for the found ID
411
+ metadata = self._id_to_meta.get(fid, {})
412
+ if not metadata:
413
+ return None
414
+
415
+ return {**metadata, "id": metadata.get("__id__")}
416
+
417
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
418
+ """Get multiple vector data by their IDs
419
+
420
+ Args:
421
+ ids: List of unique identifiers
422
+
423
+ Returns:
424
+ List of vector data objects that were found
425
+ """
426
+ if not ids:
427
+ return []
428
+
429
+ results = []
430
+ for id in ids:
431
+ fid = self._find_faiss_id_by_custom_id(id)
432
+ if fid is not None:
433
+ metadata = self._id_to_meta.get(fid, {})
434
+ if metadata:
435
+ results.append({**metadata, "id": metadata.get("__id__")})
436
+
437
+ return results
lightrag/kg/milvus_impl.py CHANGED
@@ -231,3 +231,57 @@ class MilvusVectorDBStorage(BaseVectorStorage):
231
  except Exception as e:
232
  logger.error(f"Error searching for records with prefix '{prefix}': {e}")
233
  return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  except Exception as e:
232
  logger.error(f"Error searching for records with prefix '{prefix}': {e}")
233
  return []
234
+
235
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
236
+ """Get vector data by its ID
237
+
238
+ Args:
239
+ id: The unique identifier of the vector
240
+
241
+ Returns:
242
+ The vector data if found, or None if not found
243
+ """
244
+ try:
245
+ # Query Milvus for a specific ID
246
+ result = self._client.query(
247
+ collection_name=self.namespace,
248
+ filter=f'id == "{id}"',
249
+ output_fields=list(self.meta_fields) + ["id"],
250
+ )
251
+
252
+ if not result or len(result) == 0:
253
+ return None
254
+
255
+ return result[0]
256
+ except Exception as e:
257
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
258
+ return None
259
+
260
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
261
+ """Get multiple vector data by their IDs
262
+
263
+ Args:
264
+ ids: List of unique identifiers
265
+
266
+ Returns:
267
+ List of vector data objects that were found
268
+ """
269
+ if not ids:
270
+ return []
271
+
272
+ try:
273
+ # Prepare the ID filter expression
274
+ id_list = '", "'.join(ids)
275
+ filter_expr = f'id in ["{id_list}"]'
276
+
277
+ # Query Milvus with the filter
278
+ result = self._client.query(
279
+ collection_name=self.namespace,
280
+ filter=filter_expr,
281
+ output_fields=list(self.meta_fields) + ["id"],
282
+ )
283
+
284
+ return result or []
285
+ except Exception as e:
286
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
287
+ return []
lightrag/kg/mongo_impl.py CHANGED
@@ -1071,6 +1071,59 @@ class MongoVectorDBStorage(BaseVectorStorage):
1071
  logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}")
1072
  return []
1073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1074
 
1075
  async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str):
1076
  collection_names = await db.list_collection_names()
 
1071
  logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}")
1072
  return []
1073
 
1074
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
1075
+ """Get vector data by its ID
1076
+
1077
+ Args:
1078
+ id: The unique identifier of the vector
1079
+
1080
+ Returns:
1081
+ The vector data if found, or None if not found
1082
+ """
1083
+ try:
1084
+ # Search for the specific ID in MongoDB
1085
+ result = await self._data.find_one({"_id": id})
1086
+ if result:
1087
+ # Format the result to include id field expected by API
1088
+ result_dict = dict(result)
1089
+ if "_id" in result_dict and "id" not in result_dict:
1090
+ result_dict["id"] = result_dict["_id"]
1091
+ return result_dict
1092
+ return None
1093
+ except Exception as e:
1094
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
1095
+ return None
1096
+
1097
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
1098
+ """Get multiple vector data by their IDs
1099
+
1100
+ Args:
1101
+ ids: List of unique identifiers
1102
+
1103
+ Returns:
1104
+ List of vector data objects that were found
1105
+ """
1106
+ if not ids:
1107
+ return []
1108
+
1109
+ try:
1110
+ # Query MongoDB for multiple IDs
1111
+ cursor = self._data.find({"_id": {"$in": ids}})
1112
+ results = await cursor.to_list(length=None)
1113
+
1114
+ # Format results to include id field expected by API
1115
+ formatted_results = []
1116
+ for result in results:
1117
+ result_dict = dict(result)
1118
+ if "_id" in result_dict and "id" not in result_dict:
1119
+ result_dict["id"] = result_dict["_id"]
1120
+ formatted_results.append(result_dict)
1121
+
1122
+ return formatted_results
1123
+ except Exception as e:
1124
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
1125
+ return []
1126
+
1127
 
1128
  async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str):
1129
  collection_names = await db.list_collection_names()
lightrag/kg/nano_vector_db_impl.py CHANGED
@@ -256,3 +256,33 @@ class NanoVectorDBStorage(BaseVectorStorage):
256
 
257
  logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
258
  return matching_records
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
258
  return matching_records
259
+
260
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
261
+ """Get vector data by its ID
262
+
263
+ Args:
264
+ id: The unique identifier of the vector
265
+
266
+ Returns:
267
+ The vector data if found, or None if not found
268
+ """
269
+ client = await self._get_client()
270
+ result = client.get([id])
271
+ if result:
272
+ return result[0]
273
+ return None
274
+
275
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
276
+ """Get multiple vector data by their IDs
277
+
278
+ Args:
279
+ ids: List of unique identifiers
280
+
281
+ Returns:
282
+ List of vector data objects that were found
283
+ """
284
+ if not ids:
285
+ return []
286
+
287
+ client = await self._get_client()
288
+ return client.get(ids)
lightrag/kg/neo4j_impl.py CHANGED
@@ -176,23 +176,6 @@ class Neo4JStorage(BaseGraphStorage):
176
  # Noe4J handles persistence automatically
177
  pass
178
 
179
- def _ensure_label(self, label: str) -> str:
180
- """Ensure a label is valid
181
-
182
- Args:
183
- label: The label to validate
184
-
185
- Returns:
186
- str: The cleaned label
187
-
188
- Raises:
189
- ValueError: If label is empty after cleaning
190
- """
191
- clean_label = label.strip('"')
192
- if not clean_label:
193
- raise ValueError("Neo4j: Label cannot be empty")
194
- return clean_label
195
-
196
  async def has_node(self, node_id: str) -> bool:
197
  """
198
  Check if a node with the given label exists in the database
@@ -207,20 +190,17 @@ class Neo4JStorage(BaseGraphStorage):
207
  ValueError: If node_id is invalid
208
  Exception: If there is an error executing the query
209
  """
210
- entity_name_label = self._ensure_label(node_id)
211
  async with self._driver.session(
212
  database=self._DATABASE, default_access_mode="READ"
213
  ) as session:
214
  try:
215
- query = f"MATCH (n:`{entity_name_label}`) RETURN count(n) > 0 AS node_exists"
216
- result = await session.run(query)
217
  single_result = await result.single()
218
  await result.consume() # Ensure result is fully consumed
219
  return single_result["node_exists"]
220
  except Exception as e:
221
- logger.error(
222
- f"Error checking node existence for {entity_name_label}: {str(e)}"
223
- )
224
  await result.consume() # Ensure results are consumed even on error
225
  raise
226
 
@@ -239,24 +219,25 @@ class Neo4JStorage(BaseGraphStorage):
239
  ValueError: If either node_id is invalid
240
  Exception: If there is an error executing the query
241
  """
242
- entity_name_label_source = self._ensure_label(source_node_id)
243
- entity_name_label_target = self._ensure_label(target_node_id)
244
-
245
  async with self._driver.session(
246
  database=self._DATABASE, default_access_mode="READ"
247
  ) as session:
248
  try:
249
  query = (
250
- f"MATCH (a:`{entity_name_label_source}`)-[r]-(b:`{entity_name_label_target}`) "
251
  "RETURN COUNT(r) > 0 AS edgeExists"
252
  )
253
- result = await session.run(query)
 
 
 
 
254
  single_result = await result.single()
255
  await result.consume() # Ensure result is fully consumed
256
  return single_result["edgeExists"]
257
  except Exception as e:
258
  logger.error(
259
- f"Error checking edge existence between {entity_name_label_source} and {entity_name_label_target}: {str(e)}"
260
  )
261
  await result.consume() # Ensure results are consumed even on error
262
  raise
@@ -275,13 +256,12 @@ class Neo4JStorage(BaseGraphStorage):
275
  ValueError: If node_id is invalid
276
  Exception: If there is an error executing the query
277
  """
278
- entity_name_label = self._ensure_label(node_id)
279
  async with self._driver.session(
280
  database=self._DATABASE, default_access_mode="READ"
281
  ) as session:
282
  try:
283
- query = f"MATCH (n:`{entity_name_label}` {{entity_id: $entity_id}}) RETURN n"
284
- result = await session.run(query, entity_id=entity_name_label)
285
  try:
286
  records = await result.fetch(
287
  2
@@ -289,20 +269,25 @@ class Neo4JStorage(BaseGraphStorage):
289
 
290
  if len(records) > 1:
291
  logger.warning(
292
- f"Multiple nodes found with label '{entity_name_label}'. Using first node."
293
  )
294
  if records:
295
  node = records[0]["n"]
296
  node_dict = dict(node)
297
- logger.debug(
298
- f"{inspect.currentframe().f_code.co_name}: query: {query}, result: {node_dict}"
299
- )
 
 
 
 
 
300
  return node_dict
301
  return None
302
  finally:
303
  await result.consume() # Ensure result is fully consumed
304
  except Exception as e:
305
- logger.error(f"Error getting node for {entity_name_label}: {str(e)}")
306
  raise
307
 
308
  async def node_degree(self, node_id: str) -> int:
@@ -320,43 +305,32 @@ class Neo4JStorage(BaseGraphStorage):
320
  ValueError: If node_id is invalid
321
  Exception: If there is an error executing the query
322
  """
323
- entity_name_label = self._ensure_label(node_id)
324
-
325
  async with self._driver.session(
326
  database=self._DATABASE, default_access_mode="READ"
327
  ) as session:
328
  try:
329
- query = f"""
330
- MATCH (n:`{entity_name_label}`)
331
  OPTIONAL MATCH (n)-[r]-()
332
- RETURN n, COUNT(r) AS degree
333
  """
334
- result = await session.run(query)
335
  try:
336
- records = await result.fetch(100)
337
 
338
- if not records:
339
- logger.warning(
340
- f"No node found with label '{entity_name_label}'"
341
- )
342
  return 0
343
 
344
- if len(records) > 1:
345
- logger.warning(
346
- f"Multiple nodes ({len(records)}) found with label '{entity_name_label}', using first node's degree"
347
- )
348
-
349
- degree = records[0]["degree"]
350
  logger.debug(
351
- f"{inspect.currentframe().f_code.co_name}:query:{query}:result:{degree}"
352
  )
353
  return degree
354
  finally:
355
  await result.consume() # Ensure result is fully consumed
356
  except Exception as e:
357
- logger.error(
358
- f"Error getting node degree for {entity_name_label}: {str(e)}"
359
- )
360
  raise
361
 
362
  async def edge_degree(self, src_id: str, tgt_id: str) -> int:
@@ -369,11 +343,8 @@ class Neo4JStorage(BaseGraphStorage):
369
  Returns:
370
  int: Sum of the degrees of both nodes
371
  """
372
- entity_name_label_source = self._ensure_label(src_id)
373
- entity_name_label_target = self._ensure_label(tgt_id)
374
-
375
- src_degree = await self.node_degree(entity_name_label_source)
376
- trg_degree = await self.node_degree(entity_name_label_target)
377
 
378
  # Convert None to 0 for addition
379
  src_degree = 0 if src_degree is None else src_degree
@@ -399,24 +370,24 @@ class Neo4JStorage(BaseGraphStorage):
399
  Exception: If there is an error executing the query
400
  """
401
  try:
402
- entity_name_label_source = self._ensure_label(source_node_id)
403
- entity_name_label_target = self._ensure_label(target_node_id)
404
-
405
  async with self._driver.session(
406
  database=self._DATABASE, default_access_mode="READ"
407
  ) as session:
408
- query = f"""
409
- MATCH (start:`{entity_name_label_source}`)-[r]-(end:`{entity_name_label_target}`)
410
  RETURN properties(r) as edge_properties
411
  """
412
-
413
- result = await session.run(query)
 
 
 
414
  try:
415
  records = await result.fetch(2)
416
 
417
  if len(records) > 1:
418
  logger.warning(
419
- f"Multiple edges found between '{entity_name_label_source}' and '{entity_name_label_target}'. Using first edge."
420
  )
421
  if records:
422
  try:
@@ -433,7 +404,7 @@ class Neo4JStorage(BaseGraphStorage):
433
  if key not in edge_result:
434
  edge_result[key] = default_value
435
  logger.warning(
436
- f"Edge between {entity_name_label_source} and {entity_name_label_target} "
437
  f"missing {key}, using default: {default_value}"
438
  )
439
 
@@ -443,8 +414,8 @@ class Neo4JStorage(BaseGraphStorage):
443
  return edge_result
444
  except (KeyError, TypeError, ValueError) as e:
445
  logger.error(
446
- f"Error processing edge properties between {entity_name_label_source} "
447
- f"and {entity_name_label_target}: {str(e)}"
448
  )
449
  # Return default edge properties on error
450
  return {
@@ -455,7 +426,7 @@ class Neo4JStorage(BaseGraphStorage):
455
  }
456
 
457
  logger.debug(
458
- f"{inspect.currentframe().f_code.co_name}: No edge found between {entity_name_label_source} and {entity_name_label_target}"
459
  )
460
  # Return default edge properties when no edge found
461
  return {
@@ -488,29 +459,33 @@ class Neo4JStorage(BaseGraphStorage):
488
  Exception: If there is an error executing the query
489
  """
490
  try:
491
- node_label = self._ensure_label(source_node_id)
492
-
493
- query = f"""MATCH (n:`{node_label}`)
494
- OPTIONAL MATCH (n)-[r]-(connected)
495
- RETURN n, r, connected"""
496
-
497
  async with self._driver.session(
498
  database=self._DATABASE, default_access_mode="READ"
499
  ) as session:
500
  try:
501
- results = await session.run(query)
502
- edges = []
 
 
 
503
 
 
504
  async for record in results:
505
  source_node = record["n"]
506
  connected_node = record["connected"]
507
 
 
 
 
 
508
  source_label = (
509
- list(source_node.labels)[0] if source_node.labels else None
 
 
510
  )
511
  target_label = (
512
- list(connected_node.labels)[0]
513
- if connected_node and connected_node.labels
514
  else None
515
  )
516
 
@@ -520,7 +495,9 @@ class Neo4JStorage(BaseGraphStorage):
520
  await results.consume() # Ensure results are consumed
521
  return edges
522
  except Exception as e:
523
- logger.error(f"Error getting edges for node {node_label}: {str(e)}")
 
 
524
  await results.consume() # Ensure results are consumed even on error
525
  raise
526
  except Exception as e:
@@ -547,8 +524,9 @@ class Neo4JStorage(BaseGraphStorage):
547
  node_id: The unique identifier for the node (used as label)
548
  node_data: Dictionary of node properties
549
  """
550
- label = self._ensure_label(node_id)
551
  properties = node_data
 
 
552
  if "entity_id" not in properties:
553
  raise ValueError("Neo4j: node properties must contain an 'entity_id' field")
554
 
@@ -556,13 +534,17 @@ class Neo4JStorage(BaseGraphStorage):
556
  async with self._driver.session(database=self._DATABASE) as session:
557
 
558
  async def execute_upsert(tx: AsyncManagedTransaction):
559
- query = f"""
560
- MERGE (n:`{label}` {{entity_id: $properties.entity_id}})
 
561
  SET n += $properties
 
562
  """
 
 
563
  result = await tx.run(query, properties=properties)
564
  logger.debug(
565
- f"Upserted node with label '{label}' and properties: {properties}"
566
  )
567
  await result.consume() # Ensure result is fully consumed
568
 
@@ -583,52 +565,6 @@ class Neo4JStorage(BaseGraphStorage):
583
  )
584
  ),
585
  )
586
- async def _get_unique_node_entity_id(self, node_label: str) -> str:
587
- """
588
- Get the entity_id of a node with the given label, ensuring the node is unique.
589
-
590
- Args:
591
- node_label (str): Label of the node to check
592
-
593
- Returns:
594
- str: The entity_id of the unique node
595
-
596
- Raises:
597
- ValueError: If no node with the given label exists or if multiple nodes have the same label
598
- """
599
- async with self._driver.session(
600
- database=self._DATABASE, default_access_mode="READ"
601
- ) as session:
602
- query = f"""
603
- MATCH (n:`{node_label}`)
604
- RETURN n, count(n) as node_count
605
- """
606
- result = await session.run(query)
607
- try:
608
- records = await result.fetch(
609
- 2
610
- ) # We only need to know if there are 0, 1, or >1 nodes
611
-
612
- if not records or records[0]["node_count"] == 0:
613
- raise ValueError(
614
- f"Neo4j: node with label '{node_label}' does not exist"
615
- )
616
-
617
- if records[0]["node_count"] > 1:
618
- raise ValueError(
619
- f"Neo4j: multiple nodes found with label '{node_label}', cannot determine unique node"
620
- )
621
-
622
- node = records[0]["n"]
623
- if "entity_id" not in node:
624
- raise ValueError(
625
- f"Neo4j: node with label '{node_label}' does not have an entity_id property"
626
- )
627
-
628
- return node["entity_id"]
629
- finally:
630
- await result.consume() # Ensure result is fully consumed
631
-
632
  @retry(
633
  stop=stop_after_attempt(3),
634
  wait=wait_exponential(multiplier=1, min=4, max=10),
@@ -657,38 +593,30 @@ class Neo4JStorage(BaseGraphStorage):
657
  Raises:
658
  ValueError: If either source or target node does not exist or is not unique
659
  """
660
- source_label = self._ensure_label(source_node_id)
661
- target_label = self._ensure_label(target_node_id)
662
- edge_properties = edge_data
663
-
664
- # Get entity_ids for source and target nodes, ensuring they are unique
665
- source_entity_id = await self._get_unique_node_entity_id(source_label)
666
- target_entity_id = await self._get_unique_node_entity_id(target_label)
667
-
668
  try:
 
669
  async with self._driver.session(database=self._DATABASE) as session:
670
 
671
  async def execute_upsert(tx: AsyncManagedTransaction):
672
- query = f"""
673
- MATCH (source:`{source_label}` {{entity_id: $source_entity_id}})
674
  WITH source
675
- MATCH (target:`{target_label}` {{entity_id: $target_entity_id}})
676
  MERGE (source)-[r:DIRECTED]-(target)
677
  SET r += $properties
678
  RETURN r, source, target
679
  """
680
  result = await tx.run(
681
  query,
682
- source_entity_id=source_entity_id,
683
- target_entity_id=target_entity_id,
684
  properties=edge_properties,
685
  )
686
  try:
687
- records = await result.fetch(100)
688
  if records:
689
  logger.debug(
690
- f"Upserted edge from '{source_label}' (entity_id: {source_entity_id}) "
691
- f"to '{target_label}' (entity_id: {target_entity_id}) "
692
  f"with properties: {edge_properties}"
693
  )
694
  finally:
@@ -726,7 +654,6 @@ class Neo4JStorage(BaseGraphStorage):
726
  Returns:
727
  KnowledgeGraph: Complete connected subgraph for specified node
728
  """
729
- label = node_label.strip('"')
730
  result = KnowledgeGraph()
731
  seen_nodes = set()
732
  seen_edges = set()
@@ -735,7 +662,7 @@ class Neo4JStorage(BaseGraphStorage):
735
  database=self._DATABASE, default_access_mode="READ"
736
  ) as session:
737
  try:
738
- if label == "*":
739
  main_query = """
740
  MATCH (n)
741
  OPTIONAL MATCH (n)-[r]-()
@@ -760,12 +687,11 @@ class Neo4JStorage(BaseGraphStorage):
760
  # Main query uses partial matching
761
  main_query = """
762
  MATCH (start)
763
- WHERE any(label IN labels(start) WHERE
764
  CASE
765
- WHEN $inclusive THEN label CONTAINS $label
766
- ELSE label = $label
767
  END
768
- )
769
  WITH start
770
  CALL apoc.path.subgraphAll(start, {
771
  relationshipFilter: '',
@@ -799,7 +725,7 @@ class Neo4JStorage(BaseGraphStorage):
799
  main_query,
800
  {
801
  "max_nodes": MAX_GRAPH_NODES,
802
- "label": label,
803
  "inclusive": inclusive,
804
  "max_depth": max_depth,
805
  "min_degree": min_degree,
@@ -818,7 +744,11 @@ class Neo4JStorage(BaseGraphStorage):
818
  result.nodes.append(
819
  KnowledgeGraphNode(
820
  id=f"{node_id}",
821
- labels=list(node.labels),
 
 
 
 
822
  properties=dict(node),
823
  )
824
  )
@@ -849,7 +779,7 @@ class Neo4JStorage(BaseGraphStorage):
849
 
850
  except neo4jExceptions.ClientError as e:
851
  logger.warning(f"APOC plugin error: {str(e)}")
852
- if label != "*":
853
  logger.warning(
854
  "Neo4j: falling back to basic Cypher recursive search..."
855
  )
@@ -857,12 +787,14 @@ class Neo4JStorage(BaseGraphStorage):
857
  logger.warning(
858
  "Neo4j: inclusive search mode is not supported in recursive query, using exact matching"
859
  )
860
- return await self._robust_fallback(label, max_depth, min_degree)
 
 
861
 
862
  return result
863
 
864
  async def _robust_fallback(
865
- self, label: str, max_depth: int, min_degree: int = 0
866
  ) -> KnowledgeGraph:
867
  """
868
  Fallback implementation when APOC plugin is not available or incompatible.
@@ -895,12 +827,11 @@ class Neo4JStorage(BaseGraphStorage):
895
  database=self._DATABASE, default_access_mode="READ"
896
  ) as session:
897
  query = """
898
- MATCH (a)-[r]-(b)
899
- WHERE id(a) = toInteger($node_id)
900
  WITH r, b, id(r) as edge_id, id(b) as target_id
901
  RETURN r, b, edge_id, target_id
902
  """
903
- results = await session.run(query, {"node_id": node.id})
904
 
905
  # Get all records and release database connection
906
  records = await results.fetch(
@@ -928,14 +859,16 @@ class Neo4JStorage(BaseGraphStorage):
928
  edge_id = str(record["edge_id"])
929
  if edge_id not in visited_edges:
930
  b_node = record["b"]
931
- target_id = str(record["target_id"])
932
 
933
- if b_node.labels: # Only process if target node has labels
934
  # Create KnowledgeGraphNode for target
935
  target_node = KnowledgeGraphNode(
936
  id=f"{target_id}",
937
- labels=list(b_node.labels),
938
- properties=dict(b_node),
 
 
939
  )
940
 
941
  # Create KnowledgeGraphEdge
@@ -961,11 +894,11 @@ class Neo4JStorage(BaseGraphStorage):
961
  async with self._driver.session(
962
  database=self._DATABASE, default_access_mode="READ"
963
  ) as session:
964
- query = f"""
965
- MATCH (n:`{label}`)
966
  RETURN id(n) as node_id, n
967
  """
968
- node_result = await session.run(query)
969
  try:
970
  node_record = await node_result.single()
971
  if not node_record:
@@ -973,9 +906,11 @@ class Neo4JStorage(BaseGraphStorage):
973
 
974
  # Create initial KnowledgeGraphNode
975
  start_node = KnowledgeGraphNode(
976
- id=f"{node_record['node_id']}",
977
- labels=list(node_record["n"].labels),
978
- properties=dict(node_record["n"]),
 
 
979
  )
980
  finally:
981
  await node_result.consume() # Ensure results are consumed
@@ -999,11 +934,10 @@ class Neo4JStorage(BaseGraphStorage):
999
 
1000
  # Method 2: Query compatible with older versions
1001
  query = """
1002
- MATCH (n)
1003
- WITH DISTINCT labels(n) AS node_labels
1004
- UNWIND node_labels AS label
1005
- RETURN DISTINCT label
1006
- ORDER BY label
1007
  """
1008
  result = await session.run(query)
1009
  labels = []
@@ -1034,15 +968,14 @@ class Neo4JStorage(BaseGraphStorage):
1034
  Args:
1035
  node_id: The label of the node to delete
1036
  """
1037
- label = self._ensure_label(node_id)
1038
 
1039
  async def _do_delete(tx: AsyncManagedTransaction):
1040
- query = f"""
1041
- MATCH (n:`{label}`)
1042
  DETACH DELETE n
1043
  """
1044
- result = await tx.run(query)
1045
- logger.debug(f"Deleted node with label '{label}'")
1046
  await result.consume() # Ensure result is fully consumed
1047
 
1048
  try:
@@ -1092,16 +1025,16 @@ class Neo4JStorage(BaseGraphStorage):
1092
  edges: List of edges to be deleted, each edge is a (source, target) tuple
1093
  """
1094
  for source, target in edges:
1095
- source_label = self._ensure_label(source)
1096
- target_label = self._ensure_label(target)
1097
 
1098
  async def _do_delete_edge(tx: AsyncManagedTransaction):
1099
- query = f"""
1100
- MATCH (source:`{source_label}`)-[r]-(target:`{target_label}`)
1101
  DELETE r
1102
  """
1103
- result = await tx.run(query)
1104
- logger.debug(f"Deleted edge from '{source_label}' to '{target_label}'")
 
 
1105
  await result.consume() # Ensure result is fully consumed
1106
 
1107
  try:
 
176
  # Noe4J handles persistence automatically
177
  pass
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  async def has_node(self, node_id: str) -> bool:
180
  """
181
  Check if a node with the given label exists in the database
 
190
  ValueError: If node_id is invalid
191
  Exception: If there is an error executing the query
192
  """
 
193
  async with self._driver.session(
194
  database=self._DATABASE, default_access_mode="READ"
195
  ) as session:
196
  try:
197
+ query = "MATCH (n:base {entity_id: $entity_id}) RETURN count(n) > 0 AS node_exists"
198
+ result = await session.run(query, entity_id=node_id)
199
  single_result = await result.single()
200
  await result.consume() # Ensure result is fully consumed
201
  return single_result["node_exists"]
202
  except Exception as e:
203
+ logger.error(f"Error checking node existence for {node_id}: {str(e)}")
 
 
204
  await result.consume() # Ensure results are consumed even on error
205
  raise
206
 
 
219
  ValueError: If either node_id is invalid
220
  Exception: If there is an error executing the query
221
  """
 
 
 
222
  async with self._driver.session(
223
  database=self._DATABASE, default_access_mode="READ"
224
  ) as session:
225
  try:
226
  query = (
227
+ "MATCH (a:base {entity_id: $source_entity_id})-[r]-(b:base {entity_id: $target_entity_id}) "
228
  "RETURN COUNT(r) > 0 AS edgeExists"
229
  )
230
+ result = await session.run(
231
+ query,
232
+ source_entity_id=source_node_id,
233
+ target_entity_id=target_node_id,
234
+ )
235
  single_result = await result.single()
236
  await result.consume() # Ensure result is fully consumed
237
  return single_result["edgeExists"]
238
  except Exception as e:
239
  logger.error(
240
+ f"Error checking edge existence between {source_node_id} and {target_node_id}: {str(e)}"
241
  )
242
  await result.consume() # Ensure results are consumed even on error
243
  raise
 
256
  ValueError: If node_id is invalid
257
  Exception: If there is an error executing the query
258
  """
 
259
  async with self._driver.session(
260
  database=self._DATABASE, default_access_mode="READ"
261
  ) as session:
262
  try:
263
+ query = "MATCH (n:base {entity_id: $entity_id}) RETURN n"
264
+ result = await session.run(query, entity_id=node_id)
265
  try:
266
  records = await result.fetch(
267
  2
 
269
 
270
  if len(records) > 1:
271
  logger.warning(
272
+ f"Multiple nodes found with label '{node_id}'. Using first node."
273
  )
274
  if records:
275
  node = records[0]["n"]
276
  node_dict = dict(node)
277
+ # Remove base label from labels list if it exists
278
+ if "labels" in node_dict:
279
+ node_dict["labels"] = [
280
+ label
281
+ for label in node_dict["labels"]
282
+ if label != "base"
283
+ ]
284
+ logger.debug(f"Neo4j query node {query} return: {node_dict}")
285
  return node_dict
286
  return None
287
  finally:
288
  await result.consume() # Ensure result is fully consumed
289
  except Exception as e:
290
+ logger.error(f"Error getting node for {node_id}: {str(e)}")
291
  raise
292
 
293
  async def node_degree(self, node_id: str) -> int:
 
305
  ValueError: If node_id is invalid
306
  Exception: If there is an error executing the query
307
  """
 
 
308
  async with self._driver.session(
309
  database=self._DATABASE, default_access_mode="READ"
310
  ) as session:
311
  try:
312
+ query = """
313
+ MATCH (n:base {entity_id: $entity_id})
314
  OPTIONAL MATCH (n)-[r]-()
315
+ RETURN COUNT(r) AS degree
316
  """
317
+ result = await session.run(query, entity_id=node_id)
318
  try:
319
+ record = await result.single()
320
 
321
+ if not record:
322
+ logger.warning(f"No node found with label '{node_id}'")
 
 
323
  return 0
324
 
325
+ degree = record["degree"]
 
 
 
 
 
326
  logger.debug(
327
+ "Neo4j query node degree for {node_id} return: {degree}"
328
  )
329
  return degree
330
  finally:
331
  await result.consume() # Ensure result is fully consumed
332
  except Exception as e:
333
+ logger.error(f"Error getting node degree for {node_id}: {str(e)}")
 
 
334
  raise
335
 
336
  async def edge_degree(self, src_id: str, tgt_id: str) -> int:
 
343
  Returns:
344
  int: Sum of the degrees of both nodes
345
  """
346
+ src_degree = await self.node_degree(src_id)
347
+ trg_degree = await self.node_degree(tgt_id)
 
 
 
348
 
349
  # Convert None to 0 for addition
350
  src_degree = 0 if src_degree is None else src_degree
 
370
  Exception: If there is an error executing the query
371
  """
372
  try:
 
 
 
373
  async with self._driver.session(
374
  database=self._DATABASE, default_access_mode="READ"
375
  ) as session:
376
+ query = """
377
+ MATCH (start:base {entity_id: $source_entity_id})-[r]-(end:base {entity_id: $target_entity_id})
378
  RETURN properties(r) as edge_properties
379
  """
380
+ result = await session.run(
381
+ query,
382
+ source_entity_id=source_node_id,
383
+ target_entity_id=target_node_id,
384
+ )
385
  try:
386
  records = await result.fetch(2)
387
 
388
  if len(records) > 1:
389
  logger.warning(
390
+ f"Multiple edges found between '{source_node_id}' and '{target_node_id}'. Using first edge."
391
  )
392
  if records:
393
  try:
 
404
  if key not in edge_result:
405
  edge_result[key] = default_value
406
  logger.warning(
407
+ f"Edge between {source_node_id} and {target_node_id} "
408
  f"missing {key}, using default: {default_value}"
409
  )
410
 
 
414
  return edge_result
415
  except (KeyError, TypeError, ValueError) as e:
416
  logger.error(
417
+ f"Error processing edge properties between {source_node_id} "
418
+ f"and {target_node_id}: {str(e)}"
419
  )
420
  # Return default edge properties on error
421
  return {
 
426
  }
427
 
428
  logger.debug(
429
+ f"{inspect.currentframe().f_code.co_name}: No edge found between {source_node_id} and {target_node_id}"
430
  )
431
  # Return default edge properties when no edge found
432
  return {
 
459
  Exception: If there is an error executing the query
460
  """
461
  try:
 
 
 
 
 
 
462
  async with self._driver.session(
463
  database=self._DATABASE, default_access_mode="READ"
464
  ) as session:
465
  try:
466
+ query = """MATCH (n:base {entity_id: $entity_id})
467
+ OPTIONAL MATCH (n)-[r]-(connected:base)
468
+ WHERE connected.entity_id IS NOT NULL
469
+ RETURN n, r, connected"""
470
+ results = await session.run(query, entity_id=source_node_id)
471
 
472
+ edges = []
473
  async for record in results:
474
  source_node = record["n"]
475
  connected_node = record["connected"]
476
 
477
+ # Skip if either node is None
478
+ if not source_node or not connected_node:
479
+ continue
480
+
481
  source_label = (
482
+ source_node.get("entity_id")
483
+ if source_node.get("entity_id")
484
+ else None
485
  )
486
  target_label = (
487
+ connected_node.get("entity_id")
488
+ if connected_node.get("entity_id")
489
  else None
490
  )
491
 
 
495
  await results.consume() # Ensure results are consumed
496
  return edges
497
  except Exception as e:
498
+ logger.error(
499
+ f"Error getting edges for node {source_node_id}: {str(e)}"
500
+ )
501
  await results.consume() # Ensure results are consumed even on error
502
  raise
503
  except Exception as e:
 
524
  node_id: The unique identifier for the node (used as label)
525
  node_data: Dictionary of node properties
526
  """
 
527
  properties = node_data
528
+ entity_type = properties["entity_type"]
529
+ entity_id = properties["entity_id"]
530
  if "entity_id" not in properties:
531
  raise ValueError("Neo4j: node properties must contain an 'entity_id' field")
532
 
 
534
  async with self._driver.session(database=self._DATABASE) as session:
535
 
536
  async def execute_upsert(tx: AsyncManagedTransaction):
537
+ query = (
538
+ """
539
+ MERGE (n:base {entity_id: $properties.entity_id})
540
  SET n += $properties
541
+ SET n:`%s`
542
  """
543
+ % entity_type
544
+ )
545
  result = await tx.run(query, properties=properties)
546
  logger.debug(
547
+ f"Upserted node with entity_id '{entity_id}' and properties: {properties}"
548
  )
549
  await result.consume() # Ensure result is fully consumed
550
 
 
565
  )
566
  ),
567
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  @retry(
569
  stop=stop_after_attempt(3),
570
  wait=wait_exponential(multiplier=1, min=4, max=10),
 
593
  Raises:
594
  ValueError: If either source or target node does not exist or is not unique
595
  """
 
 
 
 
 
 
 
 
596
  try:
597
+ edge_properties = edge_data
598
  async with self._driver.session(database=self._DATABASE) as session:
599
 
600
  async def execute_upsert(tx: AsyncManagedTransaction):
601
+ query = """
602
+ MATCH (source:base {entity_id: $source_entity_id})
603
  WITH source
604
+ MATCH (target:base {entity_id: $target_entity_id})
605
  MERGE (source)-[r:DIRECTED]-(target)
606
  SET r += $properties
607
  RETURN r, source, target
608
  """
609
  result = await tx.run(
610
  query,
611
+ source_entity_id=source_node_id,
612
+ target_entity_id=target_node_id,
613
  properties=edge_properties,
614
  )
615
  try:
616
+ records = await result.fetch(2)
617
  if records:
618
  logger.debug(
619
+ f"Upserted edge from '{source_node_id}' to '{target_node_id}'"
 
620
  f"with properties: {edge_properties}"
621
  )
622
  finally:
 
654
  Returns:
655
  KnowledgeGraph: Complete connected subgraph for specified node
656
  """
 
657
  result = KnowledgeGraph()
658
  seen_nodes = set()
659
  seen_edges = set()
 
662
  database=self._DATABASE, default_access_mode="READ"
663
  ) as session:
664
  try:
665
+ if node_label == "*":
666
  main_query = """
667
  MATCH (n)
668
  OPTIONAL MATCH (n)-[r]-()
 
687
  # Main query uses partial matching
688
  main_query = """
689
  MATCH (start)
690
+ WHERE
691
  CASE
692
+ WHEN $inclusive THEN start.entity_id CONTAINS $entity_id
693
+ ELSE start.entity_id = $entity_id
694
  END
 
695
  WITH start
696
  CALL apoc.path.subgraphAll(start, {
697
  relationshipFilter: '',
 
725
  main_query,
726
  {
727
  "max_nodes": MAX_GRAPH_NODES,
728
+ "entity_id": node_label,
729
  "inclusive": inclusive,
730
  "max_depth": max_depth,
731
  "min_degree": min_degree,
 
744
  result.nodes.append(
745
  KnowledgeGraphNode(
746
  id=f"{node_id}",
747
+ labels=[
748
+ label
749
+ for label in node.labels
750
+ if label != "base"
751
+ ],
752
  properties=dict(node),
753
  )
754
  )
 
779
 
780
  except neo4jExceptions.ClientError as e:
781
  logger.warning(f"APOC plugin error: {str(e)}")
782
+ if node_label != "*":
783
  logger.warning(
784
  "Neo4j: falling back to basic Cypher recursive search..."
785
  )
 
787
  logger.warning(
788
  "Neo4j: inclusive search mode is not supported in recursive query, using exact matching"
789
  )
790
+ return await self._robust_fallback(
791
+ node_label, max_depth, min_degree
792
+ )
793
 
794
  return result
795
 
796
  async def _robust_fallback(
797
+ self, node_label: str, max_depth: int, min_degree: int = 0
798
  ) -> KnowledgeGraph:
799
  """
800
  Fallback implementation when APOC plugin is not available or incompatible.
 
827
  database=self._DATABASE, default_access_mode="READ"
828
  ) as session:
829
  query = """
830
+ MATCH (a:base {entity_id: $entity_id})-[r]-(b)
 
831
  WITH r, b, id(r) as edge_id, id(b) as target_id
832
  RETURN r, b, edge_id, target_id
833
  """
834
+ results = await session.run(query, entity_id=node.id)
835
 
836
  # Get all records and release database connection
837
  records = await results.fetch(
 
859
  edge_id = str(record["edge_id"])
860
  if edge_id not in visited_edges:
861
  b_node = record["b"]
862
+ target_id = b_node.get("entity_id")
863
 
864
+ if target_id: # Only process if target node has entity_id
865
  # Create KnowledgeGraphNode for target
866
  target_node = KnowledgeGraphNode(
867
  id=f"{target_id}",
868
+ labels=[
869
+ label for label in b_node.labels if label != "base"
870
+ ],
871
+ properties=dict(b_node.properties),
872
  )
873
 
874
  # Create KnowledgeGraphEdge
 
894
  async with self._driver.session(
895
  database=self._DATABASE, default_access_mode="READ"
896
  ) as session:
897
+ query = """
898
+ MATCH (n:base {entity_id: $entity_id})
899
  RETURN id(n) as node_id, n
900
  """
901
+ node_result = await session.run(query, entity_id=node_label)
902
  try:
903
  node_record = await node_result.single()
904
  if not node_record:
 
906
 
907
  # Create initial KnowledgeGraphNode
908
  start_node = KnowledgeGraphNode(
909
+ id=f"{node_record['n'].get('entity_id')}",
910
+ labels=[
911
+ label for label in node_record["n"].labels if label != "base"
912
+ ],
913
+ properties=dict(node_record["n"].properties),
914
  )
915
  finally:
916
  await node_result.consume() # Ensure results are consumed
 
934
 
935
  # Method 2: Query compatible with older versions
936
  query = """
937
+ MATCH (n)
938
+ WHERE n.entity_id IS NOT NULL
939
+ RETURN DISTINCT n.entity_id AS label
940
+ ORDER BY label
 
941
  """
942
  result = await session.run(query)
943
  labels = []
 
968
  Args:
969
  node_id: The label of the node to delete
970
  """
 
971
 
972
  async def _do_delete(tx: AsyncManagedTransaction):
973
+ query = """
974
+ MATCH (n:base {entity_id: $entity_id})
975
  DETACH DELETE n
976
  """
977
+ result = await tx.run(query, entity_id=node_id)
978
+ logger.debug(f"Deleted node with label '{node_id}'")
979
  await result.consume() # Ensure result is fully consumed
980
 
981
  try:
 
1025
  edges: List of edges to be deleted, each edge is a (source, target) tuple
1026
  """
1027
  for source, target in edges:
 
 
1028
 
1029
  async def _do_delete_edge(tx: AsyncManagedTransaction):
1030
+ query = """
1031
+ MATCH (source:base {entity_id: $source_entity_id})-[r]-(target:base {entity_id: $target_entity_id})
1032
  DELETE r
1033
  """
1034
+ result = await tx.run(
1035
+ query, source_entity_id=source, target_entity_id=target
1036
+ )
1037
+ logger.debug(f"Deleted edge from '{source}' to '{target}'")
1038
  await result.consume() # Ensure result is fully consumed
1039
 
1040
  try:
lightrag/kg/oracle_impl.py CHANGED
@@ -529,6 +529,80 @@ class OracleVectorDBStorage(BaseVectorStorage):
529
  logger.error(f"Error searching records with prefix '{prefix}': {e}")
530
  return []
531
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
  @final
534
  @dataclass
 
529
  logger.error(f"Error searching records with prefix '{prefix}': {e}")
530
  return []
531
 
532
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
533
+ """Get vector data by its ID
534
+
535
+ Args:
536
+ id: The unique identifier of the vector
537
+
538
+ Returns:
539
+ The vector data if found, or None if not found
540
+ """
541
+ try:
542
+ # Determine the table name based on namespace
543
+ table_name = namespace_to_table_name(self.namespace)
544
+ if not table_name:
545
+ logger.error(f"Unknown namespace for ID lookup: {self.namespace}")
546
+ return None
547
+
548
+ # Create the appropriate ID field name based on namespace
549
+ id_field = "entity_id" if "NODES" in table_name else "relation_id"
550
+ if "CHUNKS" in table_name:
551
+ id_field = "chunk_id"
552
+
553
+ # Prepare and execute the query
554
+ query = f"""
555
+ SELECT * FROM {table_name}
556
+ WHERE {id_field} = :id AND workspace = :workspace
557
+ """
558
+ params = {"id": id, "workspace": self.db.workspace}
559
+
560
+ result = await self.db.query(query, params)
561
+ return result
562
+ except Exception as e:
563
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
564
+ return None
565
+
566
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
567
+ """Get multiple vector data by their IDs
568
+
569
+ Args:
570
+ ids: List of unique identifiers
571
+
572
+ Returns:
573
+ List of vector data objects that were found
574
+ """
575
+ if not ids:
576
+ return []
577
+
578
+ try:
579
+ # Determine the table name based on namespace
580
+ table_name = namespace_to_table_name(self.namespace)
581
+ if not table_name:
582
+ logger.error(f"Unknown namespace for IDs lookup: {self.namespace}")
583
+ return []
584
+
585
+ # Create the appropriate ID field name based on namespace
586
+ id_field = "entity_id" if "NODES" in table_name else "relation_id"
587
+ if "CHUNKS" in table_name:
588
+ id_field = "chunk_id"
589
+
590
+ # Format the list of IDs for SQL IN clause
591
+ ids_list = ", ".join([f"'{id}'" for id in ids])
592
+
593
+ # Prepare and execute the query
594
+ query = f"""
595
+ SELECT * FROM {table_name}
596
+ WHERE {id_field} IN ({ids_list}) AND workspace = :workspace
597
+ """
598
+ params = {"workspace": self.db.workspace}
599
+
600
+ results = await self.db.query(query, params, multirows=True)
601
+ return results or []
602
+ except Exception as e:
603
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
604
+ return []
605
+
606
 
607
  @final
608
  @dataclass
lightrag/kg/postgres_impl.py CHANGED
@@ -438,6 +438,8 @@ class PGVectorStorage(BaseVectorStorage):
438
  "entity_name": item["entity_name"],
439
  "content": item["content"],
440
  "content_vector": json.dumps(item["__vector__"].tolist()),
 
 
441
  }
442
  return upsert_sql, data
443
 
@@ -450,6 +452,8 @@ class PGVectorStorage(BaseVectorStorage):
450
  "target_id": item["tgt_id"],
451
  "content": item["content"],
452
  "content_vector": json.dumps(item["__vector__"].tolist()),
 
 
453
  }
454
  return upsert_sql, data
455
 
@@ -492,13 +496,20 @@ class PGVectorStorage(BaseVectorStorage):
492
  await self.db.execute(upsert_sql, data)
493
 
494
  #################### query method ###############
495
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
496
  embeddings = await self.embedding_func([query])
497
  embedding = embeddings[0]
498
  embedding_string = ",".join(map(str, embedding))
499
 
 
 
 
 
 
500
  sql = SQL_TEMPLATES[self.base_namespace].format(
501
- embedding_string=embedding_string
502
  )
503
  params = {
504
  "workspace": self.db.workspace,
@@ -610,6 +621,60 @@ class PGVectorStorage(BaseVectorStorage):
610
  logger.error(f"Error during prefix search for '{prefix}': {e}")
611
  return []
612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
 
614
  @final
615
  @dataclass
@@ -1491,6 +1556,7 @@ TABLES = {
1491
  content_vector VECTOR,
1492
  create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1493
  update_time TIMESTAMP,
 
1494
  CONSTRAINT LIGHTRAG_VDB_ENTITY_PK PRIMARY KEY (workspace, id)
1495
  )"""
1496
  },
@@ -1504,6 +1570,7 @@ TABLES = {
1504
  content_vector VECTOR,
1505
  create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1506
  update_time TIMESTAMP,
 
1507
  CONSTRAINT LIGHTRAG_VDB_RELATION_PK PRIMARY KEY (workspace, id)
1508
  )"""
1509
  },
@@ -1586,8 +1653,9 @@ SQL_TEMPLATES = {
1586
  content_vector=EXCLUDED.content_vector,
1587
  update_time = CURRENT_TIMESTAMP
1588
  """,
1589
- "upsert_entity": """INSERT INTO LIGHTRAG_VDB_ENTITY (workspace, id, entity_name, content, content_vector)
1590
- VALUES ($1, $2, $3, $4, $5)
 
1591
  ON CONFLICT (workspace,id) DO UPDATE
1592
  SET entity_name=EXCLUDED.entity_name,
1593
  content=EXCLUDED.content,
@@ -1595,8 +1663,8 @@ SQL_TEMPLATES = {
1595
  update_time=CURRENT_TIMESTAMP
1596
  """,
1597
  "upsert_relationship": """INSERT INTO LIGHTRAG_VDB_RELATION (workspace, id, source_id,
1598
- target_id, content, content_vector)
1599
- VALUES ($1, $2, $3, $4, $5, $6)
1600
  ON CONFLICT (workspace,id) DO UPDATE
1601
  SET source_id=EXCLUDED.source_id,
1602
  target_id=EXCLUDED.target_id,
@@ -1604,21 +1672,21 @@ SQL_TEMPLATES = {
1604
  content_vector=EXCLUDED.content_vector, update_time = CURRENT_TIMESTAMP
1605
  """,
1606
  # SQL for VectorStorage
1607
- "entities": """SELECT entity_name FROM
1608
- (SELECT id, entity_name, 1 - (content_vector <=> '[{embedding_string}]'::vector) as distance
1609
- FROM LIGHTRAG_VDB_ENTITY where workspace=$1)
1610
- WHERE distance>$2 ORDER BY distance DESC LIMIT $3
1611
- """,
1612
- "relationships": """SELECT source_id as src_id, target_id as tgt_id FROM
1613
- (SELECT id, source_id,target_id, 1 - (content_vector <=> '[{embedding_string}]'::vector) as distance
1614
- FROM LIGHTRAG_VDB_RELATION where workspace=$1)
1615
- WHERE distance>$2 ORDER BY distance DESC LIMIT $3
1616
- """,
1617
- "chunks": """SELECT id FROM
1618
- (SELECT id, 1 - (content_vector <=> '[{embedding_string}]'::vector) as distance
1619
- FROM LIGHTRAG_DOC_CHUNKS where workspace=$1)
1620
- WHERE distance>$2 ORDER BY distance DESC LIMIT $3
1621
- """,
1622
  # DROP tables
1623
  "drop_all": """
1624
  DROP TABLE IF EXISTS LIGHTRAG_DOC_FULL CASCADE;
@@ -1642,4 +1710,55 @@ SQL_TEMPLATES = {
1642
  "drop_vdb_relation": """
1643
  DROP TABLE IF EXISTS LIGHTRAG_VDB_RELATION CASCADE;
1644
  """,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1645
  }
 
438
  "entity_name": item["entity_name"],
439
  "content": item["content"],
440
  "content_vector": json.dumps(item["__vector__"].tolist()),
441
+ "chunk_id": item["source_id"],
442
+ # TODO: add document_id
443
  }
444
  return upsert_sql, data
445
 
 
452
  "target_id": item["tgt_id"],
453
  "content": item["content"],
454
  "content_vector": json.dumps(item["__vector__"].tolist()),
455
+ "chunk_id": item["source_id"],
456
+ # TODO: add document_id
457
  }
458
  return upsert_sql, data
459
 
 
496
  await self.db.execute(upsert_sql, data)
497
 
498
  #################### query method ###############
499
+ async def query(
500
+ self, query: str, top_k: int, ids: list[str] | None = None
501
+ ) -> list[dict[str, Any]]:
502
  embeddings = await self.embedding_func([query])
503
  embedding = embeddings[0]
504
  embedding_string = ",".join(map(str, embedding))
505
 
506
+ if ids:
507
+ formatted_ids = ",".join(f"'{id}'" for id in ids)
508
+ else:
509
+ formatted_ids = "NULL"
510
+
511
  sql = SQL_TEMPLATES[self.base_namespace].format(
512
+ embedding_string=embedding_string, doc_ids=formatted_ids
513
  )
514
  params = {
515
  "workspace": self.db.workspace,
 
621
  logger.error(f"Error during prefix search for '{prefix}': {e}")
622
  return []
623
 
624
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
625
+ """Get vector data by its ID
626
+
627
+ Args:
628
+ id: The unique identifier of the vector
629
+
630
+ Returns:
631
+ The vector data if found, or None if not found
632
+ """
633
+ table_name = namespace_to_table_name(self.namespace)
634
+ if not table_name:
635
+ logger.error(f"Unknown namespace for ID lookup: {self.namespace}")
636
+ return None
637
+
638
+ query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id=$2"
639
+ params = {"workspace": self.db.workspace, "id": id}
640
+
641
+ try:
642
+ result = await self.db.query(query, params)
643
+ if result:
644
+ return dict(result)
645
+ return None
646
+ except Exception as e:
647
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
648
+ return None
649
+
650
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
651
+ """Get multiple vector data by their IDs
652
+
653
+ Args:
654
+ ids: List of unique identifiers
655
+
656
+ Returns:
657
+ List of vector data objects that were found
658
+ """
659
+ if not ids:
660
+ return []
661
+
662
+ table_name = namespace_to_table_name(self.namespace)
663
+ if not table_name:
664
+ logger.error(f"Unknown namespace for IDs lookup: {self.namespace}")
665
+ return []
666
+
667
+ ids_str = ",".join([f"'{id}'" for id in ids])
668
+ query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id IN ({ids_str})"
669
+ params = {"workspace": self.db.workspace}
670
+
671
+ try:
672
+ results = await self.db.query(query, params, multirows=True)
673
+ return [dict(record) for record in results]
674
+ except Exception as e:
675
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
676
+ return []
677
+
678
 
679
  @final
680
  @dataclass
 
1556
  content_vector VECTOR,
1557
  create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1558
  update_time TIMESTAMP,
1559
+ chunk_id VARCHAR(255) NULL,
1560
  CONSTRAINT LIGHTRAG_VDB_ENTITY_PK PRIMARY KEY (workspace, id)
1561
  )"""
1562
  },
 
1570
  content_vector VECTOR,
1571
  create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1572
  update_time TIMESTAMP,
1573
+ chunk_id VARCHAR(255) NULL,
1574
  CONSTRAINT LIGHTRAG_VDB_RELATION_PK PRIMARY KEY (workspace, id)
1575
  )"""
1576
  },
 
1653
  content_vector=EXCLUDED.content_vector,
1654
  update_time = CURRENT_TIMESTAMP
1655
  """,
1656
+ "upsert_entity": """INSERT INTO LIGHTRAG_VDB_ENTITY (workspace, id, entity_name, content,
1657
+ content_vector, chunk_id)
1658
+ VALUES ($1, $2, $3, $4, $5, $6)
1659
  ON CONFLICT (workspace,id) DO UPDATE
1660
  SET entity_name=EXCLUDED.entity_name,
1661
  content=EXCLUDED.content,
 
1663
  update_time=CURRENT_TIMESTAMP
1664
  """,
1665
  "upsert_relationship": """INSERT INTO LIGHTRAG_VDB_RELATION (workspace, id, source_id,
1666
+ target_id, content, content_vector, chunk_id)
1667
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
1668
  ON CONFLICT (workspace,id) DO UPDATE
1669
  SET source_id=EXCLUDED.source_id,
1670
  target_id=EXCLUDED.target_id,
 
1672
  content_vector=EXCLUDED.content_vector, update_time = CURRENT_TIMESTAMP
1673
  """,
1674
  # SQL for VectorStorage
1675
+ # "entities": """SELECT entity_name FROM
1676
+ # (SELECT id, entity_name, 1 - (content_vector <=> '[{embedding_string}]'::vector) as distance
1677
+ # FROM LIGHTRAG_VDB_ENTITY where workspace=$1)
1678
+ # WHERE distance>$2 ORDER BY distance DESC LIMIT $3
1679
+ # """,
1680
+ # "relationships": """SELECT source_id as src_id, target_id as tgt_id FROM
1681
+ # (SELECT id, source_id,target_id, 1 - (content_vector <=> '[{embedding_string}]'::vector) as distance
1682
+ # FROM LIGHTRAG_VDB_RELATION where workspace=$1)
1683
+ # WHERE distance>$2 ORDER BY distance DESC LIMIT $3
1684
+ # """,
1685
+ # "chunks": """SELECT id FROM
1686
+ # (SELECT id, 1 - (content_vector <=> '[{embedding_string}]'::vector) as distance
1687
+ # FROM LIGHTRAG_DOC_CHUNKS where workspace=$1)
1688
+ # WHERE distance>$2 ORDER BY distance DESC LIMIT $3
1689
+ # """,
1690
  # DROP tables
1691
  "drop_all": """
1692
  DROP TABLE IF EXISTS LIGHTRAG_DOC_FULL CASCADE;
 
1710
  "drop_vdb_relation": """
1711
  DROP TABLE IF EXISTS LIGHTRAG_VDB_RELATION CASCADE;
1712
  """,
1713
+ "relationships": """
1714
+ WITH relevant_chunks AS (
1715
+ SELECT id as chunk_id
1716
+ FROM LIGHTRAG_DOC_CHUNKS
1717
+ WHERE {doc_ids} IS NULL OR full_doc_id = ANY(ARRAY[{doc_ids}])
1718
+ )
1719
+ SELECT source_id as src_id, target_id as tgt_id
1720
+ FROM (
1721
+ SELECT r.id, r.source_id, r.target_id, 1 - (r.content_vector <=> '[{embedding_string}]'::vector) as distance
1722
+ FROM LIGHTRAG_VDB_RELATION r
1723
+ WHERE r.workspace=$1
1724
+ AND r.chunk_id IN (SELECT chunk_id FROM relevant_chunks)
1725
+ ) filtered
1726
+ WHERE distance>$2
1727
+ ORDER BY distance DESC
1728
+ LIMIT $3
1729
+ """,
1730
+ "entities": """
1731
+ WITH relevant_chunks AS (
1732
+ SELECT id as chunk_id
1733
+ FROM LIGHTRAG_DOC_CHUNKS
1734
+ WHERE {doc_ids} IS NULL OR full_doc_id = ANY(ARRAY[{doc_ids}])
1735
+ )
1736
+ SELECT entity_name FROM
1737
+ (
1738
+ SELECT id, entity_name, 1 - (content_vector <=> '[{embedding_string}]'::vector) as distance
1739
+ FROM LIGHTRAG_VDB_ENTITY
1740
+ where workspace=$1
1741
+ AND chunk_id IN (SELECT chunk_id FROM relevant_chunks)
1742
+ )
1743
+ WHERE distance>$2
1744
+ ORDER BY distance DESC
1745
+ LIMIT $3
1746
+ """,
1747
+ "chunks": """
1748
+ WITH relevant_chunks AS (
1749
+ SELECT id as chunk_id
1750
+ FROM LIGHTRAG_DOC_CHUNKS
1751
+ WHERE {doc_ids} IS NULL OR full_doc_id = ANY(ARRAY[{doc_ids}])
1752
+ )
1753
+ SELECT id FROM
1754
+ (
1755
+ SELECT id, 1 - (content_vector <=> '[{embedding_string}]'::vector) as distance
1756
+ FROM LIGHTRAG_DOC_CHUNKS
1757
+ where workspace=$1
1758
+ AND id IN (SELECT chunk_id FROM relevant_chunks)
1759
+ )
1760
+ WHERE distance>$2
1761
+ ORDER BY distance DESC
1762
+ LIMIT $3
1763
+ """,
1764
  }
lightrag/kg/tidb_impl.py CHANGED
@@ -463,6 +463,100 @@ class TiDBVectorDBStorage(BaseVectorStorage):
463
  logger.error(f"Error searching records with prefix '{prefix}': {e}")
464
  return []
465
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
  @final
468
  @dataclass
 
463
  logger.error(f"Error searching records with prefix '{prefix}': {e}")
464
  return []
465
 
466
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
467
+ """Get vector data by its ID
468
+
469
+ Args:
470
+ id: The unique identifier of the vector
471
+
472
+ Returns:
473
+ The vector data if found, or None if not found
474
+ """
475
+ try:
476
+ # Determine which table to query based on namespace
477
+ if self.namespace == NameSpace.VECTOR_STORE_ENTITIES:
478
+ sql_template = """
479
+ SELECT entity_id as id, name as entity_name, entity_type, description, content
480
+ FROM LIGHTRAG_GRAPH_NODES
481
+ WHERE entity_id = :entity_id AND workspace = :workspace
482
+ """
483
+ params = {"entity_id": id, "workspace": self.db.workspace}
484
+ elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS:
485
+ sql_template = """
486
+ SELECT relation_id as id, source_name as src_id, target_name as tgt_id,
487
+ keywords, description, content
488
+ FROM LIGHTRAG_GRAPH_EDGES
489
+ WHERE relation_id = :relation_id AND workspace = :workspace
490
+ """
491
+ params = {"relation_id": id, "workspace": self.db.workspace}
492
+ elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS:
493
+ sql_template = """
494
+ SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id
495
+ FROM LIGHTRAG_DOC_CHUNKS
496
+ WHERE chunk_id = :chunk_id AND workspace = :workspace
497
+ """
498
+ params = {"chunk_id": id, "workspace": self.db.workspace}
499
+ else:
500
+ logger.warning(
501
+ f"Namespace {self.namespace} not supported for get_by_id"
502
+ )
503
+ return None
504
+
505
+ result = await self.db.query(sql_template, params=params)
506
+ return result
507
+ except Exception as e:
508
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
509
+ return None
510
+
511
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
512
+ """Get multiple vector data by their IDs
513
+
514
+ Args:
515
+ ids: List of unique identifiers
516
+
517
+ Returns:
518
+ List of vector data objects that were found
519
+ """
520
+ if not ids:
521
+ return []
522
+
523
+ try:
524
+ # Format IDs for SQL IN clause
525
+ ids_str = ", ".join([f"'{id}'" for id in ids])
526
+
527
+ # Determine which table to query based on namespace
528
+ if self.namespace == NameSpace.VECTOR_STORE_ENTITIES:
529
+ sql_template = f"""
530
+ SELECT entity_id as id, name as entity_name, entity_type, description, content
531
+ FROM LIGHTRAG_GRAPH_NODES
532
+ WHERE entity_id IN ({ids_str}) AND workspace = :workspace
533
+ """
534
+ elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS:
535
+ sql_template = f"""
536
+ SELECT relation_id as id, source_name as src_id, target_name as tgt_id,
537
+ keywords, description, content
538
+ FROM LIGHTRAG_GRAPH_EDGES
539
+ WHERE relation_id IN ({ids_str}) AND workspace = :workspace
540
+ """
541
+ elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS:
542
+ sql_template = f"""
543
+ SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id
544
+ FROM LIGHTRAG_DOC_CHUNKS
545
+ WHERE chunk_id IN ({ids_str}) AND workspace = :workspace
546
+ """
547
+ else:
548
+ logger.warning(
549
+ f"Namespace {self.namespace} not supported for get_by_ids"
550
+ )
551
+ return []
552
+
553
+ params = {"workspace": self.db.workspace}
554
+ results = await self.db.query(sql_template, params=params, multirows=True)
555
+ return results if results else []
556
+ except Exception as e:
557
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
558
+ return []
559
+
560
 
561
  @final
562
  @dataclass
lightrag/lightrag.py CHANGED
@@ -30,11 +30,10 @@ from .namespace import NameSpace, make_namespace
30
  from .operate import (
31
  chunking_by_token_size,
32
  extract_entities,
33
- extract_keywords_only,
34
  kg_query,
35
- kg_query_with_keywords,
36
  mix_kg_vector_query,
37
  naive_query,
 
38
  )
39
  from .prompt import GRAPH_FIELD_SEP, PROMPTS
40
  from .utils import (
@@ -45,6 +44,9 @@ from .utils import (
45
  encode_string_by_tiktoken,
46
  lazy_external_import,
47
  limit_async_func_call,
 
 
 
48
  logger,
49
  )
50
  from .types import KnowledgeGraph
@@ -309,7 +311,7 @@ class LightRAG:
309
  # Verify storage implementation compatibility
310
  verify_storage_implementation(storage_type, storage_name)
311
  # Check environment variables
312
- # self.check_storage_env_vars(storage_name)
313
 
314
  # Ensure vector_db_storage_cls_kwargs has required fields
315
  self.vector_db_storage_cls_kwargs = {
@@ -536,11 +538,6 @@ class LightRAG:
536
  storage_class = lazy_external_import(import_path, storage_name)
537
  return storage_class
538
 
539
- @staticmethod
540
- def clean_text(text: str) -> str:
541
- """Clean text by removing null bytes (0x00) and whitespace"""
542
- return text.strip().replace("\x00", "")
543
-
544
  def insert(
545
  self,
546
  input: str | list[str],
@@ -602,8 +599,8 @@ class LightRAG:
602
  update_storage = False
603
  try:
604
  # Clean input texts
605
- full_text = self.clean_text(full_text)
606
- text_chunks = [self.clean_text(chunk) for chunk in text_chunks]
607
 
608
  # Process cleaned texts
609
  if doc_id is None:
@@ -682,7 +679,7 @@ class LightRAG:
682
  contents = {id_: doc for id_, doc in zip(ids, input)}
683
  else:
684
  # Clean input text and remove duplicates
685
- input = list(set(self.clean_text(doc) for doc in input))
686
  # Generate contents dict of MD5 hash IDs and documents
687
  contents = {compute_mdhash_id(doc, prefix="doc-"): doc for doc in input}
688
 
@@ -698,7 +695,7 @@ class LightRAG:
698
  new_docs: dict[str, Any] = {
699
  id_: {
700
  "content": content,
701
- "content_summary": self._get_content_summary(content),
702
  "content_length": len(content),
703
  "status": DocStatus.PENDING,
704
  "created_at": datetime.now().isoformat(),
@@ -1063,7 +1060,7 @@ class LightRAG:
1063
  all_chunks_data: dict[str, dict[str, str]] = {}
1064
  chunk_to_source_map: dict[str, str] = {}
1065
  for chunk_data in custom_kg.get("chunks", []):
1066
- chunk_content = self.clean_text(chunk_data["content"])
1067
  source_id = chunk_data["source_id"]
1068
  tokens = len(
1069
  encode_string_by_tiktoken(
@@ -1296,8 +1293,17 @@ class LightRAG:
1296
  self, query: str, prompt: str, param: QueryParam = QueryParam()
1297
  ):
1298
  """
1299
- 1. Extract keywords from the 'query' using new function in operate.py.
1300
- 2. Then run the standard aquery() flow with the final prompt (formatted_question).
 
 
 
 
 
 
 
 
 
1301
  """
1302
  loop = always_get_an_event_loop()
1303
  return loop.run_until_complete(
@@ -1308,66 +1314,29 @@ class LightRAG:
1308
  self, query: str, prompt: str, param: QueryParam = QueryParam()
1309
  ) -> str | AsyncIterator[str]:
1310
  """
1311
- 1. Calls extract_keywords_only to get HL/LL keywords from 'query'.
1312
- 2. Then calls kg_query(...) or naive_query(...), etc. as the main query, while also injecting the newly extracted keywords if needed.
 
 
 
 
 
 
 
1313
  """
1314
- # ---------------------
1315
- # STEP 1: Keyword Extraction
1316
- # ---------------------
1317
- hl_keywords, ll_keywords = await extract_keywords_only(
1318
- text=query,
1319
  param=param,
 
 
 
 
 
1320
  global_config=asdict(self),
1321
- hashing_kv=self.llm_response_cache, # Directly use llm_response_cache
1322
  )
1323
 
1324
- param.hl_keywords = hl_keywords
1325
- param.ll_keywords = ll_keywords
1326
-
1327
- # ---------------------
1328
- # STEP 2: Final Query Logic
1329
- # ---------------------
1330
-
1331
- # Create a new string with the prompt and the keywords
1332
- ll_keywords_str = ", ".join(ll_keywords)
1333
- hl_keywords_str = ", ".join(hl_keywords)
1334
- formatted_question = f"{prompt}\n\n### Keywords:\nHigh-level: {hl_keywords_str}\nLow-level: {ll_keywords_str}\n\n### Query:\n{query}"
1335
-
1336
- if param.mode in ["local", "global", "hybrid"]:
1337
- response = await kg_query_with_keywords(
1338
- formatted_question,
1339
- self.chunk_entity_relation_graph,
1340
- self.entities_vdb,
1341
- self.relationships_vdb,
1342
- self.text_chunks,
1343
- param,
1344
- asdict(self),
1345
- hashing_kv=self.llm_response_cache, # Directly use llm_response_cache
1346
- )
1347
- elif param.mode == "naive":
1348
- response = await naive_query(
1349
- formatted_question,
1350
- self.chunks_vdb,
1351
- self.text_chunks,
1352
- param,
1353
- asdict(self),
1354
- hashing_kv=self.llm_response_cache, # Directly use llm_response_cache
1355
- )
1356
- elif param.mode == "mix":
1357
- response = await mix_kg_vector_query(
1358
- formatted_question,
1359
- self.chunk_entity_relation_graph,
1360
- self.entities_vdb,
1361
- self.relationships_vdb,
1362
- self.chunks_vdb,
1363
- self.text_chunks,
1364
- param,
1365
- asdict(self),
1366
- hashing_kv=self.llm_response_cache, # Directly use llm_response_cache
1367
- )
1368
- else:
1369
- raise ValueError(f"Unknown mode {param.mode}")
1370
-
1371
  await self._query_done()
1372
  return response
1373
 
@@ -1465,21 +1434,6 @@ class LightRAG:
1465
  ]
1466
  )
1467
 
1468
- def _get_content_summary(self, content: str, max_length: int = 100) -> str:
1469
- """Get summary of document content
1470
-
1471
- Args:
1472
- content: Original document content
1473
- max_length: Maximum length of summary
1474
-
1475
- Returns:
1476
- Truncated content with ellipsis if needed
1477
- """
1478
- content = content.strip()
1479
- if len(content) <= max_length:
1480
- return content
1481
- return content[:max_length] + "..."
1482
-
1483
  async def get_processing_status(self) -> dict[str, int]:
1484
  """Get current document processing status counts
1485
 
@@ -1756,19 +1710,7 @@ class LightRAG:
1756
  async def get_entity_info(
1757
  self, entity_name: str, include_vector_data: bool = False
1758
  ) -> dict[str, str | None | dict[str, str]]:
1759
- """Get detailed information of an entity
1760
-
1761
- Args:
1762
- entity_name: Entity name (no need for quotes)
1763
- include_vector_data: Whether to include data from the vector database
1764
-
1765
- Returns:
1766
- dict: A dictionary containing entity information, including:
1767
- - entity_name: Entity name
1768
- - source_id: Source document ID
1769
- - graph_data: Complete node data from the graph database
1770
- - vector_data: (optional) Data from the vector database
1771
- """
1772
 
1773
  # Get information from the graph
1774
  node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
@@ -1783,29 +1725,15 @@ class LightRAG:
1783
  # Optional: Get vector database information
1784
  if include_vector_data:
1785
  entity_id = compute_mdhash_id(entity_name, prefix="ent-")
1786
- vector_data = self.entities_vdb._client.get([entity_id])
1787
- result["vector_data"] = vector_data[0] if vector_data else None
1788
 
1789
  return result
1790
 
1791
  async def get_relation_info(
1792
  self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
1793
  ) -> dict[str, str | None | dict[str, str]]:
1794
- """Get detailed information of a relationship
1795
-
1796
- Args:
1797
- src_entity: Source entity name (no need for quotes)
1798
- tgt_entity: Target entity name (no need for quotes)
1799
- include_vector_data: Whether to include data from the vector database
1800
-
1801
- Returns:
1802
- dict: A dictionary containing relationship information, including:
1803
- - src_entity: Source entity name
1804
- - tgt_entity: Target entity name
1805
- - source_id: Source document ID
1806
- - graph_data: Complete edge data from the graph database
1807
- - vector_data: (optional) Data from the vector database
1808
- """
1809
 
1810
  # Get information from the graph
1811
  edge_data = await self.chunk_entity_relation_graph.get_edge(
@@ -1823,8 +1751,8 @@ class LightRAG:
1823
  # Optional: Get vector database information
1824
  if include_vector_data:
1825
  rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
1826
- vector_data = self.relationships_vdb._client.get([rel_id])
1827
- result["vector_data"] = vector_data[0] if vector_data else None
1828
 
1829
  return result
1830
 
@@ -2622,6 +2550,12 @@ class LightRAG:
2622
 
2623
  # 9. Delete source entities
2624
  for entity_name in source_entities:
 
 
 
 
 
 
2625
  # Delete entity node from knowledge graph
2626
  await self.chunk_entity_relation_graph.delete_node(entity_name)
2627
 
 
30
  from .operate import (
31
  chunking_by_token_size,
32
  extract_entities,
 
33
  kg_query,
 
34
  mix_kg_vector_query,
35
  naive_query,
36
+ query_with_keywords,
37
  )
38
  from .prompt import GRAPH_FIELD_SEP, PROMPTS
39
  from .utils import (
 
44
  encode_string_by_tiktoken,
45
  lazy_external_import,
46
  limit_async_func_call,
47
+ get_content_summary,
48
+ clean_text,
49
+ check_storage_env_vars,
50
  logger,
51
  )
52
  from .types import KnowledgeGraph
 
311
  # Verify storage implementation compatibility
312
  verify_storage_implementation(storage_type, storage_name)
313
  # Check environment variables
314
+ check_storage_env_vars(storage_name)
315
 
316
  # Ensure vector_db_storage_cls_kwargs has required fields
317
  self.vector_db_storage_cls_kwargs = {
 
538
  storage_class = lazy_external_import(import_path, storage_name)
539
  return storage_class
540
 
 
 
 
 
 
541
  def insert(
542
  self,
543
  input: str | list[str],
 
599
  update_storage = False
600
  try:
601
  # Clean input texts
602
+ full_text = clean_text(full_text)
603
+ text_chunks = [clean_text(chunk) for chunk in text_chunks]
604
 
605
  # Process cleaned texts
606
  if doc_id is None:
 
679
  contents = {id_: doc for id_, doc in zip(ids, input)}
680
  else:
681
  # Clean input text and remove duplicates
682
+ input = list(set(clean_text(doc) for doc in input))
683
  # Generate contents dict of MD5 hash IDs and documents
684
  contents = {compute_mdhash_id(doc, prefix="doc-"): doc for doc in input}
685
 
 
695
  new_docs: dict[str, Any] = {
696
  id_: {
697
  "content": content,
698
+ "content_summary": get_content_summary(content),
699
  "content_length": len(content),
700
  "status": DocStatus.PENDING,
701
  "created_at": datetime.now().isoformat(),
 
1060
  all_chunks_data: dict[str, dict[str, str]] = {}
1061
  chunk_to_source_map: dict[str, str] = {}
1062
  for chunk_data in custom_kg.get("chunks", []):
1063
+ chunk_content = clean_text(chunk_data["content"])
1064
  source_id = chunk_data["source_id"]
1065
  tokens = len(
1066
  encode_string_by_tiktoken(
 
1293
  self, query: str, prompt: str, param: QueryParam = QueryParam()
1294
  ):
1295
  """
1296
+ Query with separate keyword extraction step.
1297
+
1298
+ This method extracts keywords from the query first, then uses them for the query.
1299
+
1300
+ Args:
1301
+ query: User query
1302
+ prompt: Additional prompt for the query
1303
+ param: Query parameters
1304
+
1305
+ Returns:
1306
+ Query response
1307
  """
1308
  loop = always_get_an_event_loop()
1309
  return loop.run_until_complete(
 
1314
  self, query: str, prompt: str, param: QueryParam = QueryParam()
1315
  ) -> str | AsyncIterator[str]:
1316
  """
1317
+ Async version of query_with_separate_keyword_extraction.
1318
+
1319
+ Args:
1320
+ query: User query
1321
+ prompt: Additional prompt for the query
1322
+ param: Query parameters
1323
+
1324
+ Returns:
1325
+ Query response or async iterator
1326
  """
1327
+ response = await query_with_keywords(
1328
+ query=query,
1329
+ prompt=prompt,
 
 
1330
  param=param,
1331
+ knowledge_graph_inst=self.chunk_entity_relation_graph,
1332
+ entities_vdb=self.entities_vdb,
1333
+ relationships_vdb=self.relationships_vdb,
1334
+ chunks_vdb=self.chunks_vdb,
1335
+ text_chunks_db=self.text_chunks,
1336
  global_config=asdict(self),
1337
+ hashing_kv=self.llm_response_cache,
1338
  )
1339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1340
  await self._query_done()
1341
  return response
1342
 
 
1434
  ]
1435
  )
1436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1437
  async def get_processing_status(self) -> dict[str, int]:
1438
  """Get current document processing status counts
1439
 
 
1710
  async def get_entity_info(
1711
  self, entity_name: str, include_vector_data: bool = False
1712
  ) -> dict[str, str | None | dict[str, str]]:
1713
+ """Get detailed information of an entity"""
 
 
 
 
 
 
 
 
 
 
 
 
1714
 
1715
  # Get information from the graph
1716
  node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
 
1725
  # Optional: Get vector database information
1726
  if include_vector_data:
1727
  entity_id = compute_mdhash_id(entity_name, prefix="ent-")
1728
+ vector_data = await self.entities_vdb.get_by_id(entity_id)
1729
+ result["vector_data"] = vector_data
1730
 
1731
  return result
1732
 
1733
  async def get_relation_info(
1734
  self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
1735
  ) -> dict[str, str | None | dict[str, str]]:
1736
+ """Get detailed information of a relationship"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1737
 
1738
  # Get information from the graph
1739
  edge_data = await self.chunk_entity_relation_graph.get_edge(
 
1751
  # Optional: Get vector database information
1752
  if include_vector_data:
1753
  rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
1754
+ vector_data = await self.relationships_vdb.get_by_id(rel_id)
1755
+ result["vector_data"] = vector_data
1756
 
1757
  return result
1758
 
 
2550
 
2551
  # 9. Delete source entities
2552
  for entity_name in source_entities:
2553
+ if entity_name == target_entity:
2554
+ logger.info(
2555
+ f"Skipping deletion of '{entity_name}' as it's also the target entity"
2556
+ )
2557
+ continue
2558
+
2559
  # Delete entity node from knowledge graph
2560
  await self.chunk_entity_relation_graph.delete_node(entity_name)
2561
 
lightrag/llm/azure_openai.py CHANGED
@@ -55,6 +55,7 @@ async def azure_openai_complete_if_cache(
55
 
56
  openai_async_client = AsyncAzureOpenAI(
57
  azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
 
58
  api_key=os.getenv("AZURE_OPENAI_API_KEY"),
59
  api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
60
  )
@@ -136,6 +137,7 @@ async def azure_openai_embed(
136
 
137
  openai_async_client = AsyncAzureOpenAI(
138
  azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
 
139
  api_key=os.getenv("AZURE_OPENAI_API_KEY"),
140
  api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
141
  )
 
55
 
56
  openai_async_client = AsyncAzureOpenAI(
57
  azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
58
+ azure_deployment=model,
59
  api_key=os.getenv("AZURE_OPENAI_API_KEY"),
60
  api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
61
  )
 
137
 
138
  openai_async_client = AsyncAzureOpenAI(
139
  azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
140
+ azure_deployment=model,
141
  api_key=os.getenv("AZURE_OPENAI_API_KEY"),
142
  api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
143
  )
lightrag/operate.py CHANGED
@@ -141,18 +141,36 @@ async def _handle_single_entity_extraction(
141
  ):
142
  if len(record_attributes) < 4 or record_attributes[0] != '"entity"':
143
  return None
144
- # add this record as a node in the G
 
145
  entity_name = clean_str(record_attributes[1]).strip('"')
146
  if not entity_name.strip():
 
 
 
147
  return None
 
 
148
  entity_type = clean_str(record_attributes[2]).strip('"')
 
 
 
 
 
 
 
149
  entity_description = clean_str(record_attributes[3]).strip('"')
150
- entity_source_id = chunk_key
 
 
 
 
 
151
  return dict(
152
  entity_name=entity_name,
153
  entity_type=entity_type,
154
  description=entity_description,
155
- source_id=entity_source_id,
156
  metadata={"created_at": time.time()},
157
  )
158
 
@@ -438,30 +456,91 @@ async def extract_entities(
438
  else:
439
  return await use_llm_func(input_text)
440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  async def _process_single_content(chunk_key_dp: tuple[str, TextChunkSchema]):
442
- """ "Prpocess a single chunk
443
  Args:
444
  chunk_key_dp (tuple[str, TextChunkSchema]):
445
- ("chunck-xxxxxx", {"tokens": int, "content": str, "full_doc_id": str, "chunk_order_index": int})
446
  """
447
  nonlocal processed_chunks
448
  chunk_key = chunk_key_dp[0]
449
  chunk_dp = chunk_key_dp[1]
450
  content = chunk_dp["content"]
451
- # hint_prompt = entity_extract_prompt.format(**context_base, input_text=content)
 
452
  hint_prompt = entity_extract_prompt.format(
453
  **context_base, input_text="{input_text}"
454
  ).format(**context_base, input_text=content)
455
 
456
  final_result = await _user_llm_func_with_cache(hint_prompt)
457
  history = pack_user_ass_to_openai_messages(hint_prompt, final_result)
 
 
 
 
 
 
 
458
  for now_glean_index in range(entity_extract_max_gleaning):
459
  glean_result = await _user_llm_func_with_cache(
460
  continue_prompt, history_messages=history
461
  )
462
 
463
  history += pack_user_ass_to_openai_messages(continue_prompt, glean_result)
464
- final_result += glean_result
 
 
 
 
 
 
 
 
 
 
 
465
  if now_glean_index == entity_extract_max_gleaning - 1:
466
  break
467
 
@@ -472,35 +551,6 @@ async def extract_entities(
472
  if if_loop_result != "yes":
473
  break
474
 
475
- records = split_string_by_multi_markers(
476
- final_result,
477
- [context_base["record_delimiter"], context_base["completion_delimiter"]],
478
- )
479
-
480
- maybe_nodes = defaultdict(list)
481
- maybe_edges = defaultdict(list)
482
- for record in records:
483
- record = re.search(r"\((.*)\)", record)
484
- if record is None:
485
- continue
486
- record = record.group(1)
487
- record_attributes = split_string_by_multi_markers(
488
- record, [context_base["tuple_delimiter"]]
489
- )
490
- if_entities = await _handle_single_entity_extraction(
491
- record_attributes, chunk_key
492
- )
493
- if if_entities is not None:
494
- maybe_nodes[if_entities["entity_name"]].append(if_entities)
495
- continue
496
-
497
- if_relation = await _handle_single_relationship_extraction(
498
- record_attributes, chunk_key
499
- )
500
- if if_relation is not None:
501
- maybe_edges[(if_relation["src_id"], if_relation["tgt_id"])].append(
502
- if_relation
503
- )
504
  processed_chunks += 1
505
  entities_count = len(maybe_nodes)
506
  relations_count = len(maybe_edges)
@@ -912,7 +962,10 @@ async def mix_kg_vector_query(
912
  try:
913
  # Reduce top_k for vector search in hybrid mode since we have structured information from KG
914
  mix_topk = min(10, query_param.top_k)
915
- results = await chunks_vdb.query(augmented_query, top_k=mix_topk)
 
 
 
916
  if not results:
917
  return None
918
 
@@ -1121,7 +1174,11 @@ async def _get_node_data(
1121
  logger.info(
1122
  f"Query nodes: {query}, top_k: {query_param.top_k}, cosine: {entities_vdb.cosine_better_than_threshold}"
1123
  )
1124
- results = await entities_vdb.query(query, top_k=query_param.top_k)
 
 
 
 
1125
  if not len(results):
1126
  return "", "", ""
1127
  # get entity information
@@ -1374,7 +1431,10 @@ async def _get_edge_data(
1374
  logger.info(
1375
  f"Query edges: {keywords}, top_k: {query_param.top_k}, cosine: {relationships_vdb.cosine_better_than_threshold}"
1376
  )
1377
- results = await relationships_vdb.query(keywords, top_k=query_param.top_k)
 
 
 
1378
 
1379
  if not len(results):
1380
  return "", "", ""
@@ -1623,7 +1683,9 @@ async def naive_query(
1623
  if cached_response is not None:
1624
  return cached_response
1625
 
1626
- results = await chunks_vdb.query(query, top_k=query_param.top_k)
 
 
1627
  if not len(results):
1628
  return PROMPTS["fail_response"]
1629
 
@@ -1854,3 +1916,90 @@ async def kg_query_with_keywords(
1854
  )
1855
 
1856
  return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  ):
142
  if len(record_attributes) < 4 or record_attributes[0] != '"entity"':
143
  return None
144
+
145
+ # Clean and validate entity name
146
  entity_name = clean_str(record_attributes[1]).strip('"')
147
  if not entity_name.strip():
148
+ logger.warning(
149
+ f"Entity extraction error: empty entity name in: {record_attributes}"
150
+ )
151
  return None
152
+
153
+ # Clean and validate entity type
154
  entity_type = clean_str(record_attributes[2]).strip('"')
155
+ if not entity_type.strip() or entity_type.startswith('("'):
156
+ logger.warning(
157
+ f"Entity extraction error: invalid entity type in: {record_attributes}"
158
+ )
159
+ return None
160
+
161
+ # Clean and validate description
162
  entity_description = clean_str(record_attributes[3]).strip('"')
163
+ if not entity_description.strip():
164
+ logger.warning(
165
+ f"Entity extraction error: empty description for entity '{entity_name}' of type '{entity_type}'"
166
+ )
167
+ return None
168
+
169
  return dict(
170
  entity_name=entity_name,
171
  entity_type=entity_type,
172
  description=entity_description,
173
+ source_id=chunk_key,
174
  metadata={"created_at": time.time()},
175
  )
176
 
 
456
  else:
457
  return await use_llm_func(input_text)
458
 
459
+ async def _process_extraction_result(result: str, chunk_key: str):
460
+ """Process a single extraction result (either initial or gleaning)
461
+ Args:
462
+ result (str): The extraction result to process
463
+ chunk_key (str): The chunk key for source tracking
464
+ Returns:
465
+ tuple: (nodes_dict, edges_dict) containing the extracted entities and relationships
466
+ """
467
+ maybe_nodes = defaultdict(list)
468
+ maybe_edges = defaultdict(list)
469
+
470
+ records = split_string_by_multi_markers(
471
+ result,
472
+ [context_base["record_delimiter"], context_base["completion_delimiter"]],
473
+ )
474
+
475
+ for record in records:
476
+ record = re.search(r"\((.*)\)", record)
477
+ if record is None:
478
+ continue
479
+ record = record.group(1)
480
+ record_attributes = split_string_by_multi_markers(
481
+ record, [context_base["tuple_delimiter"]]
482
+ )
483
+
484
+ if_entities = await _handle_single_entity_extraction(
485
+ record_attributes, chunk_key
486
+ )
487
+ if if_entities is not None:
488
+ maybe_nodes[if_entities["entity_name"]].append(if_entities)
489
+ continue
490
+
491
+ if_relation = await _handle_single_relationship_extraction(
492
+ record_attributes, chunk_key
493
+ )
494
+ if if_relation is not None:
495
+ maybe_edges[(if_relation["src_id"], if_relation["tgt_id"])].append(
496
+ if_relation
497
+ )
498
+
499
+ return maybe_nodes, maybe_edges
500
+
501
  async def _process_single_content(chunk_key_dp: tuple[str, TextChunkSchema]):
502
+ """Process a single chunk
503
  Args:
504
  chunk_key_dp (tuple[str, TextChunkSchema]):
505
+ ("chunk-xxxxxx", {"tokens": int, "content": str, "full_doc_id": str, "chunk_order_index": int})
506
  """
507
  nonlocal processed_chunks
508
  chunk_key = chunk_key_dp[0]
509
  chunk_dp = chunk_key_dp[1]
510
  content = chunk_dp["content"]
511
+
512
+ # Get initial extraction
513
  hint_prompt = entity_extract_prompt.format(
514
  **context_base, input_text="{input_text}"
515
  ).format(**context_base, input_text=content)
516
 
517
  final_result = await _user_llm_func_with_cache(hint_prompt)
518
  history = pack_user_ass_to_openai_messages(hint_prompt, final_result)
519
+
520
+ # Process initial extraction
521
+ maybe_nodes, maybe_edges = await _process_extraction_result(
522
+ final_result, chunk_key
523
+ )
524
+
525
+ # Process additional gleaning results
526
  for now_glean_index in range(entity_extract_max_gleaning):
527
  glean_result = await _user_llm_func_with_cache(
528
  continue_prompt, history_messages=history
529
  )
530
 
531
  history += pack_user_ass_to_openai_messages(continue_prompt, glean_result)
532
+
533
+ # Process gleaning result separately
534
+ glean_nodes, glean_edges = await _process_extraction_result(
535
+ glean_result, chunk_key
536
+ )
537
+
538
+ # Merge results
539
+ for entity_name, entities in glean_nodes.items():
540
+ maybe_nodes[entity_name].extend(entities)
541
+ for edge_key, edges in glean_edges.items():
542
+ maybe_edges[edge_key].extend(edges)
543
+
544
  if now_glean_index == entity_extract_max_gleaning - 1:
545
  break
546
 
 
551
  if if_loop_result != "yes":
552
  break
553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  processed_chunks += 1
555
  entities_count = len(maybe_nodes)
556
  relations_count = len(maybe_edges)
 
962
  try:
963
  # Reduce top_k for vector search in hybrid mode since we have structured information from KG
964
  mix_topk = min(10, query_param.top_k)
965
+ # TODO: add ids to the query
966
+ results = await chunks_vdb.query(
967
+ augmented_query, top_k=mix_topk, ids=query_param.ids
968
+ )
969
  if not results:
970
  return None
971
 
 
1174
  logger.info(
1175
  f"Query nodes: {query}, top_k: {query_param.top_k}, cosine: {entities_vdb.cosine_better_than_threshold}"
1176
  )
1177
+
1178
+ results = await entities_vdb.query(
1179
+ query, top_k=query_param.top_k, ids=query_param.ids
1180
+ )
1181
+
1182
  if not len(results):
1183
  return "", "", ""
1184
  # get entity information
 
1431
  logger.info(
1432
  f"Query edges: {keywords}, top_k: {query_param.top_k}, cosine: {relationships_vdb.cosine_better_than_threshold}"
1433
  )
1434
+
1435
+ results = await relationships_vdb.query(
1436
+ keywords, top_k=query_param.top_k, ids=query_param.ids
1437
+ )
1438
 
1439
  if not len(results):
1440
  return "", "", ""
 
1683
  if cached_response is not None:
1684
  return cached_response
1685
 
1686
+ results = await chunks_vdb.query(
1687
+ query, top_k=query_param.top_k, ids=query_param.ids
1688
+ )
1689
  if not len(results):
1690
  return PROMPTS["fail_response"]
1691
 
 
1916
  )
1917
 
1918
  return response
1919
+
1920
+
1921
+ async def query_with_keywords(
1922
+ query: str,
1923
+ prompt: str,
1924
+ param: QueryParam,
1925
+ knowledge_graph_inst: BaseGraphStorage,
1926
+ entities_vdb: BaseVectorStorage,
1927
+ relationships_vdb: BaseVectorStorage,
1928
+ chunks_vdb: BaseVectorStorage,
1929
+ text_chunks_db: BaseKVStorage,
1930
+ global_config: dict[str, str],
1931
+ hashing_kv: BaseKVStorage | None = None,
1932
+ ) -> str | AsyncIterator[str]:
1933
+ """
1934
+ Extract keywords from the query and then use them for retrieving information.
1935
+
1936
+ 1. Extracts high-level and low-level keywords from the query
1937
+ 2. Formats the query with the extracted keywords and prompt
1938
+ 3. Uses the appropriate query method based on param.mode
1939
+
1940
+ Args:
1941
+ query: The user's query
1942
+ prompt: Additional prompt to prepend to the query
1943
+ param: Query parameters
1944
+ knowledge_graph_inst: Knowledge graph storage
1945
+ entities_vdb: Entities vector database
1946
+ relationships_vdb: Relationships vector database
1947
+ chunks_vdb: Document chunks vector database
1948
+ text_chunks_db: Text chunks storage
1949
+ global_config: Global configuration
1950
+ hashing_kv: Cache storage
1951
+
1952
+ Returns:
1953
+ Query response or async iterator
1954
+ """
1955
+ # Extract keywords
1956
+ hl_keywords, ll_keywords = await extract_keywords_only(
1957
+ text=query,
1958
+ param=param,
1959
+ global_config=global_config,
1960
+ hashing_kv=hashing_kv,
1961
+ )
1962
+
1963
+ param.hl_keywords = hl_keywords
1964
+ param.ll_keywords = ll_keywords
1965
+
1966
+ # Create a new string with the prompt and the keywords
1967
+ ll_keywords_str = ", ".join(ll_keywords)
1968
+ hl_keywords_str = ", ".join(hl_keywords)
1969
+ formatted_question = f"{prompt}\n\n### Keywords:\nHigh-level: {hl_keywords_str}\nLow-level: {ll_keywords_str}\n\n### Query:\n{query}"
1970
+
1971
+ # Use appropriate query method based on mode
1972
+ if param.mode in ["local", "global", "hybrid"]:
1973
+ return await kg_query_with_keywords(
1974
+ formatted_question,
1975
+ knowledge_graph_inst,
1976
+ entities_vdb,
1977
+ relationships_vdb,
1978
+ text_chunks_db,
1979
+ param,
1980
+ global_config,
1981
+ hashing_kv=hashing_kv,
1982
+ )
1983
+ elif param.mode == "naive":
1984
+ return await naive_query(
1985
+ formatted_question,
1986
+ chunks_vdb,
1987
+ text_chunks_db,
1988
+ param,
1989
+ global_config,
1990
+ hashing_kv=hashing_kv,
1991
+ )
1992
+ elif param.mode == "mix":
1993
+ return await mix_kg_vector_query(
1994
+ formatted_question,
1995
+ knowledge_graph_inst,
1996
+ entities_vdb,
1997
+ relationships_vdb,
1998
+ chunks_vdb,
1999
+ text_chunks_db,
2000
+ param,
2001
+ global_config,
2002
+ hashing_kv=hashing_kv,
2003
+ )
2004
+ else:
2005
+ raise ValueError(f"Unknown mode {param.mode}")
lightrag/prompt.py CHANGED
@@ -236,7 +236,7 @@ Given the query and conversation history, list both high-level and low-level key
236
  ---Instructions---
237
 
238
  - Consider both the current query and relevant conversation history when extracting keywords
239
- - Output the keywords in JSON format
240
  - The JSON should have two keys:
241
  - "high_level_keywords" for overarching concepts or themes
242
  - "low_level_keywords" for specific entities or details
 
236
  ---Instructions---
237
 
238
  - Consider both the current query and relevant conversation history when extracting keywords
239
+ - Output the keywords in JSON format, it will be parsed by a JSON parser, do not add any extra content in output
240
  - The JSON should have two keys:
241
  - "high_level_keywords" for overarching concepts or themes
242
  - "low_level_keywords" for specific entities or details
lightrag/utils.py CHANGED
@@ -890,3 +890,52 @@ def lazy_external_import(module_name: str, class_name: str) -> Callable[..., Any
890
  return cls(*args, **kwargs)
891
 
892
  return import_class
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  return cls(*args, **kwargs)
891
 
892
  return import_class
893
+
894
+
895
+ def get_content_summary(content: str, max_length: int = 100) -> str:
896
+ """Get summary of document content
897
+
898
+ Args:
899
+ content: Original document content
900
+ max_length: Maximum length of summary
901
+
902
+ Returns:
903
+ Truncated content with ellipsis if needed
904
+ """
905
+ content = content.strip()
906
+ if len(content) <= max_length:
907
+ return content
908
+ return content[:max_length] + "..."
909
+
910
+
911
+ def clean_text(text: str) -> str:
912
+ """Clean text by removing null bytes (0x00) and whitespace
913
+
914
+ Args:
915
+ text: Input text to clean
916
+
917
+ Returns:
918
+ Cleaned text
919
+ """
920
+ return text.strip().replace("\x00", "")
921
+
922
+
923
+ def check_storage_env_vars(storage_name: str) -> None:
924
+ """Check if all required environment variables for storage implementation exist
925
+
926
+ Args:
927
+ storage_name: Storage implementation name
928
+
929
+ Raises:
930
+ ValueError: If required environment variables are missing
931
+ """
932
+ from lightrag.kg import STORAGE_ENV_REQUIREMENTS
933
+
934
+ required_vars = STORAGE_ENV_REQUIREMENTS.get(storage_name, [])
935
+ missing_vars = [var for var in required_vars if var not in os.environ]
936
+
937
+ if missing_vars:
938
+ raise ValueError(
939
+ f"Storage implementation '{storage_name}' requires the following "
940
+ f"environment variables: {', '.join(missing_vars)}"
941
+ )
lightrag_webui/bun.lock CHANGED
@@ -34,11 +34,13 @@
34
  "cmdk": "^1.0.4",
35
  "graphology": "^0.26.0",
36
  "graphology-generators": "^0.11.2",
 
37
  "lucide-react": "^0.475.0",
38
  "minisearch": "^7.1.2",
39
  "react": "^19.0.0",
40
  "react-dom": "^19.0.0",
41
  "react-dropzone": "^14.3.6",
 
42
  "react-markdown": "^9.1.0",
43
  "react-number-format": "^5.4.3",
44
  "react-router-dom": "^7.3.0",
@@ -770,8 +772,12 @@
770
 
771
  "hoist-non-react-statics": ["[email protected]", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
772
 
 
 
773
  "html-url-attributes": ["[email protected]", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
774
 
 
 
775
  "ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
776
 
777
  "import-fresh": ["[email protected]", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -1098,6 +1104,8 @@
1098
 
1099
  "react-dropzone": ["[email protected]", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-U792j+x0rcwH/U/Slv/OBNU/LGFYbDLHKKiJoPhNaOianayZevCt4Y5S0CraPssH/6/wT6xhKDfzdXUgCBS0HQ=="],
1100
 
 
 
1101
  "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
1102
 
1103
  "react-markdown": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="],
@@ -1284,6 +1292,8 @@
1284
 
1285
  "vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA=="],
1286
 
 
 
1287
  "which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
1288
 
1289
  "which-boxed-primitive": ["[email protected]", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
 
34
  "cmdk": "^1.0.4",
35
  "graphology": "^0.26.0",
36
  "graphology-generators": "^0.11.2",
37
+ "i18next": "^24.2.2",
38
  "lucide-react": "^0.475.0",
39
  "minisearch": "^7.1.2",
40
  "react": "^19.0.0",
41
  "react-dom": "^19.0.0",
42
  "react-dropzone": "^14.3.6",
43
+ "react-i18next": "^15.4.1",
44
  "react-markdown": "^9.1.0",
45
  "react-number-format": "^5.4.3",
46
  "react-router-dom": "^7.3.0",
 
772
 
773
  "hoist-non-react-statics": ["[email protected]", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
774
 
775
+ "html-parse-stringify": ["[email protected]", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
776
+
777
  "html-url-attributes": ["[email protected]", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
778
 
779
+ "i18next": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="],
780
+
781
  "ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
782
 
783
  "import-fresh": ["[email protected]", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
 
1104
 
1105
  "react-dropzone": ["[email protected]", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-U792j+x0rcwH/U/Slv/OBNU/LGFYbDLHKKiJoPhNaOianayZevCt4Y5S0CraPssH/6/wT6xhKDfzdXUgCBS0HQ=="],
1106
 
1107
+ "react-i18next": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="],
1108
+
1109
  "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
1110
 
1111
  "react-markdown": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="],
 
1292
 
1293
  "vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA=="],
1294
 
1295
+ "void-elements": ["[email protected]", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
1296
+
1297
  "which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
1298
 
1299
  "which-boxed-primitive": ["[email protected]", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
lightrag_webui/package.json CHANGED
@@ -43,11 +43,13 @@
43
  "cmdk": "^1.0.4",
44
  "graphology": "^0.26.0",
45
  "graphology-generators": "^0.11.2",
 
46
  "lucide-react": "^0.475.0",
47
  "minisearch": "^7.1.2",
48
  "react": "^19.0.0",
49
  "react-dom": "^19.0.0",
50
  "react-dropzone": "^14.3.6",
 
51
  "react-markdown": "^9.1.0",
52
  "react-number-format": "^5.4.3",
53
  "react-router-dom": "^7.3.0",
 
43
  "cmdk": "^1.0.4",
44
  "graphology": "^0.26.0",
45
  "graphology-generators": "^0.11.2",
46
+ "i18next": "^24.2.2",
47
  "lucide-react": "^0.475.0",
48
  "minisearch": "^7.1.2",
49
  "react": "^19.0.0",
50
  "react-dom": "^19.0.0",
51
  "react-dropzone": "^14.3.6",
52
+ "react-i18next": "^15.4.1",
53
  "react-markdown": "^9.1.0",
54
  "react-number-format": "^5.4.3",
55
  "react-router-dom": "^7.3.0",
lightrag_webui/src/components/ThemeToggle.tsx CHANGED
@@ -3,6 +3,7 @@ import useTheme from '@/hooks/useTheme'
3
  import { MoonIcon, SunIcon } from 'lucide-react'
4
  import { useCallback } from 'react'
5
  import { controlButtonVariant } from '@/lib/constants'
 
6
 
7
  /**
8
  * Component that toggles the theme between light and dark.
@@ -11,13 +12,14 @@ export default function ThemeToggle() {
11
  const { theme, setTheme } = useTheme()
12
  const setLight = useCallback(() => setTheme('light'), [setTheme])
13
  const setDark = useCallback(() => setTheme('dark'), [setTheme])
 
14
 
15
  if (theme === 'dark') {
16
  return (
17
  <Button
18
  onClick={setLight}
19
  variant={controlButtonVariant}
20
- tooltip="Switch to light theme"
21
  size="icon"
22
  side="bottom"
23
  >
@@ -29,7 +31,7 @@ export default function ThemeToggle() {
29
  <Button
30
  onClick={setDark}
31
  variant={controlButtonVariant}
32
- tooltip="Switch to dark theme"
33
  size="icon"
34
  side="bottom"
35
  >
 
3
  import { MoonIcon, SunIcon } from 'lucide-react'
4
  import { useCallback } from 'react'
5
  import { controlButtonVariant } from '@/lib/constants'
6
+ import { useTranslation } from 'react-i18next'
7
 
8
  /**
9
  * Component that toggles the theme between light and dark.
 
12
  const { theme, setTheme } = useTheme()
13
  const setLight = useCallback(() => setTheme('light'), [setTheme])
14
  const setDark = useCallback(() => setTheme('dark'), [setTheme])
15
+ const { t } = useTranslation()
16
 
17
  if (theme === 'dark') {
18
  return (
19
  <Button
20
  onClick={setLight}
21
  variant={controlButtonVariant}
22
+ tooltip={t('header.themeToggle.switchToLight')}
23
  size="icon"
24
  side="bottom"
25
  >
 
31
  <Button
32
  onClick={setDark}
33
  variant={controlButtonVariant}
34
+ tooltip={t('header.themeToggle.switchToDark')}
35
  size="icon"
36
  side="bottom"
37
  >
lightrag_webui/src/components/documents/ClearDocumentsDialog.tsx CHANGED
@@ -13,38 +13,40 @@ import { errorMessage } from '@/lib/utils'
13
  import { clearDocuments } from '@/api/lightrag'
14
 
15
  import { EraserIcon } from 'lucide-react'
 
16
 
17
  export default function ClearDocumentsDialog() {
 
18
  const [open, setOpen] = useState(false)
19
 
20
  const handleClear = useCallback(async () => {
21
  try {
22
  const result = await clearDocuments()
23
  if (result.status === 'success') {
24
- toast.success('Documents cleared successfully')
25
  setOpen(false)
26
  } else {
27
- toast.error(`Clear Documents Failed:\n${result.message}`)
28
  }
29
  } catch (err) {
30
- toast.error('Clear Documents Failed:\n' + errorMessage(err))
31
  }
32
  }, [setOpen])
33
 
34
  return (
35
  <Dialog open={open} onOpenChange={setOpen}>
36
  <DialogTrigger asChild>
37
- <Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
38
- <EraserIcon/> Clear
39
  </Button>
40
  </DialogTrigger>
41
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
42
  <DialogHeader>
43
- <DialogTitle>Clear documents</DialogTitle>
44
- <DialogDescription>Do you really want to clear all documents?</DialogDescription>
45
  </DialogHeader>
46
  <Button variant="destructive" onClick={handleClear}>
47
- YES
48
  </Button>
49
  </DialogContent>
50
  </Dialog>
 
13
  import { clearDocuments } from '@/api/lightrag'
14
 
15
  import { EraserIcon } from 'lucide-react'
16
+ import { useTranslation } from 'react-i18next'
17
 
18
  export default function ClearDocumentsDialog() {
19
+ const { t } = useTranslation()
20
  const [open, setOpen] = useState(false)
21
 
22
  const handleClear = useCallback(async () => {
23
  try {
24
  const result = await clearDocuments()
25
  if (result.status === 'success') {
26
+ toast.success(t('documentPanel.clearDocuments.success'))
27
  setOpen(false)
28
  } else {
29
+ toast.error(t('documentPanel.clearDocuments.failed', { message: result.message }))
30
  }
31
  } catch (err) {
32
+ toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
33
  }
34
  }, [setOpen])
35
 
36
  return (
37
  <Dialog open={open} onOpenChange={setOpen}>
38
  <DialogTrigger asChild>
39
+ <Button variant="outline" side="bottom" tooltip={t('documentPanel.clearDocuments.tooltip')} size="sm">
40
+ <EraserIcon/> {t('documentPanel.clearDocuments.button')}
41
  </Button>
42
  </DialogTrigger>
43
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
44
  <DialogHeader>
45
+ <DialogTitle>{t('documentPanel.clearDocuments.title')}</DialogTitle>
46
+ <DialogDescription>{t('documentPanel.clearDocuments.confirm')}</DialogDescription>
47
  </DialogHeader>
48
  <Button variant="destructive" onClick={handleClear}>
49
+ {t('documentPanel.clearDocuments.confirmButton')}
50
  </Button>
51
  </DialogContent>
52
  </Dialog>
lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx CHANGED
@@ -14,8 +14,10 @@ import { errorMessage } from '@/lib/utils'
14
  import { uploadDocument } from '@/api/lightrag'
15
 
16
  import { UploadIcon } from 'lucide-react'
 
17
 
18
  export default function UploadDocumentsDialog() {
 
19
  const [open, setOpen] = useState(false)
20
  const [isUploading, setIsUploading] = useState(false)
21
  const [progresses, setProgresses] = useState<Record<string, number>>({})
@@ -29,24 +31,24 @@ export default function UploadDocumentsDialog() {
29
  filesToUpload.map(async (file) => {
30
  try {
31
  const result = await uploadDocument(file, (percentCompleted: number) => {
32
- console.debug(`Uploading ${file.name}: ${percentCompleted}%`)
33
  setProgresses((pre) => ({
34
  ...pre,
35
  [file.name]: percentCompleted
36
  }))
37
  })
38
  if (result.status === 'success') {
39
- toast.success(`Upload Success:\n${file.name} uploaded successfully`)
40
  } else {
41
- toast.error(`Upload Failed:\n${file.name}\n${result.message}`)
42
  }
43
  } catch (err) {
44
- toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`)
45
  }
46
  })
47
  )
48
  } catch (err) {
49
- toast.error('Upload Failed\n' + errorMessage(err))
50
  } finally {
51
  setIsUploading(false)
52
  // setOpen(false)
@@ -66,21 +68,21 @@ export default function UploadDocumentsDialog() {
66
  }}
67
  >
68
  <DialogTrigger asChild>
69
- <Button variant="default" side="bottom" tooltip="Upload documents" size="sm">
70
- <UploadIcon /> Upload
71
  </Button>
72
  </DialogTrigger>
73
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
74
  <DialogHeader>
75
- <DialogTitle>Upload documents</DialogTitle>
76
  <DialogDescription>
77
- Drag and drop your documents here or click to browse.
78
  </DialogDescription>
79
  </DialogHeader>
80
  <FileUploader
81
  maxFileCount={Infinity}
82
  maxSize={200 * 1024 * 1024}
83
- description="supported types: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
84
  onUpload={handleDocumentsUpload}
85
  progresses={progresses}
86
  disabled={isUploading}
 
14
  import { uploadDocument } from '@/api/lightrag'
15
 
16
  import { UploadIcon } from 'lucide-react'
17
+ import { useTranslation } from 'react-i18next'
18
 
19
  export default function UploadDocumentsDialog() {
20
+ const { t } = useTranslation()
21
  const [open, setOpen] = useState(false)
22
  const [isUploading, setIsUploading] = useState(false)
23
  const [progresses, setProgresses] = useState<Record<string, number>>({})
 
31
  filesToUpload.map(async (file) => {
32
  try {
33
  const result = await uploadDocument(file, (percentCompleted: number) => {
34
+ console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted }))
35
  setProgresses((pre) => ({
36
  ...pre,
37
  [file.name]: percentCompleted
38
  }))
39
  })
40
  if (result.status === 'success') {
41
+ toast.success(t('documentPanel.uploadDocuments.success', { name: file.name }))
42
  } else {
43
+ toast.error(t('documentPanel.uploadDocuments.failed', { name: file.name, message: result.message }))
44
  }
45
  } catch (err) {
46
+ toast.error(t('documentPanel.uploadDocuments.error', { name: file.name, error: errorMessage(err) }))
47
  }
48
  })
49
  )
50
  } catch (err) {
51
+ toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }))
52
  } finally {
53
  setIsUploading(false)
54
  // setOpen(false)
 
68
  }}
69
  >
70
  <DialogTrigger asChild>
71
+ <Button variant="default" side="bottom" tooltip={t('documentPanel.uploadDocuments.tooltip')} size="sm">
72
+ <UploadIcon /> {t('documentPanel.uploadDocuments.button')}
73
  </Button>
74
  </DialogTrigger>
75
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
76
  <DialogHeader>
77
+ <DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle>
78
  <DialogDescription>
79
+ {t('documentPanel.uploadDocuments.description')}
80
  </DialogDescription>
81
  </DialogHeader>
82
  <FileUploader
83
  maxFileCount={Infinity}
84
  maxSize={200 * 1024 * 1024}
85
+ description={t('documentPanel.uploadDocuments.fileTypes')}
86
  onUpload={handleDocumentsUpload}
87
  progresses={progresses}
88
  disabled={isUploading}
lightrag_webui/src/components/graph/FullScreenControl.tsx CHANGED
@@ -2,21 +2,23 @@ import { useFullScreen } from '@react-sigma/core'
2
  import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
3
  import { controlButtonVariant } from '@/lib/constants'
4
  import Button from '@/components/ui/Button'
 
5
 
6
  /**
7
  * Component that toggles full screen mode.
8
  */
9
  const FullScreenControl = () => {
10
  const { isFullScreen, toggle } = useFullScreen()
 
11
 
12
  return (
13
  <>
14
  {isFullScreen ? (
15
- <Button variant={controlButtonVariant} onClick={toggle} tooltip="Windowed" size="icon">
16
  <MinimizeIcon />
17
  </Button>
18
  ) : (
19
- <Button variant={controlButtonVariant} onClick={toggle} tooltip="Full Screen" size="icon">
20
  <MaximizeIcon />
21
  </Button>
22
  )}
 
2
  import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
3
  import { controlButtonVariant } from '@/lib/constants'
4
  import Button from '@/components/ui/Button'
5
+ import { useTranslation } from 'react-i18next'
6
 
7
  /**
8
  * Component that toggles full screen mode.
9
  */
10
  const FullScreenControl = () => {
11
  const { isFullScreen, toggle } = useFullScreen()
12
+ const { t } = useTranslation()
13
 
14
  return (
15
  <>
16
  {isFullScreen ? (
17
+ <Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.windowed')} size="icon">
18
  <MinimizeIcon />
19
  </Button>
20
  ) : (
21
+ <Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.fullScreen')} size="icon">
22
  <MaximizeIcon />
23
  </Button>
24
  )}
lightrag_webui/src/components/graph/GraphLabels.tsx CHANGED
@@ -5,6 +5,7 @@ import { useSettingsStore } from '@/stores/settings'
5
  import { useGraphStore } from '@/stores/graph'
6
  import { labelListLimit } from '@/lib/constants'
7
  import MiniSearch from 'minisearch'
 
8
 
9
  const lastGraph: any = {
10
  graph: null,
@@ -13,6 +14,7 @@ const lastGraph: any = {
13
  }
14
 
15
  const GraphLabels = () => {
 
16
  const label = useSettingsStore.use.queryLabel()
17
  const graph = useGraphStore.use.sigmaGraph()
18
 
@@ -69,7 +71,7 @@ const GraphLabels = () => {
69
 
70
  return result.length <= labelListLimit
71
  ? result
72
- : [...result.slice(0, labelListLimit), `And ${result.length - labelListLimit} others`]
73
  },
74
  [getSearchEngine]
75
  )
@@ -84,14 +86,14 @@ const GraphLabels = () => {
84
  className="ml-2"
85
  triggerClassName="max-h-8"
86
  searchInputClassName="max-h-8"
87
- triggerTooltip="Select query label"
88
  fetcher={fetchData}
89
  renderOption={(item) => <div>{item}</div>}
90
  getOptionValue={(item) => item}
91
  getDisplayValue={(item) => <div>{item}</div>}
92
  notFound={<div className="py-6 text-center text-sm">No labels found</div>}
93
- label="Label"
94
- placeholder="Search labels..."
95
  value={label !== null ? label : ''}
96
  onChange={setQueryLabel}
97
  />
 
5
  import { useGraphStore } from '@/stores/graph'
6
  import { labelListLimit } from '@/lib/constants'
7
  import MiniSearch from 'minisearch'
8
+ import { useTranslation } from 'react-i18next'
9
 
10
  const lastGraph: any = {
11
  graph: null,
 
14
  }
15
 
16
  const GraphLabels = () => {
17
+ const { t } = useTranslation()
18
  const label = useSettingsStore.use.queryLabel()
19
  const graph = useGraphStore.use.sigmaGraph()
20
 
 
71
 
72
  return result.length <= labelListLimit
73
  ? result
74
+ : [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
75
  },
76
  [getSearchEngine]
77
  )
 
86
  className="ml-2"
87
  triggerClassName="max-h-8"
88
  searchInputClassName="max-h-8"
89
+ triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
90
  fetcher={fetchData}
91
  renderOption={(item) => <div>{item}</div>}
92
  getOptionValue={(item) => item}
93
  getDisplayValue={(item) => <div>{item}</div>}
94
  notFound={<div className="py-6 text-center text-sm">No labels found</div>}
95
+ label={t('graphPanel.graphLabels.label')}
96
+ placeholder={t('graphPanel.graphLabels.placeholder')}
97
  value={label !== null ? label : ''}
98
  onChange={setQueryLabel}
99
  />
lightrag_webui/src/components/graph/GraphSearch.tsx CHANGED
@@ -9,6 +9,7 @@ import { AsyncSearch } from '@/components/ui/AsyncSearch'
9
  import { searchResultLimit } from '@/lib/constants'
10
  import { useGraphStore } from '@/stores/graph'
11
  import MiniSearch from 'minisearch'
 
12
 
13
  interface OptionItem {
14
  id: string
@@ -44,6 +45,7 @@ export const GraphSearchInput = ({
44
  onFocus?: GraphSearchInputProps['onFocus']
45
  value?: GraphSearchInputProps['value']
46
  }) => {
 
47
  const graph = useGraphStore.use.sigmaGraph()
48
 
49
  const searchEngine = useMemo(() => {
@@ -97,7 +99,7 @@ export const GraphSearchInput = ({
97
  {
98
  type: 'message',
99
  id: messageId,
100
- message: `And ${result.length - searchResultLimit} others`
101
  }
102
  ]
103
  },
@@ -118,7 +120,7 @@ export const GraphSearchInput = ({
118
  if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
119
  }}
120
  label={'item'}
121
- placeholder="Search nodes..."
122
  />
123
  )
124
  }
 
9
  import { searchResultLimit } from '@/lib/constants'
10
  import { useGraphStore } from '@/stores/graph'
11
  import MiniSearch from 'minisearch'
12
+ import { useTranslation } from 'react-i18next'
13
 
14
  interface OptionItem {
15
  id: string
 
45
  onFocus?: GraphSearchInputProps['onFocus']
46
  value?: GraphSearchInputProps['value']
47
  }) => {
48
+ const { t } = useTranslation()
49
  const graph = useGraphStore.use.sigmaGraph()
50
 
51
  const searchEngine = useMemo(() => {
 
99
  {
100
  type: 'message',
101
  id: messageId,
102
+ message: t('graphPanel.search.message', { count: result.length - searchResultLimit })
103
  }
104
  ]
105
  },
 
120
  if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
121
  }}
122
  label={'item'}
123
+ placeholder={t('graphPanel.search.placeholder')}
124
  />
125
  )
126
  }
lightrag_webui/src/components/graph/LayoutsControl.tsx CHANGED
@@ -16,6 +16,7 @@ import { controlButtonVariant } from '@/lib/constants'
16
  import { useSettingsStore } from '@/stores/settings'
17
 
18
  import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
 
19
 
20
  type LayoutName =
21
  | 'Circular'
@@ -28,6 +29,7 @@ type LayoutName =
28
  const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
29
  const sigma = useSigma()
30
  const { stop, start, isRunning } = layout
 
31
 
32
  /**
33
  * Init component when Sigma or component settings change.
@@ -61,7 +63,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
61
  <Button
62
  size="icon"
63
  onClick={() => (isRunning ? stop() : start())}
64
- tooltip={isRunning ? 'Stop the layout animation' : 'Start the layout animation'}
65
  variant={controlButtonVariant}
66
  >
67
  {isRunning ? <PauseIcon /> : <PlayIcon />}
@@ -74,6 +76,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
74
  */
75
  const LayoutsControl = () => {
76
  const sigma = useSigma()
 
77
  const [layout, setLayout] = useState<LayoutName>('Circular')
78
  const [opened, setOpened] = useState<boolean>(false)
79
 
@@ -149,7 +152,7 @@ const LayoutsControl = () => {
149
  size="icon"
150
  variant={controlButtonVariant}
151
  onClick={() => setOpened((e: boolean) => !e)}
152
- tooltip="Layout Graph"
153
  >
154
  <GripIcon />
155
  </Button>
@@ -166,7 +169,7 @@ const LayoutsControl = () => {
166
  key={name}
167
  className="cursor-pointer text-xs"
168
  >
169
- {name}
170
  </CommandItem>
171
  ))}
172
  </CommandGroup>
 
16
  import { useSettingsStore } from '@/stores/settings'
17
 
18
  import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
19
+ import { useTranslation } from 'react-i18next'
20
 
21
  type LayoutName =
22
  | 'Circular'
 
29
  const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
30
  const sigma = useSigma()
31
  const { stop, start, isRunning } = layout
32
+ const { t } = useTranslation()
33
 
34
  /**
35
  * Init component when Sigma or component settings change.
 
63
  <Button
64
  size="icon"
65
  onClick={() => (isRunning ? stop() : start())}
66
+ tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
67
  variant={controlButtonVariant}
68
  >
69
  {isRunning ? <PauseIcon /> : <PlayIcon />}
 
76
  */
77
  const LayoutsControl = () => {
78
  const sigma = useSigma()
79
+ const { t } = useTranslation()
80
  const [layout, setLayout] = useState<LayoutName>('Circular')
81
  const [opened, setOpened] = useState<boolean>(false)
82
 
 
152
  size="icon"
153
  variant={controlButtonVariant}
154
  onClick={() => setOpened((e: boolean) => !e)}
155
+ tooltip={t('graphPanel.sideBar.layoutsControl.layoutGraph')}
156
  >
157
  <GripIcon />
158
  </Button>
 
169
  key={name}
170
  className="cursor-pointer text-xs"
171
  >
172
+ {t(`graphPanel.sideBar.layoutsControl.layouts.${name}`)}
173
  </CommandItem>
174
  ))}
175
  </CommandGroup>
lightrag_webui/src/components/graph/PropertiesView.tsx CHANGED
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
2
  import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
3
  import Text from '@/components/ui/Text'
4
  import useLightragGraph from '@/hooks/useLightragGraph'
 
5
 
6
  /**
7
  * Component that view properties of elements in graph.
@@ -147,21 +148,22 @@ const PropertyRow = ({
147
  }
148
 
149
  const NodePropertiesView = ({ node }: { node: NodeType }) => {
 
150
  return (
151
  <div className="flex flex-col gap-2">
152
- <label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
153
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
154
- <PropertyRow name={'Id'} value={node.id} />
155
  <PropertyRow
156
- name={'Labels'}
157
  value={node.labels.join(', ')}
158
  onClick={() => {
159
  useGraphStore.getState().setSelectedNode(node.id, true)
160
  }}
161
  />
162
- <PropertyRow name={'Degree'} value={node.degree} />
163
  </div>
164
- <label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
165
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
166
  {Object.keys(node.properties)
167
  .sort()
@@ -172,7 +174,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
172
  {node.relationships.length > 0 && (
173
  <>
174
  <label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
175
- Relationships
176
  </label>
177
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
178
  {node.relationships.map(({ type, id, label }) => {
@@ -195,28 +197,29 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
195
  }
196
 
197
  const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
 
198
  return (
199
  <div className="flex flex-col gap-2">
200
- <label className="text-md pl-1 font-bold tracking-wide text-teal-600">Relationship</label>
201
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
202
- <PropertyRow name={'Id'} value={edge.id} />
203
- {edge.type && <PropertyRow name={'Type'} value={edge.type} />}
204
  <PropertyRow
205
- name={'Source'}
206
  value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
207
  onClick={() => {
208
  useGraphStore.getState().setSelectedNode(edge.source, true)
209
  }}
210
  />
211
  <PropertyRow
212
- name={'Target'}
213
  value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
214
  onClick={() => {
215
  useGraphStore.getState().setSelectedNode(edge.target, true)
216
  }}
217
  />
218
  </div>
219
- <label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
220
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
221
  {Object.keys(edge.properties)
222
  .sort()
 
2
  import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
3
  import Text from '@/components/ui/Text'
4
  import useLightragGraph from '@/hooks/useLightragGraph'
5
+ import { useTranslation } from 'react-i18next'
6
 
7
  /**
8
  * Component that view properties of elements in graph.
 
148
  }
149
 
150
  const NodePropertiesView = ({ node }: { node: NodeType }) => {
151
+ const { t } = useTranslation()
152
  return (
153
  <div className="flex flex-col gap-2">
154
+ <label className="text-md pl-1 font-bold tracking-wide text-sky-300">{t('graphPanel.propertiesView.node.title')}</label>
155
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
156
+ <PropertyRow name={t('graphPanel.propertiesView.node.id')} value={node.id} />
157
  <PropertyRow
158
+ name={t('graphPanel.propertiesView.node.labels')}
159
  value={node.labels.join(', ')}
160
  onClick={() => {
161
  useGraphStore.getState().setSelectedNode(node.id, true)
162
  }}
163
  />
164
+ <PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} />
165
  </div>
166
+ <label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.node.properties')}</label>
167
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
168
  {Object.keys(node.properties)
169
  .sort()
 
174
  {node.relationships.length > 0 && (
175
  <>
176
  <label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
177
+ {t('graphPanel.propertiesView.node.relationships')}
178
  </label>
179
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
180
  {node.relationships.map(({ type, id, label }) => {
 
197
  }
198
 
199
  const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
200
+ const { t } = useTranslation()
201
  return (
202
  <div className="flex flex-col gap-2">
203
+ <label className="text-md pl-1 font-bold tracking-wide text-teal-600">{t('graphPanel.propertiesView.edge.title')}</label>
204
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
205
+ <PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} />
206
+ {edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />}
207
  <PropertyRow
208
+ name={t('graphPanel.propertiesView.edge.source')}
209
  value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
210
  onClick={() => {
211
  useGraphStore.getState().setSelectedNode(edge.source, true)
212
  }}
213
  />
214
  <PropertyRow
215
+ name={t('graphPanel.propertiesView.edge.target')}
216
  value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
217
  onClick={() => {
218
  useGraphStore.getState().setSelectedNode(edge.target, true)
219
  }}
220
  />
221
  </div>
222
+ <label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.edge.properties')}</label>
223
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
224
  {Object.keys(edge.properties)
225
  .sort()
lightrag_webui/src/components/graph/Settings.tsx CHANGED
@@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings'
10
  import { useBackendState } from '@/stores/state'
11
 
12
  import { SettingsIcon } from 'lucide-react'
 
13
 
14
  /**
15
  * Component that displays a checkbox with a label.
@@ -204,10 +205,12 @@ export default function Settings() {
204
  [setTempApiKey]
205
  )
206
 
 
 
207
  return (
208
  <Popover open={opened} onOpenChange={setOpened}>
209
  <PopoverTrigger asChild>
210
- <Button variant={controlButtonVariant} tooltip="Settings" size="icon">
211
  <SettingsIcon />
212
  </Button>
213
  </PopoverTrigger>
@@ -221,7 +224,7 @@ export default function Settings() {
221
  <LabeledCheckBox
222
  checked={enableHealthCheck}
223
  onCheckedChange={setEnableHealthCheck}
224
- label="Health Check"
225
  />
226
 
227
  <Separator />
@@ -229,12 +232,12 @@ export default function Settings() {
229
  <LabeledCheckBox
230
  checked={showPropertyPanel}
231
  onCheckedChange={setShowPropertyPanel}
232
- label="Show Property Panel"
233
  />
234
  <LabeledCheckBox
235
  checked={showNodeSearchBar}
236
  onCheckedChange={setShowNodeSearchBar}
237
- label="Show Search Bar"
238
  />
239
 
240
  <Separator />
@@ -242,12 +245,12 @@ export default function Settings() {
242
  <LabeledCheckBox
243
  checked={showNodeLabel}
244
  onCheckedChange={setShowNodeLabel}
245
- label="Show Node Label"
246
  />
247
  <LabeledCheckBox
248
  checked={enableNodeDrag}
249
  onCheckedChange={setEnableNodeDrag}
250
- label="Node Draggable"
251
  />
252
 
253
  <Separator />
@@ -255,51 +258,50 @@ export default function Settings() {
255
  <LabeledCheckBox
256
  checked={showEdgeLabel}
257
  onCheckedChange={setShowEdgeLabel}
258
- label="Show Edge Label"
259
  />
260
  <LabeledCheckBox
261
  checked={enableHideUnselectedEdges}
262
  onCheckedChange={setEnableHideUnselectedEdges}
263
- label="Hide Unselected Edges"
264
  />
265
  <LabeledCheckBox
266
  checked={enableEdgeEvents}
267
  onCheckedChange={setEnableEdgeEvents}
268
- label="Edge Events"
269
  />
270
 
271
  <Separator />
272
  <LabeledNumberInput
273
- label="Max Query Depth"
274
  min={1}
275
  value={graphQueryMaxDepth}
276
  onEditFinished={setGraphQueryMaxDepth}
277
  />
278
  <LabeledNumberInput
279
- label="Minimum Degree"
280
  min={0}
281
  value={graphMinDegree}
282
  onEditFinished={setGraphMinDegree}
283
  />
284
  <LabeledNumberInput
285
- label="Max Layout Iterations"
286
  min={1}
287
  max={20}
288
  value={graphLayoutMaxIterations}
289
  onEditFinished={setGraphLayoutMaxIterations}
290
  />
291
-
292
  <Separator />
293
 
294
  <div className="flex flex-col gap-2">
295
- <label className="text-sm font-medium">API Key</label>
296
  <form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
297
  <div className="w-0 flex-1">
298
  <Input
299
  type="password"
300
  value={tempApiKey}
301
  onChange={handleTempApiKeyChange}
302
- placeholder="Enter your API key"
303
  className="max-h-full w-full min-w-0"
304
  autoComplete="off"
305
  />
@@ -310,7 +312,7 @@ export default function Settings() {
310
  size="sm"
311
  className="max-h-full shrink-0"
312
  >
313
- Save
314
  </Button>
315
  </form>
316
  </div>
 
10
  import { useBackendState } from '@/stores/state'
11
 
12
  import { SettingsIcon } from 'lucide-react'
13
+ import { useTranslation } from "react-i18next";
14
 
15
  /**
16
  * Component that displays a checkbox with a label.
 
205
  [setTempApiKey]
206
  )
207
 
208
+ const { t } = useTranslation();
209
+
210
  return (
211
  <Popover open={opened} onOpenChange={setOpened}>
212
  <PopoverTrigger asChild>
213
+ <Button variant={controlButtonVariant} tooltip={t("graphPanel.sideBar.settings.settings")} size="icon">
214
  <SettingsIcon />
215
  </Button>
216
  </PopoverTrigger>
 
224
  <LabeledCheckBox
225
  checked={enableHealthCheck}
226
  onCheckedChange={setEnableHealthCheck}
227
+ label={t("graphPanel.sideBar.settings.healthCheck")}
228
  />
229
 
230
  <Separator />
 
232
  <LabeledCheckBox
233
  checked={showPropertyPanel}
234
  onCheckedChange={setShowPropertyPanel}
235
+ label={t("graphPanel.sideBar.settings.showPropertyPanel")}
236
  />
237
  <LabeledCheckBox
238
  checked={showNodeSearchBar}
239
  onCheckedChange={setShowNodeSearchBar}
240
+ label={t("graphPanel.sideBar.settings.showSearchBar")}
241
  />
242
 
243
  <Separator />
 
245
  <LabeledCheckBox
246
  checked={showNodeLabel}
247
  onCheckedChange={setShowNodeLabel}
248
+ label={t("graphPanel.sideBar.settings.showNodeLabel")}
249
  />
250
  <LabeledCheckBox
251
  checked={enableNodeDrag}
252
  onCheckedChange={setEnableNodeDrag}
253
+ label={t("graphPanel.sideBar.settings.nodeDraggable")}
254
  />
255
 
256
  <Separator />
 
258
  <LabeledCheckBox
259
  checked={showEdgeLabel}
260
  onCheckedChange={setShowEdgeLabel}
261
+ label={t("graphPanel.sideBar.settings.showEdgeLabel")}
262
  />
263
  <LabeledCheckBox
264
  checked={enableHideUnselectedEdges}
265
  onCheckedChange={setEnableHideUnselectedEdges}
266
+ label={t("graphPanel.sideBar.settings.hideUnselectedEdges")}
267
  />
268
  <LabeledCheckBox
269
  checked={enableEdgeEvents}
270
  onCheckedChange={setEnableEdgeEvents}
271
+ label={t("graphPanel.sideBar.settings.edgeEvents")}
272
  />
273
 
274
  <Separator />
275
  <LabeledNumberInput
276
+ label={t("graphPanel.sideBar.settings.maxQueryDepth")}
277
  min={1}
278
  value={graphQueryMaxDepth}
279
  onEditFinished={setGraphQueryMaxDepth}
280
  />
281
  <LabeledNumberInput
282
+ label={t("graphPanel.sideBar.settings.minDegree")}
283
  min={0}
284
  value={graphMinDegree}
285
  onEditFinished={setGraphMinDegree}
286
  />
287
  <LabeledNumberInput
288
+ label={t("graphPanel.sideBar.settings.maxLayoutIterations")}
289
  min={1}
290
  max={20}
291
  value={graphLayoutMaxIterations}
292
  onEditFinished={setGraphLayoutMaxIterations}
293
  />
 
294
  <Separator />
295
 
296
  <div className="flex flex-col gap-2">
297
+ <label className="text-sm font-medium">{t("graphPanel.sideBar.settings.apiKey")}</label>
298
  <form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
299
  <div className="w-0 flex-1">
300
  <Input
301
  type="password"
302
  value={tempApiKey}
303
  onChange={handleTempApiKeyChange}
304
+ placeholder={t("graphPanel.sideBar.settings.enterYourAPIkey")}
305
  className="max-h-full w-full min-w-0"
306
  autoComplete="off"
307
  />
 
312
  size="sm"
313
  className="max-h-full shrink-0"
314
  >
315
+ {t("graphPanel.sideBar.settings.save")}
316
  </Button>
317
  </form>
318
  </div>
lightrag_webui/src/components/graph/StatusCard.tsx CHANGED
@@ -1,58 +1,60 @@
1
  import { LightragStatus } from '@/api/lightrag'
 
2
 
3
  const StatusCard = ({ status }: { status: LightragStatus | null }) => {
 
4
  if (!status) {
5
- return <div className="text-muted-foreground text-sm">Status information unavailable</div>
6
  }
7
 
8
  return (
9
  <div className="min-w-[300px] space-y-3 text-sm">
10
  <div className="space-y-1">
11
- <h4 className="font-medium">Storage Info</h4>
12
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
13
- <span>Working Directory:</span>
14
  <span className="truncate">{status.working_directory}</span>
15
- <span>Input Directory:</span>
16
  <span className="truncate">{status.input_directory}</span>
17
  </div>
18
  </div>
19
 
20
  <div className="space-y-1">
21
- <h4 className="font-medium">LLM Configuration</h4>
22
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
23
- <span>LLM Binding:</span>
24
  <span>{status.configuration.llm_binding}</span>
25
- <span>LLM Binding Host:</span>
26
  <span>{status.configuration.llm_binding_host}</span>
27
- <span>LLM Model:</span>
28
  <span>{status.configuration.llm_model}</span>
29
- <span>Max Tokens:</span>
30
  <span>{status.configuration.max_tokens}</span>
31
  </div>
32
  </div>
33
 
34
  <div className="space-y-1">
35
- <h4 className="font-medium">Embedding Configuration</h4>
36
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
37
- <span>Embedding Binding:</span>
38
  <span>{status.configuration.embedding_binding}</span>
39
- <span>Embedding Binding Host:</span>
40
  <span>{status.configuration.embedding_binding_host}</span>
41
- <span>Embedding Model:</span>
42
  <span>{status.configuration.embedding_model}</span>
43
  </div>
44
  </div>
45
 
46
  <div className="space-y-1">
47
- <h4 className="font-medium">Storage Configuration</h4>
48
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
49
- <span>KV Storage:</span>
50
  <span>{status.configuration.kv_storage}</span>
51
- <span>Doc Status Storage:</span>
52
  <span>{status.configuration.doc_status_storage}</span>
53
- <span>Graph Storage:</span>
54
  <span>{status.configuration.graph_storage}</span>
55
- <span>Vector Storage:</span>
56
  <span>{status.configuration.vector_storage}</span>
57
  </div>
58
  </div>
 
1
  import { LightragStatus } from '@/api/lightrag'
2
+ import { useTranslation } from 'react-i18next'
3
 
4
  const StatusCard = ({ status }: { status: LightragStatus | null }) => {
5
+ const { t } = useTranslation()
6
  if (!status) {
7
+ return <div className="text-muted-foreground text-sm">{t('graphPanel.statusCard.unavailable')}</div>
8
  }
9
 
10
  return (
11
  <div className="min-w-[300px] space-y-3 text-sm">
12
  <div className="space-y-1">
13
+ <h4 className="font-medium">{t('graphPanel.statusCard.storageInfo')}</h4>
14
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
15
+ <span>{t('graphPanel.statusCard.workingDirectory')}:</span>
16
  <span className="truncate">{status.working_directory}</span>
17
+ <span>{t('graphPanel.statusCard.inputDirectory')}:</span>
18
  <span className="truncate">{status.input_directory}</span>
19
  </div>
20
  </div>
21
 
22
  <div className="space-y-1">
23
+ <h4 className="font-medium">{t('graphPanel.statusCard.llmConfig')}</h4>
24
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
25
+ <span>{t('graphPanel.statusCard.llmBinding')}:</span>
26
  <span>{status.configuration.llm_binding}</span>
27
+ <span>{t('graphPanel.statusCard.llmBindingHost')}:</span>
28
  <span>{status.configuration.llm_binding_host}</span>
29
+ <span>{t('graphPanel.statusCard.llmModel')}:</span>
30
  <span>{status.configuration.llm_model}</span>
31
+ <span>{t('graphPanel.statusCard.maxTokens')}:</span>
32
  <span>{status.configuration.max_tokens}</span>
33
  </div>
34
  </div>
35
 
36
  <div className="space-y-1">
37
+ <h4 className="font-medium">{t('graphPanel.statusCard.embeddingConfig')}</h4>
38
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
39
+ <span>{t('graphPanel.statusCard.embeddingBinding')}:</span>
40
  <span>{status.configuration.embedding_binding}</span>
41
+ <span>{t('graphPanel.statusCard.embeddingBindingHost')}:</span>
42
  <span>{status.configuration.embedding_binding_host}</span>
43
+ <span>{t('graphPanel.statusCard.embeddingModel')}:</span>
44
  <span>{status.configuration.embedding_model}</span>
45
  </div>
46
  </div>
47
 
48
  <div className="space-y-1">
49
+ <h4 className="font-medium">{t('graphPanel.statusCard.storageConfig')}</h4>
50
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
51
+ <span>{t('graphPanel.statusCard.kvStorage')}:</span>
52
  <span>{status.configuration.kv_storage}</span>
53
+ <span>{t('graphPanel.statusCard.docStatusStorage')}:</span>
54
  <span>{status.configuration.doc_status_storage}</span>
55
+ <span>{t('graphPanel.statusCard.graphStorage')}:</span>
56
  <span>{status.configuration.graph_storage}</span>
57
+ <span>{t('graphPanel.statusCard.vectorStorage')}:</span>
58
  <span>{status.configuration.vector_storage}</span>
59
  </div>
60
  </div>
lightrag_webui/src/components/graph/StatusIndicator.tsx CHANGED
@@ -3,8 +3,10 @@ import { useBackendState } from '@/stores/state'
3
  import { useEffect, useState } from 'react'
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
5
  import StatusCard from '@/components/graph/StatusCard'
 
6
 
7
  const StatusIndicator = () => {
 
8
  const health = useBackendState.use.health()
9
  const lastCheckTime = useBackendState.use.lastCheckTime()
10
  const status = useBackendState.use.status()
@@ -33,7 +35,7 @@ const StatusIndicator = () => {
33
  )}
34
  />
35
  <span className="text-muted-foreground text-xs">
36
- {health ? 'Connected' : 'Disconnected'}
37
  </span>
38
  </div>
39
  </PopoverTrigger>
 
3
  import { useEffect, useState } from 'react'
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
5
  import StatusCard from '@/components/graph/StatusCard'
6
+ import { useTranslation } from 'react-i18next'
7
 
8
  const StatusIndicator = () => {
9
+ const { t } = useTranslation()
10
  const health = useBackendState.use.health()
11
  const lastCheckTime = useBackendState.use.lastCheckTime()
12
  const status = useBackendState.use.status()
 
35
  )}
36
  />
37
  <span className="text-muted-foreground text-xs">
38
+ {health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')}
39
  </span>
40
  </div>
41
  </PopoverTrigger>
lightrag_webui/src/components/graph/ZoomControl.tsx CHANGED
@@ -3,12 +3,14 @@ import { useCallback } from 'react'
3
  import Button from '@/components/ui/Button'
4
  import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
5
  import { controlButtonVariant } from '@/lib/constants'
 
6
 
7
  /**
8
  * Component that provides zoom controls for the graph viewer.
9
  */
10
  const ZoomControl = () => {
11
  const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
 
12
 
13
  const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
14
  const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
@@ -16,16 +18,16 @@ const ZoomControl = () => {
16
 
17
  return (
18
  <>
19
- <Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip="Zoom In" size="icon">
20
  <ZoomInIcon />
21
  </Button>
22
- <Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip="Zoom Out" size="icon">
23
  <ZoomOutIcon />
24
  </Button>
25
  <Button
26
  variant={controlButtonVariant}
27
  onClick={handleResetZoom}
28
- tooltip="Reset Zoom"
29
  size="icon"
30
  >
31
  <FullscreenIcon />
 
3
  import Button from '@/components/ui/Button'
4
  import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
5
  import { controlButtonVariant } from '@/lib/constants'
6
+ import { useTranslation } from "react-i18next";
7
 
8
  /**
9
  * Component that provides zoom controls for the graph viewer.
10
  */
11
  const ZoomControl = () => {
12
  const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
13
+ const { t } = useTranslation();
14
 
15
  const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
16
  const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
 
18
 
19
  return (
20
  <>
21
+ <Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip={t("graphPanel.sideBar.zoomControl.zoomIn")} size="icon">
22
  <ZoomInIcon />
23
  </Button>
24
+ <Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip={t("graphPanel.sideBar.zoomControl.zoomOut")} size="icon">
25
  <ZoomOutIcon />
26
  </Button>
27
  <Button
28
  variant={controlButtonVariant}
29
  onClick={handleResetZoom}
30
+ tooltip={t("graphPanel.sideBar.zoomControl.resetZoom")}
31
  size="icon"
32
  >
33
  <FullscreenIcon />
lightrag_webui/src/components/retrieval/ChatMessage.tsx CHANGED
@@ -15,18 +15,21 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
15
  import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
16
 
17
  import { LoaderIcon, CopyIcon } from 'lucide-react'
 
18
 
19
  export type MessageWithError = Message & {
20
  isError?: boolean
21
  }
22
 
23
  export const ChatMessage = ({ message }: { message: MessageWithError }) => {
 
 
24
  const handleCopyMarkdown = useCallback(async () => {
25
  if (message.content) {
26
  try {
27
  await navigator.clipboard.writeText(message.content)
28
  } catch (err) {
29
- console.error('Failed to copy:', err)
30
  }
31
  }
32
  }, [message])
@@ -57,7 +60,7 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => {
57
  <Button
58
  onClick={handleCopyMarkdown}
59
  className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100"
60
- tooltip="Copy to clipboard"
61
  variant="default"
62
  size="icon"
63
  >
 
15
  import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
16
 
17
  import { LoaderIcon, CopyIcon } from 'lucide-react'
18
+ import { useTranslation } from 'react-i18next'
19
 
20
  export type MessageWithError = Message & {
21
  isError?: boolean
22
  }
23
 
24
  export const ChatMessage = ({ message }: { message: MessageWithError }) => {
25
+ const { t } = useTranslation()
26
+
27
  const handleCopyMarkdown = useCallback(async () => {
28
  if (message.content) {
29
  try {
30
  await navigator.clipboard.writeText(message.content)
31
  } catch (err) {
32
+ console.error(t('chat.copyError'), err)
33
  }
34
  }
35
  }, [message])
 
60
  <Button
61
  onClick={handleCopyMarkdown}
62
  className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100"
63
+ tooltip={t('retrievePanel.chatMessage.copyTooltip')}
64
  variant="default"
65
  size="icon"
66
  >
lightrag_webui/src/components/retrieval/QuerySettings.tsx CHANGED
@@ -14,8 +14,10 @@ import {
14
  SelectValue
15
  } from '@/components/ui/Select'
16
  import { useSettingsStore } from '@/stores/settings'
 
17
 
18
  export default function QuerySettings() {
 
19
  const querySettings = useSettingsStore((state) => state.querySettings)
20
 
21
  const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
@@ -25,8 +27,8 @@ export default function QuerySettings() {
25
  return (
26
  <Card className="flex shrink-0 flex-col">
27
  <CardHeader className="px-4 pt-4 pb-2">
28
- <CardTitle>Parameters</CardTitle>
29
- <CardDescription>Configure your query parameters</CardDescription>
30
  </CardHeader>
31
  <CardContent className="m-0 flex grow flex-col p-0 text-xs">
32
  <div className="relative size-full">
@@ -35,8 +37,8 @@ export default function QuerySettings() {
35
  <>
36
  <Text
37
  className="ml-1"
38
- text="Query Mode"
39
- tooltip="Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval"
40
  side="left"
41
  />
42
  <Select
@@ -48,11 +50,11 @@ export default function QuerySettings() {
48
  </SelectTrigger>
49
  <SelectContent>
50
  <SelectGroup>
51
- <SelectItem value="naive">Naive</SelectItem>
52
- <SelectItem value="local">Local</SelectItem>
53
- <SelectItem value="global">Global</SelectItem>
54
- <SelectItem value="hybrid">Hybrid</SelectItem>
55
- <SelectItem value="mix">Mix</SelectItem>
56
  </SelectGroup>
57
  </SelectContent>
58
  </Select>
@@ -62,8 +64,8 @@ export default function QuerySettings() {
62
  <>
63
  <Text
64
  className="ml-1"
65
- text="Response Format"
66
- tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
67
  side="left"
68
  />
69
  <Select
@@ -75,9 +77,9 @@ export default function QuerySettings() {
75
  </SelectTrigger>
76
  <SelectContent>
77
  <SelectGroup>
78
- <SelectItem value="Multiple Paragraphs">Multiple Paragraphs</SelectItem>
79
- <SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
80
- <SelectItem value="Bullet Points">Bullet Points</SelectItem>
81
  </SelectGroup>
82
  </SelectContent>
83
  </Select>
@@ -87,8 +89,8 @@ export default function QuerySettings() {
87
  <>
88
  <Text
89
  className="ml-1"
90
- text="Top K Results"
91
- tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
92
  side="left"
93
  />
94
  <NumberInput
@@ -97,7 +99,7 @@ export default function QuerySettings() {
97
  value={querySettings.top_k}
98
  onValueChange={(v) => handleChange('top_k', v)}
99
  min={1}
100
- placeholder="Number of results"
101
  />
102
  </>
103
 
@@ -106,8 +108,8 @@ export default function QuerySettings() {
106
  <>
107
  <Text
108
  className="ml-1"
109
- text="Max Tokens for Text Unit"
110
- tooltip="Maximum number of tokens allowed for each retrieved text chunk"
111
  side="left"
112
  />
113
  <NumberInput
@@ -116,14 +118,14 @@ export default function QuerySettings() {
116
  value={querySettings.max_token_for_text_unit}
117
  onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
118
  min={1}
119
- placeholder="Max tokens for text unit"
120
  />
121
  </>
122
 
123
  <>
124
  <Text
125
- text="Max Tokens for Global Context"
126
- tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
127
  side="left"
128
  />
129
  <NumberInput
@@ -132,15 +134,15 @@ export default function QuerySettings() {
132
  value={querySettings.max_token_for_global_context}
133
  onValueChange={(v) => handleChange('max_token_for_global_context', v)}
134
  min={1}
135
- placeholder="Max tokens for global context"
136
  />
137
  </>
138
 
139
  <>
140
  <Text
141
  className="ml-1"
142
- text="Max Tokens for Local Context"
143
- tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
144
  side="left"
145
  />
146
  <NumberInput
@@ -149,7 +151,7 @@ export default function QuerySettings() {
149
  value={querySettings.max_token_for_local_context}
150
  onValueChange={(v) => handleChange('max_token_for_local_context', v)}
151
  min={1}
152
- placeholder="Max tokens for local context"
153
  />
154
  </>
155
  </>
@@ -158,8 +160,8 @@ export default function QuerySettings() {
158
  <>
159
  <Text
160
  className="ml-1"
161
- text="History Turns"
162
- tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
163
  side="left"
164
  />
165
  <NumberInput
@@ -170,7 +172,7 @@ export default function QuerySettings() {
170
  value={querySettings.history_turns}
171
  onValueChange={(v) => handleChange('history_turns', v)}
172
  min={0}
173
- placeholder="Number of history turns"
174
  />
175
  </>
176
 
@@ -179,8 +181,8 @@ export default function QuerySettings() {
179
  <>
180
  <Text
181
  className="ml-1"
182
- text="High-Level Keywords"
183
- tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
184
  side="left"
185
  />
186
  <Input
@@ -194,15 +196,15 @@ export default function QuerySettings() {
194
  .filter((k) => k !== '')
195
  handleChange('hl_keywords', keywords)
196
  }}
197
- placeholder="Enter keywords"
198
  />
199
  </>
200
 
201
  <>
202
  <Text
203
  className="ml-1"
204
- text="Low-Level Keywords"
205
- tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
206
  side="left"
207
  />
208
  <Input
@@ -216,7 +218,7 @@ export default function QuerySettings() {
216
  .filter((k) => k !== '')
217
  handleChange('ll_keywords', keywords)
218
  }}
219
- placeholder="Enter keywords"
220
  />
221
  </>
222
  </>
@@ -226,8 +228,8 @@ export default function QuerySettings() {
226
  <div className="flex items-center gap-2">
227
  <Text
228
  className="ml-1"
229
- text="Only Need Context"
230
- tooltip="If True, only returns the retrieved context without generating a response"
231
  side="left"
232
  />
233
  <div className="grow" />
@@ -242,8 +244,8 @@ export default function QuerySettings() {
242
  <div className="flex items-center gap-2">
243
  <Text
244
  className="ml-1"
245
- text="Only Need Prompt"
246
- tooltip="If True, only returns the generated prompt without producing a response"
247
  side="left"
248
  />
249
  <div className="grow" />
@@ -258,8 +260,8 @@ export default function QuerySettings() {
258
  <div className="flex items-center gap-2">
259
  <Text
260
  className="ml-1"
261
- text="Stream Response"
262
- tooltip="If True, enables streaming output for real-time responses"
263
  side="left"
264
  />
265
  <div className="grow" />
 
14
  SelectValue
15
  } from '@/components/ui/Select'
16
  import { useSettingsStore } from '@/stores/settings'
17
+ import { useTranslation } from 'react-i18next'
18
 
19
  export default function QuerySettings() {
20
+ const { t } = useTranslation()
21
  const querySettings = useSettingsStore((state) => state.querySettings)
22
 
23
  const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
 
27
  return (
28
  <Card className="flex shrink-0 flex-col">
29
  <CardHeader className="px-4 pt-4 pb-2">
30
+ <CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
31
+ <CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
32
  </CardHeader>
33
  <CardContent className="m-0 flex grow flex-col p-0 text-xs">
34
  <div className="relative size-full">
 
37
  <>
38
  <Text
39
  className="ml-1"
40
+ text={t('retrievePanel.querySettings.queryMode')}
41
+ tooltip={t('retrievePanel.querySettings.queryModeTooltip')}
42
  side="left"
43
  />
44
  <Select
 
50
  </SelectTrigger>
51
  <SelectContent>
52
  <SelectGroup>
53
+ <SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
54
+ <SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
55
+ <SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
56
+ <SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
57
+ <SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
58
  </SelectGroup>
59
  </SelectContent>
60
  </Select>
 
64
  <>
65
  <Text
66
  className="ml-1"
67
+ text={t('retrievePanel.querySettings.responseFormat')}
68
+ tooltip={t('retrievePanel.querySettings.responseFormatTooltip')}
69
  side="left"
70
  />
71
  <Select
 
77
  </SelectTrigger>
78
  <SelectContent>
79
  <SelectGroup>
80
+ <SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
81
+ <SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
82
+ <SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
83
  </SelectGroup>
84
  </SelectContent>
85
  </Select>
 
89
  <>
90
  <Text
91
  className="ml-1"
92
+ text={t('retrievePanel.querySettings.topK')}
93
+ tooltip={t('retrievePanel.querySettings.topKTooltip')}
94
  side="left"
95
  />
96
  <NumberInput
 
99
  value={querySettings.top_k}
100
  onValueChange={(v) => handleChange('top_k', v)}
101
  min={1}
102
+ placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
103
  />
104
  </>
105
 
 
108
  <>
109
  <Text
110
  className="ml-1"
111
+ text={t('retrievePanel.querySettings.maxTokensTextUnit')}
112
+ tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')}
113
  side="left"
114
  />
115
  <NumberInput
 
118
  value={querySettings.max_token_for_text_unit}
119
  onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
120
  min={1}
121
+ placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')}
122
  />
123
  </>
124
 
125
  <>
126
  <Text
127
+ text={t('retrievePanel.querySettings.maxTokensGlobalContext')}
128
+ tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')}
129
  side="left"
130
  />
131
  <NumberInput
 
134
  value={querySettings.max_token_for_global_context}
135
  onValueChange={(v) => handleChange('max_token_for_global_context', v)}
136
  min={1}
137
+ placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')}
138
  />
139
  </>
140
 
141
  <>
142
  <Text
143
  className="ml-1"
144
+ text={t('retrievePanel.querySettings.maxTokensLocalContext')}
145
+ tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')}
146
  side="left"
147
  />
148
  <NumberInput
 
151
  value={querySettings.max_token_for_local_context}
152
  onValueChange={(v) => handleChange('max_token_for_local_context', v)}
153
  min={1}
154
+ placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')}
155
  />
156
  </>
157
  </>
 
160
  <>
161
  <Text
162
  className="ml-1"
163
+ text={t('retrievePanel.querySettings.historyTurns')}
164
+ tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')}
165
  side="left"
166
  />
167
  <NumberInput
 
172
  value={querySettings.history_turns}
173
  onValueChange={(v) => handleChange('history_turns', v)}
174
  min={0}
175
+ placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
176
  />
177
  </>
178
 
 
181
  <>
182
  <Text
183
  className="ml-1"
184
+ text={t('retrievePanel.querySettings.hlKeywords')}
185
+ tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')}
186
  side="left"
187
  />
188
  <Input
 
196
  .filter((k) => k !== '')
197
  handleChange('hl_keywords', keywords)
198
  }}
199
+ placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
200
  />
201
  </>
202
 
203
  <>
204
  <Text
205
  className="ml-1"
206
+ text={t('retrievePanel.querySettings.llKeywords')}
207
+ tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')}
208
  side="left"
209
  />
210
  <Input
 
218
  .filter((k) => k !== '')
219
  handleChange('ll_keywords', keywords)
220
  }}
221
+ placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
222
  />
223
  </>
224
  </>
 
228
  <div className="flex items-center gap-2">
229
  <Text
230
  className="ml-1"
231
+ text={t('retrievePanel.querySettings.onlyNeedContext')}
232
+ tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')}
233
  side="left"
234
  />
235
  <div className="grow" />
 
244
  <div className="flex items-center gap-2">
245
  <Text
246
  className="ml-1"
247
+ text={t('retrievePanel.querySettings.onlyNeedPrompt')}
248
+ tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')}
249
  side="left"
250
  />
251
  <div className="grow" />
 
260
  <div className="flex items-center gap-2">
261
  <Text
262
  className="ml-1"
263
+ text={t('retrievePanel.querySettings.streamResponse')}
264
+ tooltip={t('retrievePanel.querySettings.streamResponseTooltip')}
265
  side="left"
266
  />
267
  <div className="grow" />
lightrag_webui/src/features/DocumentManager.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import { useState, useEffect, useCallback } from 'react'
 
2
  import Button from '@/components/ui/Button'
3
  import {
4
  Table,
@@ -22,6 +23,7 @@ import { useBackendState } from '@/stores/state'
22
  import { RefreshCwIcon } from 'lucide-react'
23
 
24
  export default function DocumentManager() {
 
25
  const health = useBackendState.use.health()
26
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
27
 
@@ -44,7 +46,7 @@ export default function DocumentManager() {
44
  setDocs(null)
45
  }
46
  } catch (err) {
47
- toast.error('Failed to load documents\n' + errorMessage(err))
48
  }
49
  }, [setDocs])
50
 
@@ -57,7 +59,7 @@ export default function DocumentManager() {
57
  const { status } = await scanNewDocuments()
58
  toast.message(status)
59
  } catch (err) {
60
- toast.error('Failed to load documents\n' + errorMessage(err))
61
  }
62
  }, [])
63
 
@@ -69,7 +71,7 @@ export default function DocumentManager() {
69
  try {
70
  await fetchDocuments()
71
  } catch (err) {
72
- toast.error('Failed to get scan progress\n' + errorMessage(err))
73
  }
74
  }, 5000)
75
  return () => clearInterval(interval)
@@ -78,7 +80,7 @@ export default function DocumentManager() {
78
  return (
79
  <Card className="!size-full !rounded-none !border-none">
80
  <CardHeader>
81
- <CardTitle className="text-lg">Document Management</CardTitle>
82
  </CardHeader>
83
  <CardContent className="space-y-4">
84
  <div className="flex gap-2">
@@ -86,10 +88,10 @@ export default function DocumentManager() {
86
  variant="outline"
87
  onClick={scanDocuments}
88
  side="bottom"
89
- tooltip="Scan documents"
90
  size="sm"
91
  >
92
- <RefreshCwIcon /> Scan
93
  </Button>
94
  <div className="flex-1" />
95
  <ClearDocumentsDialog />
@@ -98,29 +100,29 @@ export default function DocumentManager() {
98
 
99
  <Card>
100
  <CardHeader>
101
- <CardTitle>Uploaded documents</CardTitle>
102
- <CardDescription>view the uploaded documents here</CardDescription>
103
  </CardHeader>
104
 
105
  <CardContent>
106
  {!docs && (
107
  <EmptyCard
108
- title="No documents uploaded"
109
- description="upload documents to see them here"
110
  />
111
  )}
112
  {docs && (
113
  <Table>
114
  <TableHeader>
115
  <TableRow>
116
- <TableHead>ID</TableHead>
117
- <TableHead>Summary</TableHead>
118
- <TableHead>Status</TableHead>
119
- <TableHead>Length</TableHead>
120
- <TableHead>Chunks</TableHead>
121
- <TableHead>Created</TableHead>
122
- <TableHead>Updated</TableHead>
123
- <TableHead>Metadata</TableHead>
124
  </TableRow>
125
  </TableHeader>
126
  <TableBody className="text-sm">
@@ -137,13 +139,13 @@ export default function DocumentManager() {
137
  </TableCell>
138
  <TableCell>
139
  {status === 'processed' && (
140
- <span className="text-green-600">Completed</span>
141
  )}
142
  {status === 'processing' && (
143
- <span className="text-blue-600">Processing</span>
144
  )}
145
- {status === 'pending' && <span className="text-yellow-600">Pending</span>}
146
- {status === 'failed' && <span className="text-red-600">Failed</span>}
147
  {doc.error && (
148
  <span className="ml-2 text-red-500" title={doc.error}>
149
  ⚠️
 
1
  import { useState, useEffect, useCallback } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
  import Button from '@/components/ui/Button'
4
  import {
5
  Table,
 
23
  import { RefreshCwIcon } from 'lucide-react'
24
 
25
  export default function DocumentManager() {
26
+ const { t } = useTranslation()
27
  const health = useBackendState.use.health()
28
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
29
 
 
46
  setDocs(null)
47
  }
48
  } catch (err) {
49
+ toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
50
  }
51
  }, [setDocs])
52
 
 
59
  const { status } = await scanNewDocuments()
60
  toast.message(status)
61
  } catch (err) {
62
+ toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
63
  }
64
  }, [])
65
 
 
71
  try {
72
  await fetchDocuments()
73
  } catch (err) {
74
+ toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
75
  }
76
  }, 5000)
77
  return () => clearInterval(interval)
 
80
  return (
81
  <Card className="!size-full !rounded-none !border-none">
82
  <CardHeader>
83
+ <CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
84
  </CardHeader>
85
  <CardContent className="space-y-4">
86
  <div className="flex gap-2">
 
88
  variant="outline"
89
  onClick={scanDocuments}
90
  side="bottom"
91
+ tooltip={t('documentPanel.documentManager.scanTooltip')}
92
  size="sm"
93
  >
94
+ <RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
95
  </Button>
96
  <div className="flex-1" />
97
  <ClearDocumentsDialog />
 
100
 
101
  <Card>
102
  <CardHeader>
103
+ <CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
104
+ <CardDescription>{t('documentPanel.documentManager.uploadedDescription')}</CardDescription>
105
  </CardHeader>
106
 
107
  <CardContent>
108
  {!docs && (
109
  <EmptyCard
110
+ title={t('documentPanel.documentManager.emptyTitle')}
111
+ description={t('documentPanel.documentManager.emptyDescription')}
112
  />
113
  )}
114
  {docs && (
115
  <Table>
116
  <TableHeader>
117
  <TableRow>
118
+ <TableHead>{t('documentPanel.documentManager.columns.id')}</TableHead>
119
+ <TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
120
+ <TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
121
+ <TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
122
+ <TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
123
+ <TableHead>{t('documentPanel.documentManager.columns.created')}</TableHead>
124
+ <TableHead>{t('documentPanel.documentManager.columns.updated')}</TableHead>
125
+ <TableHead>{t('documentPanel.documentManager.columns.metadata')}</TableHead>
126
  </TableRow>
127
  </TableHeader>
128
  <TableBody className="text-sm">
 
139
  </TableCell>
140
  <TableCell>
141
  {status === 'processed' && (
142
+ <span className="text-green-600">{t('documentPanel.documentManager.status.completed')}</span>
143
  )}
144
  {status === 'processing' && (
145
+ <span className="text-blue-600">{t('documentPanel.documentManager.status.processing')}</span>
146
  )}
147
+ {status === 'pending' && <span className="text-yellow-600">{t('documentPanel.documentManager.status.pending')}</span>}
148
+ {status === 'failed' && <span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span>}
149
  {doc.error && (
150
  <span className="ml-2 text-red-500" title={doc.error}>
151
  ⚠️
lightrag_webui/src/features/RetrievalTesting.tsx CHANGED
@@ -8,8 +8,10 @@ import { useDebounce } from '@/hooks/useDebounce'
8
  import QuerySettings from '@/components/retrieval/QuerySettings'
9
  import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
10
  import { EraserIcon, SendIcon } from 'lucide-react'
 
11
 
12
  export default function RetrievalTesting() {
 
13
  const [messages, setMessages] = useState<MessageWithError[]>(
14
  () => useSettingsStore.getState().retrievalHistory || []
15
  )
@@ -89,7 +91,7 @@ export default function RetrievalTesting() {
89
  }
90
  } catch (err) {
91
  // Handle error
92
- updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`, true)
93
  } finally {
94
  // Clear loading and add messages to state
95
  setIsLoading(false)
@@ -98,7 +100,7 @@ export default function RetrievalTesting() {
98
  .setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
99
  }
100
  },
101
- [inputValue, isLoading, messages, setMessages]
102
  )
103
 
104
  const debouncedMessages = useDebounce(messages, 100)
@@ -117,7 +119,7 @@ export default function RetrievalTesting() {
117
  <div className="flex min-h-0 flex-1 flex-col gap-2">
118
  {messages.length === 0 ? (
119
  <div className="text-muted-foreground flex h-full items-center justify-center text-lg">
120
- Start a retrieval by typing your query below
121
  </div>
122
  ) : (
123
  messages.map((message, idx) => (
@@ -143,18 +145,18 @@ export default function RetrievalTesting() {
143
  size="sm"
144
  >
145
  <EraserIcon />
146
- Clear
147
  </Button>
148
  <Input
149
  className="flex-1"
150
  value={inputValue}
151
  onChange={(e) => setInputValue(e.target.value)}
152
- placeholder="Type your query..."
153
  disabled={isLoading}
154
  />
155
  <Button type="submit" variant="default" disabled={isLoading} size="sm">
156
  <SendIcon />
157
- Send
158
  </Button>
159
  </form>
160
  </div>
 
8
  import QuerySettings from '@/components/retrieval/QuerySettings'
9
  import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
10
  import { EraserIcon, SendIcon } from 'lucide-react'
11
+ import { useTranslation } from 'react-i18next'
12
 
13
  export default function RetrievalTesting() {
14
+ const { t } = useTranslation()
15
  const [messages, setMessages] = useState<MessageWithError[]>(
16
  () => useSettingsStore.getState().retrievalHistory || []
17
  )
 
91
  }
92
  } catch (err) {
93
  // Handle error
94
+ updateAssistantMessage(`${t('retrievePanel.retrieval.error')}\n${errorMessage(err)}`, true)
95
  } finally {
96
  // Clear loading and add messages to state
97
  setIsLoading(false)
 
100
  .setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
101
  }
102
  },
103
+ [inputValue, isLoading, messages, setMessages, t]
104
  )
105
 
106
  const debouncedMessages = useDebounce(messages, 100)
 
119
  <div className="flex min-h-0 flex-1 flex-col gap-2">
120
  {messages.length === 0 ? (
121
  <div className="text-muted-foreground flex h-full items-center justify-center text-lg">
122
+ {t('retrievePanel.retrieval.startPrompt')}
123
  </div>
124
  ) : (
125
  messages.map((message, idx) => (
 
145
  size="sm"
146
  >
147
  <EraserIcon />
148
+ {t('retrievePanel.retrieval.clear')}
149
  </Button>
150
  <Input
151
  className="flex-1"
152
  value={inputValue}
153
  onChange={(e) => setInputValue(e.target.value)}
154
+ placeholder={t('retrievePanel.retrieval.placeholder')}
155
  disabled={isLoading}
156
  />
157
  <Button type="submit" variant="default" disabled={isLoading} size="sm">
158
  <SendIcon />
159
+ {t('retrievePanel.retrieval.send')}
160
  </Button>
161
  </form>
162
  </div>
lightrag_webui/src/features/SiteHeader.tsx CHANGED
@@ -5,6 +5,7 @@ import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
5
  import { useSettingsStore } from '@/stores/settings'
6
  import { useAuthStore } from '@/stores/state'
7
  import { cn } from '@/lib/utils'
 
8
  import { useNavigate } from 'react-router-dom'
9
 
10
  import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
@@ -31,21 +32,22 @@ function NavigationTab({ value, currentTab, children }: NavigationTabProps) {
31
 
32
  function TabsNavigation() {
33
  const currentTab = useSettingsStore.use.currentTab()
 
34
 
35
  return (
36
  <div className="flex h-8 self-center">
37
  <TabsList className="h-full gap-2">
38
  <NavigationTab value="documents" currentTab={currentTab}>
39
- Documents
40
  </NavigationTab>
41
  <NavigationTab value="knowledge-graph" currentTab={currentTab}>
42
- Knowledge Graph
43
  </NavigationTab>
44
  <NavigationTab value="retrieval" currentTab={currentTab}>
45
- Retrieval
46
  </NavigationTab>
47
  <NavigationTab value="api" currentTab={currentTab}>
48
- API
49
  </NavigationTab>
50
  </TabsList>
51
  </div>
@@ -53,6 +55,7 @@ function TabsNavigation() {
53
  }
54
 
55
  export default function SiteHeader() {
 
56
  const navigate = useNavigate()
57
  const { logout } = useAuthStore()
58
 
@@ -74,7 +77,7 @@ export default function SiteHeader() {
74
  </div>
75
 
76
  <nav className="flex items-center gap-2">
77
- <Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
78
  <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
79
  <GithubIcon className="size-4" aria-hidden="true" />
80
  </a>
 
5
  import { useSettingsStore } from '@/stores/settings'
6
  import { useAuthStore } from '@/stores/state'
7
  import { cn } from '@/lib/utils'
8
+ import { useTranslation } from 'react-i18next'
9
  import { useNavigate } from 'react-router-dom'
10
 
11
  import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
 
32
 
33
  function TabsNavigation() {
34
  const currentTab = useSettingsStore.use.currentTab()
35
+ const { t } = useTranslation()
36
 
37
  return (
38
  <div className="flex h-8 self-center">
39
  <TabsList className="h-full gap-2">
40
  <NavigationTab value="documents" currentTab={currentTab}>
41
+ {t('header.documents')}
42
  </NavigationTab>
43
  <NavigationTab value="knowledge-graph" currentTab={currentTab}>
44
+ {t('header.knowledgeGraph')}
45
  </NavigationTab>
46
  <NavigationTab value="retrieval" currentTab={currentTab}>
47
+ {t('header.retrieval')}
48
  </NavigationTab>
49
  <NavigationTab value="api" currentTab={currentTab}>
50
+ {t('header.api')}
51
  </NavigationTab>
52
  </TabsList>
53
  </div>
 
55
  }
56
 
57
  export default function SiteHeader() {
58
+ const { t } = useTranslation()
59
  const navigate = useNavigate()
60
  const { logout } = useAuthStore()
61
 
 
77
  </div>
78
 
79
  <nav className="flex items-center gap-2">
80
+ <Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
81
  <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
82
  <GithubIcon className="size-4" aria-hidden="true" />
83
  </a>
lightrag_webui/src/i18n.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next";
3
+
4
+ import en from "./locales/en.json";
5
+ import zh from "./locales/zh.json";
6
+
7
+ i18n
8
+ .use(initReactI18next)
9
+ .init({
10
+ resources: {
11
+ en: { translation: en },
12
+ zh: { translation: zh }
13
+ },
14
+ lng: "en", // default
15
+ fallbackLng: "en",
16
+ interpolation: {
17
+ escapeValue: false
18
+ }
19
+ });
20
+
21
+ export default i18n;
lightrag_webui/src/locales/en.json ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "header": {
3
+ "documents": "Documents",
4
+ "knowledgeGraph": "Knowledge Graph",
5
+ "retrieval": "Retrieval",
6
+ "api": "API",
7
+ "projectRepository": "Project Repository",
8
+ "themeToggle": {
9
+ "switchToLight": "Switch to light theme",
10
+ "switchToDark": "Switch to dark theme"
11
+ }
12
+ },
13
+ "documentPanel": {
14
+ "clearDocuments": {
15
+ "button": "Clear",
16
+ "tooltip": "Clear documents",
17
+ "title": "Clear Documents",
18
+ "confirm": "Do you really want to clear all documents?",
19
+ "confirmButton": "YES",
20
+ "success": "Documents cleared successfully",
21
+ "failed": "Clear Documents Failed:\n{{message}}",
22
+ "error": "Clear Documents Failed:\n{{error}}"
23
+ },
24
+ "uploadDocuments": {
25
+ "button": "Upload",
26
+ "tooltip": "Upload documents",
27
+ "title": "Upload Documents",
28
+ "description": "Drag and drop your documents here or click to browse.",
29
+ "uploading": "Uploading {{name}}: {{percent}}%",
30
+ "success": "Upload Success:\n{{name}} uploaded successfully",
31
+ "failed": "Upload Failed:\n{{name}}\n{{message}}",
32
+ "error": "Upload Failed:\n{{name}}\n{{error}}",
33
+ "generalError": "Upload Failed\n{{error}}",
34
+ "fileTypes": "Supported types: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
35
+ },
36
+ "documentManager": {
37
+ "title": "Document Management",
38
+ "scanButton": "Scan",
39
+ "scanTooltip": "Scan documents",
40
+ "uploadedTitle": "Uploaded Documents",
41
+ "uploadedDescription": "List of uploaded documents and their statuses.",
42
+ "emptyTitle": "No Documents",
43
+ "emptyDescription": "There are no uploaded documents yet.",
44
+ "columns": {
45
+ "id": "ID",
46
+ "summary": "Summary",
47
+ "status": "Status",
48
+ "length": "Length",
49
+ "chunks": "Chunks",
50
+ "created": "Created",
51
+ "updated": "Updated",
52
+ "metadata": "Metadata"
53
+ },
54
+ "status": {
55
+ "completed": "Completed",
56
+ "processing": "Processing",
57
+ "pending": "Pending",
58
+ "failed": "Failed"
59
+ },
60
+ "errors": {
61
+ "loadFailed": "Failed to load documents\n{{error}}",
62
+ "scanFailed": "Failed to scan documents\n{{error}}",
63
+ "scanProgressFailed": "Failed to get scan progress\n{{error}}"
64
+ }
65
+ }
66
+ },
67
+ "graphPanel": {
68
+ "sideBar": {
69
+ "settings": {
70
+ "settings": "Settings",
71
+ "healthCheck": "Health Check",
72
+ "showPropertyPanel": "Show Property Panel",
73
+ "showSearchBar": "Show Search Bar",
74
+ "showNodeLabel": "Show Node Label",
75
+ "nodeDraggable": "Node Draggable",
76
+ "showEdgeLabel": "Show Edge Label",
77
+ "hideUnselectedEdges": "Hide Unselected Edges",
78
+ "edgeEvents": "Edge Events",
79
+ "maxQueryDepth": "Max Query Depth",
80
+ "minDegree": "Minimum Degree",
81
+ "maxLayoutIterations": "Max Layout Iterations",
82
+ "apiKey": "API Key",
83
+ "enterYourAPIkey": "Enter your API key",
84
+ "save": "Save"
85
+ },
86
+
87
+ "zoomControl": {
88
+ "zoomIn": "Zoom In",
89
+ "zoomOut": "Zoom Out",
90
+ "resetZoom": "Reset Zoom"
91
+ },
92
+
93
+ "layoutsControl": {
94
+ "startAnimation": "Start the layout animation",
95
+ "stopAnimation": "Stop the layout animation",
96
+ "layoutGraph": "Layout Graph",
97
+ "layouts": {
98
+ "Circular": "Circular",
99
+ "Circlepack": "Circlepack",
100
+ "Random": "Random",
101
+ "Noverlaps": "Noverlaps",
102
+ "Force Directed": "Force Directed",
103
+ "Force Atlas": "Force Atlas"
104
+ }
105
+ },
106
+
107
+ "fullScreenControl": {
108
+ "fullScreen": "Full Screen",
109
+ "windowed": "Windowed"
110
+ }
111
+ },
112
+ "statusIndicator": {
113
+ "connected": "Connected",
114
+ "disconnected": "Disconnected"
115
+ },
116
+ "statusCard": {
117
+ "unavailable": "Status information unavailable",
118
+ "storageInfo": "Storage Info",
119
+ "workingDirectory": "Working Directory",
120
+ "inputDirectory": "Input Directory",
121
+ "llmConfig": "LLM Configuration",
122
+ "llmBinding": "LLM Binding",
123
+ "llmBindingHost": "LLM Binding Host",
124
+ "llmModel": "LLM Model",
125
+ "maxTokens": "Max Tokens",
126
+ "embeddingConfig": "Embedding Configuration",
127
+ "embeddingBinding": "Embedding Binding",
128
+ "embeddingBindingHost": "Embedding Binding Host",
129
+ "embeddingModel": "Embedding Model",
130
+ "storageConfig": "Storage Configuration",
131
+ "kvStorage": "KV Storage",
132
+ "docStatusStorage": "Doc Status Storage",
133
+ "graphStorage": "Graph Storage",
134
+ "vectorStorage": "Vector Storage"
135
+ },
136
+ "propertiesView": {
137
+ "node": {
138
+ "title": "Node",
139
+ "id": "ID",
140
+ "labels": "Labels",
141
+ "degree": "Degree",
142
+ "properties": "Properties",
143
+ "relationships": "Relationships"
144
+ },
145
+ "edge": {
146
+ "title": "Relationship",
147
+ "id": "ID",
148
+ "type": "Type",
149
+ "source": "Source",
150
+ "target": "Target",
151
+ "properties": "Properties"
152
+ }
153
+ },
154
+ "search": {
155
+ "placeholder": "Search nodes...",
156
+ "message": "And {count} others"
157
+ },
158
+ "graphLabels": {
159
+ "selectTooltip": "Select query label",
160
+ "noLabels": "No labels found",
161
+ "label": "Label",
162
+ "placeholder": "Search labels...",
163
+ "andOthers": "And {count} others"
164
+ }
165
+ },
166
+ "retrievePanel": {
167
+ "chatMessage": {
168
+ "copyTooltip": "Copy to clipboard",
169
+ "copyError": "Failed to copy text to clipboard"
170
+ },
171
+ "retrieval": {
172
+ "startPrompt": "Start a retrieval by typing your query below",
173
+ "clear": "Clear",
174
+ "send": "Send",
175
+ "placeholder": "Type your query...",
176
+ "error": "Error: Failed to get response"
177
+ },
178
+ "querySettings": {
179
+ "parametersTitle": "Parameters",
180
+ "parametersDescription": "Configure your query parameters",
181
+
182
+ "queryMode": "Query Mode",
183
+ "queryModeTooltip": "Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval",
184
+ "queryModeOptions": {
185
+ "naive": "Naive",
186
+ "local": "Local",
187
+ "global": "Global",
188
+ "hybrid": "Hybrid",
189
+ "mix": "Mix"
190
+ },
191
+
192
+ "responseFormat": "Response Format",
193
+ "responseFormatTooltip": "Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points",
194
+ "responseFormatOptions": {
195
+ "multipleParagraphs": "Multiple Paragraphs",
196
+ "singleParagraph": "Single Paragraph",
197
+ "bulletPoints": "Bullet Points"
198
+ },
199
+
200
+ "topK": "Top K Results",
201
+ "topKTooltip": "Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode",
202
+ "topKPlaceholder": "Number of results",
203
+
204
+ "maxTokensTextUnit": "Max Tokens for Text Unit",
205
+ "maxTokensTextUnitTooltip": "Maximum number of tokens allowed for each retrieved text chunk",
206
+
207
+ "maxTokensGlobalContext": "Max Tokens for Global Context",
208
+ "maxTokensGlobalContextTooltip": "Maximum number of tokens allocated for relationship descriptions in global retrieval",
209
+
210
+ "maxTokensLocalContext": "Max Tokens for Local Context",
211
+ "maxTokensLocalContextTooltip": "Maximum number of tokens allocated for entity descriptions in local retrieval",
212
+
213
+ "historyTurns": "History Turns",
214
+ "historyTurnsTooltip": "Number of complete conversation turns (user-assistant pairs) to consider in the response context",
215
+ "historyTurnsPlaceholder": "Number of history turns",
216
+
217
+ "hlKeywords": "High-Level Keywords",
218
+ "hlKeywordsTooltip": "List of high-level keywords to prioritize in retrieval. Separate with commas",
219
+ "hlkeywordsPlaceHolder": "Enter keywords",
220
+
221
+ "llKeywords": "Low-Level Keywords",
222
+ "llKeywordsTooltip": "List of low-level keywords to refine retrieval focus. Separate with commas",
223
+
224
+ "onlyNeedContext": "Only Need Context",
225
+ "onlyNeedContextTooltip": "If True, only returns the retrieved context without generating a response",
226
+
227
+ "onlyNeedPrompt": "Only Need Prompt",
228
+ "onlyNeedPromptTooltip": "If True, only returns the generated prompt without producing a response",
229
+
230
+ "streamResponse": "Stream Response",
231
+ "streamResponseTooltip": "If True, enables streaming output for real-time responses"
232
+ }
233
+ }
234
+ }
lightrag_webui/src/locales/zh.json ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "header": {
3
+ "documents": "文档",
4
+ "knowledgeGraph": "知识图谱",
5
+ "retrieval": "检索",
6
+ "api": "API",
7
+ "projectRepository": "项目仓库",
8
+ "themeToggle": {
9
+ "switchToLight": "切换到亮色主题",
10
+ "switchToDark": "切换到暗色主题"
11
+ }
12
+ },
13
+ "documentPanel": {
14
+ "clearDocuments": {
15
+ "button": "清除",
16
+ "tooltip": "清除文档",
17
+ "title": "清除文档",
18
+ "confirm": "您确定要清除所有文档吗?",
19
+ "confirmButton": "确定",
20
+ "success": "文档已成功清除",
21
+ "failed": "清除文档失败:\n{{message}}",
22
+ "error": "清除文档失败:\n{{error}}"
23
+ },
24
+ "uploadDocuments": {
25
+ "button": "上传",
26
+ "tooltip": "上传文档",
27
+ "title": "上传文档",
28
+ "description": "拖放文档到此处或点击浏览。",
29
+ "uploading": "正在上传 {{name}}: {{percent}}%",
30
+ "success": "上传成功:\n{{name}} 上传成功",
31
+ "failed": "上传失败:\n{{name}}\n{{message}}",
32
+ "error": "上传失败:\n{{name}}\n{{error}}",
33
+ "generalError": "上传失败\n{{error}}",
34
+ "fileTypes": "支持的文件类型: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
35
+ },
36
+ "documentManager": {
37
+ "title": "文档管理",
38
+ "scanButton": "扫描",
39
+ "scanTooltip": "扫描文档",
40
+ "uploadedTitle": "已上传文档",
41
+ "uploadedDescription": "已上传文档及其状态列表。",
42
+ "emptyTitle": "暂无文档",
43
+ "emptyDescription": "尚未上传任何文档。",
44
+ "columns": {
45
+ "id": "ID",
46
+ "summary": "摘要",
47
+ "status": "状态",
48
+ "length": "长度",
49
+ "chunks": "分块",
50
+ "created": "创建时间",
51
+ "updated": "更新时间",
52
+ "metadata": "元数据"
53
+ },
54
+ "status": {
55
+ "completed": "已完成",
56
+ "processing": "处理中",
57
+ "pending": "待处理",
58
+ "failed": "失败"
59
+ },
60
+ "errors": {
61
+ "loadFailed": "加载文档失败\n{{error}}",
62
+ "scanFailed": "扫描文档失败\n{{error}}",
63
+ "scanProgressFailed": "获取扫描进度失败\n{{error}}"
64
+ }
65
+ }
66
+ },
67
+ "graphPanel": {
68
+ "sideBar": {
69
+ "settings": {
70
+ "settings": "设置",
71
+ "healthCheck": "健康检查",
72
+ "showPropertyPanel": "显示属性面板",
73
+ "showSearchBar": "显示搜索栏",
74
+ "showNodeLabel": "显示节点标签",
75
+ "nodeDraggable": "节点可拖动",
76
+ "showEdgeLabel": "显示边标签",
77
+ "hideUnselectedEdges": "隐藏未选中边",
78
+ "edgeEvents": "边事件",
79
+ "maxQueryDepth": "最大查询深度",
80
+ "minDegree": "最小度数",
81
+ "maxLayoutIterations": "最大布局迭代次数",
82
+ "apiKey": "API 密钥",
83
+ "enterYourAPIkey": "输入您的 API 密钥",
84
+ "save": "保存"
85
+ },
86
+
87
+ "zoomControl": {
88
+ "zoomIn": "放大",
89
+ "zoomOut": "缩小",
90
+ "resetZoom": "重置缩放"
91
+ },
92
+
93
+ "layoutsControl": {
94
+ "startAnimation": "开始布局动画",
95
+ "stopAnimation": "停止布局动画",
96
+ "layoutGraph": "布局图",
97
+ "layouts": {
98
+ "Circular": "环形布局",
99
+ "Circlepack": "圆形打包布局",
100
+ "Random": "随机布局",
101
+ "Noverlaps": "无重叠布局",
102
+ "Force Directed": "力导向布局",
103
+ "Force Atlas": "力导向图谱布局"
104
+ }
105
+ },
106
+
107
+ "fullScreenControl": {
108
+ "fullScreen": "全屏",
109
+ "windowed": "窗口模式"
110
+ }
111
+ },
112
+ "statusIndicator": {
113
+ "connected": "已连接",
114
+ "disconnected": "未连接"
115
+ },
116
+ "statusCard": {
117
+ "unavailable": "状态信息不可用",
118
+ "storageInfo": "存储信息",
119
+ "workingDirectory": "工作目录",
120
+ "inputDirectory": "输入目录",
121
+ "llmConfig": "LLM 配置",
122
+ "llmBinding": "LLM 绑定",
123
+ "llmBindingHost": "LLM 绑定主机",
124
+ "llmModel": "LLM 模型",
125
+ "maxTokens": "最大 Token 数",
126
+ "embeddingConfig": "嵌入配置",
127
+ "embeddingBinding": "嵌入绑定",
128
+ "embeddingBindingHost": "嵌入绑定主机",
129
+ "embeddingModel": "嵌入模型",
130
+ "storageConfig": "存储配置",
131
+ "kvStorage": "KV 存储",
132
+ "docStatusStorage": "文档状态存储",
133
+ "graphStorage": "图存储",
134
+ "vectorStorage": "向量存储"
135
+ },
136
+ "propertiesView": {
137
+ "node": {
138
+ "title": "节点",
139
+ "id": "ID",
140
+ "labels": "标签",
141
+ "degree": "度数",
142
+ "properties": "属性",
143
+ "relationships": "关系"
144
+ },
145
+ "edge": {
146
+ "title": "关系",
147
+ "id": "ID",
148
+ "type": "类型",
149
+ "source": "源",
150
+ "target": "目标",
151
+ "properties": "属性"
152
+ }
153
+ },
154
+ "search": {
155
+ "placeholder": "搜索节点...",
156
+ "message": "以及其它 {count} 项"
157
+ },
158
+ "graphLabels": {
159
+ "selectTooltip": "选择查询标签",
160
+ "noLabels": "未找到标签",
161
+ "label": "标签",
162
+ "placeholder": "搜索标签...",
163
+ "andOthers": "以及其它 {count} 个"
164
+ }
165
+ },
166
+ "retrievePanel": {
167
+ "chatMessage": {
168
+ "copyTooltip": "复制到剪贴板",
169
+ "copyError": "无法复制文本到剪贴板"
170
+ },
171
+
172
+ "retrieval": {
173
+ "startPrompt": "在下面输入您的查询以开始检索",
174
+ "clear": "清除",
175
+ "send": "发送",
176
+ "placeholder": "输入您的查询...",
177
+ "error": "错误:无法获取响应"
178
+ },
179
+ "querySettings": {
180
+ "parametersTitle": "参数设置",
181
+ "parametersDescription": "配置查询参数",
182
+
183
+ "queryMode": "查询模式",
184
+ "queryModeTooltip": "选择检索策略:\n• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文的信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱与向量检索",
185
+ "queryModeOptions": {
186
+ "naive": "朴素",
187
+ "local": "本地",
188
+ "global": "全局",
189
+ "hybrid": "混合",
190
+ "mix": "综合"
191
+ },
192
+
193
+ "responseFormat": "响应格式",
194
+ "responseFormatTooltip": "定义响应格式。例如:\n• 多个段落\n• 单个段落\n• 项目符号",
195
+ "responseFormatOptions": {
196
+ "multipleParagraphs": "多个段落",
197
+ "singleParagraph": "单个段落",
198
+ "bulletPoints": "项目符号"
199
+ },
200
+
201
+ "topK": "Top K 结果数",
202
+ "topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系",
203
+ "topKPlaceholder": "结果数",
204
+
205
+ "maxTokensTextUnit": "文本单元最大 Token 数",
206
+ "maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数",
207
+
208
+ "maxTokensGlobalContext": "全局上下文最大 Token 数",
209
+ "maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数",
210
+
211
+ "maxTokensLocalContext": "本地上下文最大 Token 数",
212
+ "maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数",
213
+
214
+ "historyTurns": "历史轮次",
215
+ "historyTurnsTooltip": "在响应上下文中考虑的完整对话轮次(用户-助手对)",
216
+ "historyTurnsPlaceholder": "历史轮次的数量",
217
+
218
+ "hlKeywords": "高级关键词",
219
+ "hlKeywordsTooltip": "检索时优先考虑的高级关键词。请用逗号分隔",
220
+ "hlkeywordsPlaceHolder": "输入关键词",
221
+
222
+ "llKeywords": "低级关键词",
223
+ "llKeywordsTooltip": "用于优化检索焦点的低级关键词。请用逗号分隔",
224
+
225
+ "onlyNeedContext": "仅需要上下文",
226
+ "onlyNeedContextTooltip": "如果为 True,则仅返回检索到的上下文,而不会生成回复",
227
+
228
+ "onlyNeedPrompt": "仅需要提示",
229
+ "onlyNeedPromptTooltip": "如果为 True,则仅返回生成的提示,而不会生成回复",
230
+
231
+ "streamResponse": "流式响应",
232
+ "streamResponseTooltip": "如果为 True,则启用流式输出以获得实时响应"
233
+ }
234
+ }
235
+ }
lightrag_webui/src/main.tsx CHANGED
@@ -2,6 +2,8 @@ import { StrictMode } from 'react'
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
4
  import AppRouter from './AppRouter'
 
 
5
 
6
  createRoot(document.getElementById('root')!).render(
7
  <StrictMode>
 
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
4
  import AppRouter from './AppRouter'
5
+ import "./i18n";
6
+
7
 
8
  createRoot(document.getElementById('root')!).render(
9
  <StrictMode>