Daniel.y commited on
Commit
5d25c62
·
unverified ·
2 Parent(s): ecaddc9 cfb4814

Merge pull request #1367 from danielaskdd/add-graph-db-lock

Browse files

Add graph_db_lock to ensure consistency across multiple processes for node and edge edition jobs

lightrag/api/__init__.py CHANGED
@@ -1 +1 @@
1
- __api_version__ = "0149"
 
1
+ __api_version__ = "0150"
lightrag/api/routers/graph_routes.py CHANGED
@@ -2,14 +2,29 @@
2
  This module contains all graph-related routes for the LightRAG API.
3
  """
4
 
5
- from typing import Optional
6
- from fastapi import APIRouter, Depends, Query
 
 
7
 
 
8
  from ..utils_api import get_combined_auth_dependency
9
 
10
  router = APIRouter(tags=["graph"])
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def create_graph_routes(rag, api_key: Optional[str] = None):
14
  combined_auth = get_combined_auth_dependency(api_key)
15
 
@@ -21,7 +36,14 @@ def create_graph_routes(rag, api_key: Optional[str] = None):
21
  Returns:
22
  List[str]: List of graph labels
23
  """
24
- return await rag.get_graph_labels()
 
 
 
 
 
 
 
25
 
26
  @router.get("/graphs", dependencies=[Depends(combined_auth)])
27
  async def get_knowledge_graph(
@@ -43,10 +65,109 @@ def create_graph_routes(rag, api_key: Optional[str] = None):
43
  Returns:
44
  Dict[str, List[str]]: Knowledge graph for label
45
  """
46
- return await rag.get_knowledge_graph(
47
- node_label=label,
48
- max_depth=max_depth,
49
- max_nodes=max_nodes,
50
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
  return router
 
2
  This module contains all graph-related routes for the LightRAG API.
3
  """
4
 
5
+ from typing import Optional, Dict, Any
6
+ import traceback
7
+ from fastapi import APIRouter, Depends, Query, HTTPException
8
+ from pydantic import BaseModel
9
 
10
+ from lightrag.utils import logger
11
  from ..utils_api import get_combined_auth_dependency
12
 
13
  router = APIRouter(tags=["graph"])
14
 
15
 
16
+ class EntityUpdateRequest(BaseModel):
17
+ entity_name: str
18
+ updated_data: Dict[str, Any]
19
+ allow_rename: bool = False
20
+
21
+
22
+ class RelationUpdateRequest(BaseModel):
23
+ source_id: str
24
+ target_id: str
25
+ updated_data: Dict[str, Any]
26
+
27
+
28
  def create_graph_routes(rag, api_key: Optional[str] = None):
29
  combined_auth = get_combined_auth_dependency(api_key)
30
 
 
36
  Returns:
37
  List[str]: List of graph labels
38
  """
39
+ try:
40
+ return await rag.get_graph_labels()
41
+ except Exception as e:
42
+ logger.error(f"Error getting graph labels: {str(e)}")
43
+ logger.error(traceback.format_exc())
44
+ raise HTTPException(
45
+ status_code=500, detail=f"Error getting graph labels: {str(e)}"
46
+ )
47
 
48
  @router.get("/graphs", dependencies=[Depends(combined_auth)])
49
  async def get_knowledge_graph(
 
65
  Returns:
66
  Dict[str, List[str]]: Knowledge graph for label
67
  """
68
+ try:
69
+ return await rag.get_knowledge_graph(
70
+ node_label=label,
71
+ max_depth=max_depth,
72
+ max_nodes=max_nodes,
73
+ )
74
+ except Exception as e:
75
+ logger.error(f"Error getting knowledge graph for label '{label}': {str(e)}")
76
+ logger.error(traceback.format_exc())
77
+ raise HTTPException(
78
+ status_code=500, detail=f"Error getting knowledge graph: {str(e)}"
79
+ )
80
+
81
+ @router.get("/graph/entity/exists", dependencies=[Depends(combined_auth)])
82
+ async def check_entity_exists(
83
+ name: str = Query(..., description="Entity name to check"),
84
+ ):
85
+ """
86
+ Check if an entity with the given name exists in the knowledge graph
87
+
88
+ Args:
89
+ name (str): Name of the entity to check
90
+
91
+ Returns:
92
+ Dict[str, bool]: Dictionary with 'exists' key indicating if entity exists
93
+ """
94
+ try:
95
+ exists = await rag.chunk_entity_relation_graph.has_node(name)
96
+ return {"exists": exists}
97
+ except Exception as e:
98
+ logger.error(f"Error checking entity existence for '{name}': {str(e)}")
99
+ logger.error(traceback.format_exc())
100
+ raise HTTPException(
101
+ status_code=500, detail=f"Error checking entity existence: {str(e)}"
102
+ )
103
+
104
+ @router.post("/graph/entity/edit", dependencies=[Depends(combined_auth)])
105
+ async def update_entity(request: EntityUpdateRequest):
106
+ """
107
+ Update an entity's properties in the knowledge graph
108
+
109
+ Args:
110
+ request (EntityUpdateRequest): Request containing entity name, updated data, and rename flag
111
+
112
+ Returns:
113
+ Dict: Updated entity information
114
+ """
115
+ try:
116
+ result = await rag.aedit_entity(
117
+ entity_name=request.entity_name,
118
+ updated_data=request.updated_data,
119
+ allow_rename=request.allow_rename,
120
+ )
121
+ return {
122
+ "status": "success",
123
+ "message": "Entity updated successfully",
124
+ "data": result,
125
+ }
126
+ except ValueError as ve:
127
+ logger.error(
128
+ f"Validation error updating entity '{request.entity_name}': {str(ve)}"
129
+ )
130
+ raise HTTPException(status_code=400, detail=str(ve))
131
+ except Exception as e:
132
+ logger.error(f"Error updating entity '{request.entity_name}': {str(e)}")
133
+ logger.error(traceback.format_exc())
134
+ raise HTTPException(
135
+ status_code=500, detail=f"Error updating entity: {str(e)}"
136
+ )
137
+
138
+ @router.post("/graph/relation/edit", dependencies=[Depends(combined_auth)])
139
+ async def update_relation(request: RelationUpdateRequest):
140
+ """Update a relation's properties in the knowledge graph
141
+
142
+ Args:
143
+ request (RelationUpdateRequest): Request containing source ID, target ID and updated data
144
+
145
+ Returns:
146
+ Dict: Updated relation information
147
+ """
148
+ try:
149
+ result = await rag.aedit_relation(
150
+ source_entity=request.source_id,
151
+ target_entity=request.target_id,
152
+ updated_data=request.updated_data,
153
+ )
154
+ return {
155
+ "status": "success",
156
+ "message": "Relation updated successfully",
157
+ "data": result,
158
+ }
159
+ except ValueError as ve:
160
+ logger.error(
161
+ f"Validation error updating relation between '{request.source_id}' and '{request.target_id}': {str(ve)}"
162
+ )
163
+ raise HTTPException(status_code=400, detail=str(ve))
164
+ except Exception as e:
165
+ logger.error(
166
+ f"Error updating relation between '{request.source_id}' and '{request.target_id}': {str(e)}"
167
+ )
168
+ logger.error(traceback.format_exc())
169
+ raise HTTPException(
170
+ status_code=500, detail=f"Error updating relation: {str(e)}"
171
+ )
172
 
173
  return router
lightrag/api/webui/assets/index-BJDb04H1.css ADDED
Binary file (57.5 kB). View file
 
lightrag/api/webui/assets/{index-vzpYU2q3.js → index-CIRM3gxn.js} RENAMED
Binary files a/lightrag/api/webui/assets/index-vzpYU2q3.js and b/lightrag/api/webui/assets/index-CIRM3gxn.js differ
 
lightrag/api/webui/assets/index-CTB4Vp_z.css DELETED
Binary file (57.2 kB)
 
lightrag/api/webui/index.html CHANGED
Binary files a/lightrag/api/webui/index.html and b/lightrag/api/webui/index.html differ
 
lightrag/lightrag.py CHANGED
@@ -3,20 +3,22 @@ from __future__ import annotations
3
  import asyncio
4
  import configparser
5
  import os
6
- import csv
7
  import warnings
8
  from dataclasses import asdict, dataclass, field
9
  from datetime import datetime
10
  from functools import partial
11
  from typing import Any, AsyncIterator, Callable, Iterator, cast, final, Literal
12
- import pandas as pd
13
-
14
 
15
  from lightrag.kg import (
16
  STORAGES,
17
  verify_storage_implementation,
18
  )
19
 
 
 
 
 
 
20
  from .base import (
21
  BaseGraphStorage,
22
  BaseKVStorage,
@@ -779,10 +781,6 @@ class LightRAG:
779
  3. Process each chunk for entity and relation extraction
780
  4. Update the document status
781
  """
782
- from lightrag.kg.shared_storage import (
783
- get_namespace_data,
784
- get_pipeline_status_lock,
785
- )
786
 
787
  # Get pipeline status shared data and lock
788
  pipeline_status = await get_namespace_data("pipeline_status")
@@ -1427,107 +1425,58 @@ class LightRAG:
1427
  async def _query_done(self):
1428
  await self.llm_response_cache.index_done_callback()
1429
 
1430
- def delete_by_entity(self, entity_name: str) -> None:
1431
- loop = always_get_an_event_loop()
1432
- return loop.run_until_complete(self.adelete_by_entity(entity_name))
1433
 
1434
- # TODO: Lock all KG relative DB to esure consistency across multiple processes
1435
- async def adelete_by_entity(self, entity_name: str) -> None:
1436
- try:
1437
- await self.entities_vdb.delete_entity(entity_name)
1438
- await self.relationships_vdb.delete_entity_relation(entity_name)
1439
- await self.chunk_entity_relation_graph.delete_node(entity_name)
1440
 
1441
- logger.info(
1442
- f"Entity '{entity_name}' and its relationships have been deleted."
1443
- )
1444
- await self._delete_by_entity_done()
1445
- except Exception as e:
1446
- logger.error(f"Error while deleting entity '{entity_name}': {e}")
1447
-
1448
- async def _delete_by_entity_done(self) -> None:
1449
- await asyncio.gather(
1450
- *[
1451
- cast(StorageNameSpace, storage_inst).index_done_callback()
1452
- for storage_inst in [ # type: ignore
1453
- self.entities_vdb,
1454
- self.relationships_vdb,
1455
- self.chunk_entity_relation_graph,
1456
- ]
1457
- ]
1458
- )
1459
 
1460
- def delete_by_relation(self, source_entity: str, target_entity: str) -> None:
1461
- """Synchronously delete a relation between two entities.
1462
 
1463
- Args:
1464
- source_entity: Name of the source entity
1465
- target_entity: Name of the target entity
1466
  """
1467
- loop = always_get_an_event_loop()
1468
- return loop.run_until_complete(
1469
- self.adelete_by_relation(source_entity, target_entity)
1470
- )
1471
 
1472
- # TODO: Lock all KG relative DB to esure consistency across multiple processes
1473
- async def adelete_by_relation(self, source_entity: str, target_entity: str) -> None:
1474
- """Asynchronously delete a relation between two entities.
1475
 
1476
- Args:
1477
- source_entity: Name of the source entity
1478
- target_entity: Name of the target entity
1479
- """
1480
- try:
1481
- # TODO: check if has_edge function works on reverse relation
1482
- # Check if the relation exists
1483
- edge_exists = await self.chunk_entity_relation_graph.has_edge(
1484
- source_entity, target_entity
1485
- )
1486
- if not edge_exists:
1487
- logger.warning(
1488
- f"Relation from '{source_entity}' to '{target_entity}' does not exist"
1489
- )
1490
- return
1491
 
1492
- # Delete relation from vector database
1493
- relation_id = compute_mdhash_id(
1494
- source_entity + target_entity, prefix="rel-"
1495
- )
1496
- await self.relationships_vdb.delete([relation_id])
 
 
 
 
 
 
 
 
 
 
1497
 
1498
- # Delete relation from knowledge graph
1499
- await self.chunk_entity_relation_graph.remove_edges(
1500
- [(source_entity, target_entity)]
1501
- )
1502
 
1503
- logger.info(
1504
- f"Successfully deleted relation from '{source_entity}' to '{target_entity}'"
1505
- )
1506
- await self._delete_relation_done()
1507
  except Exception as e:
1508
- logger.error(
1509
- f"Error while deleting relation from '{source_entity}' to '{target_entity}': {e}"
1510
- )
1511
-
1512
- async def _delete_relation_done(self) -> None:
1513
- """Callback after relation deletion is complete"""
1514
- await asyncio.gather(
1515
- *[
1516
- cast(StorageNameSpace, storage_inst).index_done_callback()
1517
- for storage_inst in [ # type: ignore
1518
- self.relationships_vdb,
1519
- self.chunk_entity_relation_graph,
1520
- ]
1521
- ]
1522
- )
1523
-
1524
- async def get_processing_status(self) -> dict[str, int]:
1525
- """Get current document processing status counts
1526
 
1527
- Returns:
1528
- Dict with counts for each status
1529
- """
1530
- return await self.doc_status.get_status_counts()
1531
 
1532
  async def get_docs_by_status(
1533
  self, status: DocStatus
@@ -1539,7 +1488,8 @@ class LightRAG:
1539
  """
1540
  return await self.doc_status.get_docs_by_status(status)
1541
 
1542
- # TODO: Lock all KG relative DB to esure consistency across multiple processes
 
1543
  async def adelete_by_doc_id(self, doc_id: str) -> None:
1544
  """Delete a document and all its related data
1545
 
@@ -1796,109 +1746,82 @@ class LightRAG:
1796
  except Exception as e:
1797
  logger.error(f"Error while deleting document {doc_id}: {e}")
1798
 
1799
- async def get_entity_info(
1800
- self, entity_name: str, include_vector_data: bool = False
1801
- ) -> dict[str, str | None | dict[str, str]]:
1802
- """Get detailed information of an entity"""
1803
-
1804
- # Get information from the graph
1805
- node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
1806
- source_id = node_data.get("source_id") if node_data else None
1807
-
1808
- result: dict[str, str | None | dict[str, str]] = {
1809
- "entity_name": entity_name,
1810
- "source_id": source_id,
1811
- "graph_data": node_data,
1812
- }
1813
-
1814
- # Optional: Get vector database information
1815
- if include_vector_data:
1816
- entity_id = compute_mdhash_id(entity_name, prefix="ent-")
1817
- vector_data = await self.entities_vdb.get_by_id(entity_id)
1818
- result["vector_data"] = vector_data
1819
-
1820
- return result
1821
 
1822
- async def get_relation_info(
1823
- self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
1824
- ) -> dict[str, str | None | dict[str, str]]:
1825
- """Get detailed information of a relationship"""
1826
 
1827
- # Get information from the graph
1828
- edge_data = await self.chunk_entity_relation_graph.get_edge(
1829
- src_entity, tgt_entity
 
 
1830
  )
1831
- source_id = edge_data.get("source_id") if edge_data else None
1832
-
1833
- result: dict[str, str | None | dict[str, str]] = {
1834
- "src_entity": src_entity,
1835
- "tgt_entity": tgt_entity,
1836
- "source_id": source_id,
1837
- "graph_data": edge_data,
1838
- }
1839
 
1840
- # Optional: Get vector database information
1841
- if include_vector_data:
1842
- rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
1843
- vector_data = await self.relationships_vdb.get_by_id(rel_id)
1844
- result["vector_data"] = vector_data
1845
-
1846
- return result
1847
 
1848
- async def aclear_cache(self, modes: list[str] | None = None) -> None:
1849
- """Clear cache data from the LLM response cache storage.
1850
 
1851
  Args:
1852
- modes (list[str] | None): Modes of cache to clear. Options: ["default", "naive", "local", "global", "hybrid", "mix"].
1853
- "default" represents extraction cache.
1854
- If None, clears all cache.
1855
-
1856
- Example:
1857
- # Clear all cache
1858
- await rag.aclear_cache()
1859
-
1860
- # Clear local mode cache
1861
- await rag.aclear_cache(modes=["local"])
1862
-
1863
- # Clear extraction cache
1864
- await rag.aclear_cache(modes=["default"])
1865
  """
1866
- if not self.llm_response_cache:
1867
- logger.warning("No cache storage configured")
1868
- return
1869
 
1870
- valid_modes = ["default", "naive", "local", "global", "hybrid", "mix"]
 
 
 
 
 
1871
 
1872
- # Validate input
1873
- if modes and not all(mode in valid_modes for mode in modes):
1874
- raise ValueError(f"Invalid mode. Valid modes are: {valid_modes}")
 
 
1875
 
1876
- try:
1877
- # Reset the cache storage for specified mode
1878
- if modes:
1879
- success = await self.llm_response_cache.drop_cache_by_modes(modes)
1880
- if success:
1881
- logger.info(f"Cleared cache for modes: {modes}")
1882
- else:
1883
- logger.warning(f"Failed to clear cache for modes: {modes}")
1884
- else:
1885
- # Clear all modes
1886
- success = await self.llm_response_cache.drop_cache_by_modes(valid_modes)
1887
- if success:
1888
- logger.info("Cleared all cache")
1889
- else:
1890
- logger.warning("Failed to clear all cache")
1891
 
1892
- await self.llm_response_cache.index_done_callback()
 
 
 
1893
 
1894
- except Exception as e:
1895
- logger.error(f"Error while clearing cache: {e}")
 
 
 
1896
 
1897
- def clear_cache(self, modes: list[str] | None = None) -> None:
1898
- """Synchronous version of aclear_cache."""
1899
- return always_get_an_event_loop().run_until_complete(self.aclear_cache(modes))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1900
 
1901
- # TODO: Lock all KG relative DB to esure consistency across multiple processes
1902
  async def aedit_entity(
1903
  self, entity_name: str, updated_data: dict[str, str], allow_rename: bool = True
1904
  ) -> dict[str, Any]:
@@ -1914,204 +1837,25 @@ class LightRAG:
1914
  Returns:
1915
  Dictionary containing updated entity information
1916
  """
1917
- try:
1918
- # 1. Get current entity information
1919
- node_exists = await self.chunk_entity_relation_graph.has_node(entity_name)
1920
- if not node_exists:
1921
- raise ValueError(f"Entity '{entity_name}' does not exist")
1922
- node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
1923
-
1924
- # Check if entity is being renamed
1925
- new_entity_name = updated_data.get("entity_name", entity_name)
1926
- is_renaming = new_entity_name != entity_name
1927
-
1928
- # If renaming, check if new name already exists
1929
- if is_renaming:
1930
- if not allow_rename:
1931
- raise ValueError(
1932
- "Entity renaming is not allowed. Set allow_rename=True to enable this feature"
1933
- )
1934
-
1935
- existing_node = await self.chunk_entity_relation_graph.has_node(
1936
- new_entity_name
1937
- )
1938
- if existing_node:
1939
- raise ValueError(
1940
- f"Entity name '{new_entity_name}' already exists, cannot rename"
1941
- )
1942
-
1943
- # 2. Update entity information in the graph
1944
- new_node_data = {**node_data, **updated_data}
1945
- new_node_data["entity_id"] = new_entity_name
1946
-
1947
- if "entity_name" in new_node_data:
1948
- del new_node_data[
1949
- "entity_name"
1950
- ] # Node data should not contain entity_name field
1951
-
1952
- # If renaming entity
1953
- if is_renaming:
1954
- logger.info(f"Renaming entity '{entity_name}' to '{new_entity_name}'")
1955
-
1956
- # Create new entity
1957
- await self.chunk_entity_relation_graph.upsert_node(
1958
- new_entity_name, new_node_data
1959
- )
1960
-
1961
- # Store relationships that need to be updated
1962
- relations_to_update = []
1963
- relations_to_delete = []
1964
- # Get all edges related to the original entity
1965
- edges = await self.chunk_entity_relation_graph.get_node_edges(
1966
- entity_name
1967
- )
1968
- if edges:
1969
- # Recreate edges for the new entity
1970
- for source, target in edges:
1971
- edge_data = await self.chunk_entity_relation_graph.get_edge(
1972
- source, target
1973
- )
1974
- if edge_data:
1975
- relations_to_delete.append(
1976
- compute_mdhash_id(source + target, prefix="rel-")
1977
- )
1978
- relations_to_delete.append(
1979
- compute_mdhash_id(target + source, prefix="rel-")
1980
- )
1981
- if source == entity_name:
1982
- await self.chunk_entity_relation_graph.upsert_edge(
1983
- new_entity_name, target, edge_data
1984
- )
1985
- relations_to_update.append(
1986
- (new_entity_name, target, edge_data)
1987
- )
1988
- else: # target == entity_name
1989
- await self.chunk_entity_relation_graph.upsert_edge(
1990
- source, new_entity_name, edge_data
1991
- )
1992
- relations_to_update.append(
1993
- (source, new_entity_name, edge_data)
1994
- )
1995
-
1996
- # Delete old entity
1997
- await self.chunk_entity_relation_graph.delete_node(entity_name)
1998
-
1999
- # Delete old entity record from vector database
2000
- old_entity_id = compute_mdhash_id(entity_name, prefix="ent-")
2001
- await self.entities_vdb.delete([old_entity_id])
2002
- logger.info(
2003
- f"Deleted old entity '{entity_name}' and its vector embedding from database"
2004
- )
2005
-
2006
- # Delete old relation records from vector database
2007
- await self.relationships_vdb.delete(relations_to_delete)
2008
- logger.info(
2009
- f"Deleted {len(relations_to_delete)} relation records for entity '{entity_name}' from vector database"
2010
- )
2011
-
2012
- # Update relationship vector representations
2013
- for src, tgt, edge_data in relations_to_update:
2014
- description = edge_data.get("description", "")
2015
- keywords = edge_data.get("keywords", "")
2016
- source_id = edge_data.get("source_id", "")
2017
- weight = float(edge_data.get("weight", 1.0))
2018
-
2019
- # Create new content for embedding
2020
- content = f"{src}\t{tgt}\n{keywords}\n{description}"
2021
-
2022
- # Calculate relationship ID
2023
- relation_id = compute_mdhash_id(src + tgt, prefix="rel-")
2024
-
2025
- # Prepare data for vector database update
2026
- relation_data = {
2027
- relation_id: {
2028
- "content": content,
2029
- "src_id": src,
2030
- "tgt_id": tgt,
2031
- "source_id": source_id,
2032
- "description": description,
2033
- "keywords": keywords,
2034
- "weight": weight,
2035
- }
2036
- }
2037
-
2038
- # Update vector database
2039
- await self.relationships_vdb.upsert(relation_data)
2040
-
2041
- # Update working entity name to new name
2042
- entity_name = new_entity_name
2043
- else:
2044
- # If not renaming, directly update node data
2045
- await self.chunk_entity_relation_graph.upsert_node(
2046
- entity_name, new_node_data
2047
- )
2048
-
2049
- # 3. Recalculate entity's vector representation and update vector database
2050
- description = new_node_data.get("description", "")
2051
- source_id = new_node_data.get("source_id", "")
2052
- entity_type = new_node_data.get("entity_type", "")
2053
- content = entity_name + "\n" + description
2054
-
2055
- # Calculate entity ID
2056
- entity_id = compute_mdhash_id(entity_name, prefix="ent-")
2057
-
2058
- # Prepare data for vector database update
2059
- entity_data = {
2060
- entity_id: {
2061
- "content": content,
2062
- "entity_name": entity_name,
2063
- "source_id": source_id,
2064
- "description": description,
2065
- "entity_type": entity_type,
2066
- }
2067
- }
2068
-
2069
- # Update vector database
2070
- await self.entities_vdb.upsert(entity_data)
2071
-
2072
- # 4. Save changes
2073
- await self._edit_entity_done()
2074
-
2075
- logger.info(f"Entity '{entity_name}' successfully updated")
2076
- return await self.get_entity_info(entity_name, include_vector_data=True)
2077
- except Exception as e:
2078
- logger.error(f"Error while editing entity '{entity_name}': {e}")
2079
- raise
2080
 
2081
  def edit_entity(
2082
  self, entity_name: str, updated_data: dict[str, str], allow_rename: bool = True
2083
  ) -> dict[str, Any]:
2084
- """Synchronously edit entity information.
2085
-
2086
- Updates entity information in the knowledge graph and re-embeds the entity in the vector database.
2087
-
2088
- Args:
2089
- entity_name: Name of the entity to edit
2090
- updated_data: Dictionary containing updated attributes, e.g. {"description": "new description", "entity_type": "new type"}
2091
- allow_rename: Whether to allow entity renaming, defaults to True
2092
-
2093
- Returns:
2094
- Dictionary containing updated entity information
2095
- """
2096
  loop = always_get_an_event_loop()
2097
  return loop.run_until_complete(
2098
  self.aedit_entity(entity_name, updated_data, allow_rename)
2099
  )
2100
 
2101
- async def _edit_entity_done(self) -> None:
2102
- """Callback after entity editing is complete, ensures updates are persisted"""
2103
- await asyncio.gather(
2104
- *[
2105
- cast(StorageNameSpace, storage_inst).index_done_callback()
2106
- for storage_inst in [ # type: ignore
2107
- self.entities_vdb,
2108
- self.relationships_vdb,
2109
- self.chunk_entity_relation_graph,
2110
- ]
2111
- ]
2112
- )
2113
-
2114
- # TODO: Lock all KG relative DB to esure consistency across multiple processes
2115
  async def aedit_relation(
2116
  self, source_entity: str, target_entity: str, updated_data: dict[str, Any]
2117
  ) -> dict[str, Any]:
@@ -2127,110 +1871,25 @@ class LightRAG:
2127
  Returns:
2128
  Dictionary containing updated relation information
2129
  """
2130
- try:
2131
- # 1. Get current relation information
2132
- edge_exists = await self.chunk_entity_relation_graph.has_edge(
2133
- source_entity, target_entity
2134
- )
2135
- if not edge_exists:
2136
- raise ValueError(
2137
- f"Relation from '{source_entity}' to '{target_entity}' does not exist"
2138
- )
2139
- edge_data = await self.chunk_entity_relation_graph.get_edge(
2140
- source_entity, target_entity
2141
- )
2142
- # Important: First delete the old relation record from the vector database
2143
- old_relation_id = compute_mdhash_id(
2144
- source_entity + target_entity, prefix="rel-"
2145
- )
2146
- await self.relationships_vdb.delete([old_relation_id])
2147
- logger.info(
2148
- f"Deleted old relation record from vector database for relation {source_entity} -> {target_entity}"
2149
- )
2150
-
2151
- # 2. Update relation information in the graph
2152
- new_edge_data = {**edge_data, **updated_data}
2153
- await self.chunk_entity_relation_graph.upsert_edge(
2154
- source_entity, target_entity, new_edge_data
2155
- )
2156
-
2157
- # 3. Recalculate relation's vector representation and update vector database
2158
- description = new_edge_data.get("description", "")
2159
- keywords = new_edge_data.get("keywords", "")
2160
- source_id = new_edge_data.get("source_id", "")
2161
- weight = float(new_edge_data.get("weight", 1.0))
2162
-
2163
- # Create content for embedding
2164
- content = f"{source_entity}\t{target_entity}\n{keywords}\n{description}"
2165
-
2166
- # Calculate relation ID
2167
- relation_id = compute_mdhash_id(
2168
- source_entity + target_entity, prefix="rel-"
2169
- )
2170
-
2171
- # Prepare data for vector database update
2172
- relation_data = {
2173
- relation_id: {
2174
- "content": content,
2175
- "src_id": source_entity,
2176
- "tgt_id": target_entity,
2177
- "source_id": source_id,
2178
- "description": description,
2179
- "keywords": keywords,
2180
- "weight": weight,
2181
- }
2182
- }
2183
-
2184
- # Update vector database
2185
- await self.relationships_vdb.upsert(relation_data)
2186
-
2187
- # 4. Save changes
2188
- await self._edit_relation_done()
2189
-
2190
- logger.info(
2191
- f"Relation from '{source_entity}' to '{target_entity}' successfully updated"
2192
- )
2193
- return await self.get_relation_info(
2194
- source_entity, target_entity, include_vector_data=True
2195
- )
2196
- except Exception as e:
2197
- logger.error(
2198
- f"Error while editing relation from '{source_entity}' to '{target_entity}': {e}"
2199
- )
2200
- raise
2201
 
2202
  def edit_relation(
2203
  self, source_entity: str, target_entity: str, updated_data: dict[str, Any]
2204
  ) -> dict[str, Any]:
2205
- """Synchronously edit relation information.
2206
-
2207
- Updates relation (edge) information in the knowledge graph and re-embeds the relation in the vector database.
2208
-
2209
- Args:
2210
- source_entity: Name of the source entity
2211
- target_entity: Name of the target entity
2212
- updated_data: Dictionary containing updated attributes, e.g. {"description": "new description", "keywords": "keywords"}
2213
-
2214
- Returns:
2215
- Dictionary containing updated relation information
2216
- """
2217
  loop = always_get_an_event_loop()
2218
  return loop.run_until_complete(
2219
  self.aedit_relation(source_entity, target_entity, updated_data)
2220
  )
2221
 
2222
- async def _edit_relation_done(self) -> None:
2223
- """Callback after relation editing is complete, ensures updates are persisted"""
2224
- await asyncio.gather(
2225
- *[
2226
- cast(StorageNameSpace, storage_inst).index_done_callback()
2227
- for storage_inst in [ # type: ignore
2228
- self.relationships_vdb,
2229
- self.chunk_entity_relation_graph,
2230
- ]
2231
- ]
2232
- )
2233
-
2234
  async def acreate_entity(
2235
  self, entity_name: str, entity_data: dict[str, Any]
2236
  ) -> dict[str, Any]:
@@ -2245,68 +1904,19 @@ class LightRAG:
2245
  Returns:
2246
  Dictionary containing created entity information
2247
  """
2248
- try:
2249
- # Check if entity already exists
2250
- existing_node = await self.chunk_entity_relation_graph.has_node(entity_name)
2251
- if existing_node:
2252
- raise ValueError(f"Entity '{entity_name}' already exists")
2253
-
2254
- # Prepare node data with defaults if missing
2255
- node_data = {
2256
- "entity_id": entity_name,
2257
- "entity_type": entity_data.get("entity_type", "UNKNOWN"),
2258
- "description": entity_data.get("description", ""),
2259
- "source_id": entity_data.get("source_id", "manual"),
2260
- }
2261
-
2262
- # Add entity to knowledge graph
2263
- await self.chunk_entity_relation_graph.upsert_node(entity_name, node_data)
2264
-
2265
- # Prepare content for entity
2266
- description = node_data.get("description", "")
2267
- source_id = node_data.get("source_id", "")
2268
- entity_type = node_data.get("entity_type", "")
2269
- content = entity_name + "\n" + description
2270
-
2271
- # Calculate entity ID
2272
- entity_id = compute_mdhash_id(entity_name, prefix="ent-")
2273
-
2274
- # Prepare data for vector database update
2275
- entity_data_for_vdb = {
2276
- entity_id: {
2277
- "content": content,
2278
- "entity_name": entity_name,
2279
- "source_id": source_id,
2280
- "description": description,
2281
- "entity_type": entity_type,
2282
- }
2283
- }
2284
-
2285
- # Update vector database
2286
- await self.entities_vdb.upsert(entity_data_for_vdb)
2287
-
2288
- # Save changes
2289
- await self._edit_entity_done()
2290
-
2291
- logger.info(f"Entity '{entity_name}' successfully created")
2292
- return await self.get_entity_info(entity_name, include_vector_data=True)
2293
- except Exception as e:
2294
- logger.error(f"Error while creating entity '{entity_name}': {e}")
2295
- raise
2296
 
2297
  def create_entity(
2298
  self, entity_name: str, entity_data: dict[str, Any]
2299
  ) -> dict[str, Any]:
2300
- """Synchronously create a new entity.
2301
-
2302
- Creates a new entity in the knowledge graph and adds it to the vector database.
2303
- Args:
2304
- entity_name: Name of the new entity
2305
- entity_data: Dictionary containing entity attributes, e.g. {"description": "description", "entity_type": "type"}
2306
-
2307
- Returns:
2308
- Dictionary containing created entity information
2309
- """
2310
  loop = always_get_an_event_loop()
2311
  return loop.run_until_complete(self.acreate_entity(entity_name, entity_data))
2312
 
@@ -2325,108 +1935,25 @@ class LightRAG:
2325
  Returns:
2326
  Dictionary containing created relation information
2327
  """
2328
- try:
2329
- # Check if both entities exist
2330
- source_exists = await self.chunk_entity_relation_graph.has_node(
2331
- source_entity
2332
- )
2333
- target_exists = await self.chunk_entity_relation_graph.has_node(
2334
- target_entity
2335
- )
2336
-
2337
- if not source_exists:
2338
- raise ValueError(f"Source entity '{source_entity}' does not exist")
2339
- if not target_exists:
2340
- raise ValueError(f"Target entity '{target_entity}' does not exist")
2341
-
2342
- # Check if relation already exists
2343
- existing_edge = await self.chunk_entity_relation_graph.has_edge(
2344
- source_entity, target_entity
2345
- )
2346
- if existing_edge:
2347
- raise ValueError(
2348
- f"Relation from '{source_entity}' to '{target_entity}' already exists"
2349
- )
2350
-
2351
- # Prepare edge data with defaults if missing
2352
- edge_data = {
2353
- "description": relation_data.get("description", ""),
2354
- "keywords": relation_data.get("keywords", ""),
2355
- "source_id": relation_data.get("source_id", "manual"),
2356
- "weight": float(relation_data.get("weight", 1.0)),
2357
- }
2358
-
2359
- # Add relation to knowledge graph
2360
- await self.chunk_entity_relation_graph.upsert_edge(
2361
- source_entity, target_entity, edge_data
2362
- )
2363
-
2364
- # Prepare content for embedding
2365
- description = edge_data.get("description", "")
2366
- keywords = edge_data.get("keywords", "")
2367
- source_id = edge_data.get("source_id", "")
2368
- weight = edge_data.get("weight", 1.0)
2369
-
2370
- # Create content for embedding
2371
- content = f"{keywords}\t{source_entity}\n{target_entity}\n{description}"
2372
-
2373
- # Calculate relation ID
2374
- relation_id = compute_mdhash_id(
2375
- source_entity + target_entity, prefix="rel-"
2376
- )
2377
-
2378
- # Prepare data for vector database update
2379
- relation_data_for_vdb = {
2380
- relation_id: {
2381
- "content": content,
2382
- "src_id": source_entity,
2383
- "tgt_id": target_entity,
2384
- "source_id": source_id,
2385
- "description": description,
2386
- "keywords": keywords,
2387
- "weight": weight,
2388
- }
2389
- }
2390
-
2391
- # Update vector database
2392
- await self.relationships_vdb.upsert(relation_data_for_vdb)
2393
-
2394
- # Save changes
2395
- await self._edit_relation_done()
2396
-
2397
- logger.info(
2398
- f"Relation from '{source_entity}' to '{target_entity}' successfully created"
2399
- )
2400
- return await self.get_relation_info(
2401
- source_entity, target_entity, include_vector_data=True
2402
- )
2403
- except Exception as e:
2404
- logger.error(
2405
- f"Error while creating relation from '{source_entity}' to '{target_entity}': {e}"
2406
- )
2407
- raise
2408
 
2409
  def create_relation(
2410
  self, source_entity: str, target_entity: str, relation_data: dict[str, Any]
2411
  ) -> dict[str, Any]:
2412
- """Synchronously create a new relation between entities.
2413
-
2414
- Creates a new relation (edge) in the knowledge graph and adds it to the vector database.
2415
-
2416
- Args:
2417
- source_entity: Name of the source entity
2418
- target_entity: Name of the target entity
2419
- relation_data: Dictionary containing relation attributes, e.g. {"description": "description", "keywords": "keywords"}
2420
-
2421
- Returns:
2422
- Dictionary containing created relation information
2423
- """
2424
  loop = always_get_an_event_loop()
2425
  return loop.run_until_complete(
2426
  self.acreate_relation(source_entity, target_entity, relation_data)
2427
  )
2428
 
2429
- # TODO: Lock all KG relative DB to esure consistency across multiple processes
2430
  async def amerge_entities(
2431
  self,
2432
  source_entities: list[str],
@@ -2454,221 +1981,31 @@ class LightRAG:
2454
  Returns:
2455
  Dictionary containing the merged entity information
2456
  """
2457
- try:
2458
- # Default merge strategy
2459
- default_strategy = {
2460
- "description": "concatenate",
2461
- "entity_type": "keep_first",
2462
- "source_id": "join_unique",
2463
- }
2464
-
2465
- merge_strategy = (
2466
- default_strategy
2467
- if merge_strategy is None
2468
- else {**default_strategy, **merge_strategy}
2469
- )
2470
- target_entity_data = (
2471
- {} if target_entity_data is None else target_entity_data
2472
- )
2473
-
2474
- # 1. Check if all source entities exist
2475
- source_entities_data = {}
2476
- for entity_name in source_entities:
2477
- node_exists = await self.chunk_entity_relation_graph.has_node(
2478
- entity_name
2479
- )
2480
- if not node_exists:
2481
- raise ValueError(f"Source entity '{entity_name}' does not exist")
2482
- node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
2483
- source_entities_data[entity_name] = node_data
2484
-
2485
- # 2. Check if target entity exists and get its data if it does
2486
- target_exists = await self.chunk_entity_relation_graph.has_node(
2487
- target_entity
2488
- )
2489
- existing_target_entity_data = {}
2490
- if target_exists:
2491
- existing_target_entity_data = (
2492
- await self.chunk_entity_relation_graph.get_node(target_entity)
2493
- )
2494
- logger.info(
2495
- f"Target entity '{target_entity}' already exists, will merge data"
2496
- )
2497
-
2498
- # 3. Merge entity data
2499
- merged_entity_data = self._merge_entity_attributes(
2500
- list(source_entities_data.values())
2501
- + ([existing_target_entity_data] if target_exists else []),
2502
- merge_strategy,
2503
- )
2504
-
2505
- # Apply any explicitly provided target entity data (overrides merged data)
2506
- for key, value in target_entity_data.items():
2507
- merged_entity_data[key] = value
2508
-
2509
- # 4. Get all relationships of the source entities
2510
- all_relations = []
2511
- for entity_name in source_entities:
2512
- # Get all relationships of the source entities
2513
- edges = await self.chunk_entity_relation_graph.get_node_edges(
2514
- entity_name
2515
- )
2516
- if edges:
2517
- for src, tgt in edges:
2518
- # Ensure src is the current entity
2519
- if src == entity_name:
2520
- edge_data = await self.chunk_entity_relation_graph.get_edge(
2521
- src, tgt
2522
- )
2523
- all_relations.append((src, tgt, edge_data))
2524
-
2525
- # 5. Create or update the target entity
2526
- merged_entity_data["entity_id"] = target_entity
2527
- if not target_exists:
2528
- await self.chunk_entity_relation_graph.upsert_node(
2529
- target_entity, merged_entity_data
2530
- )
2531
- logger.info(f"Created new target entity '{target_entity}'")
2532
- else:
2533
- await self.chunk_entity_relation_graph.upsert_node(
2534
- target_entity, merged_entity_data
2535
- )
2536
- logger.info(f"Updated existing target entity '{target_entity}'")
2537
-
2538
- # 6. Recreate all relationships, pointing to the target entity
2539
- relation_updates = {} # Track relationships that need to be merged
2540
- relations_to_delete = []
2541
-
2542
- for src, tgt, edge_data in all_relations:
2543
- relations_to_delete.append(compute_mdhash_id(src + tgt, prefix="rel-"))
2544
- relations_to_delete.append(compute_mdhash_id(tgt + src, prefix="rel-"))
2545
- new_src = target_entity if src in source_entities else src
2546
- new_tgt = target_entity if tgt in source_entities else tgt
2547
-
2548
- # Skip relationships between source entities to avoid self-loops
2549
- if new_src == new_tgt:
2550
- logger.info(
2551
- f"Skipping relationship between source entities: {src} -> {tgt} to avoid self-loop"
2552
- )
2553
- continue
2554
-
2555
- # Check if the same relationship already exists
2556
- relation_key = f"{new_src}|{new_tgt}"
2557
- if relation_key in relation_updates:
2558
- # Merge relationship data
2559
- existing_data = relation_updates[relation_key]["data"]
2560
- merged_relation = self._merge_relation_attributes(
2561
- [existing_data, edge_data],
2562
- {
2563
- "description": "concatenate",
2564
- "keywords": "join_unique",
2565
- "source_id": "join_unique",
2566
- "weight": "max",
2567
- },
2568
- )
2569
- relation_updates[relation_key]["data"] = merged_relation
2570
- logger.info(
2571
- f"Merged duplicate relationship: {new_src} -> {new_tgt}"
2572
- )
2573
- else:
2574
- relation_updates[relation_key] = {
2575
- "src": new_src,
2576
- "tgt": new_tgt,
2577
- "data": edge_data.copy(),
2578
- }
2579
-
2580
- # Apply relationship updates
2581
- for rel_data in relation_updates.values():
2582
- await self.chunk_entity_relation_graph.upsert_edge(
2583
- rel_data["src"], rel_data["tgt"], rel_data["data"]
2584
- )
2585
- logger.info(
2586
- f"Created or updated relationship: {rel_data['src']} -> {rel_data['tgt']}"
2587
- )
2588
-
2589
- # Delete relationships records from vector database
2590
- await self.relationships_vdb.delete(relations_to_delete)
2591
- logger.info(
2592
- f"Deleted {len(relations_to_delete)} relation records for entity '{entity_name}' from vector database"
2593
- )
2594
-
2595
- # 7. Update entity vector representation
2596
- description = merged_entity_data.get("description", "")
2597
- source_id = merged_entity_data.get("source_id", "")
2598
- entity_type = merged_entity_data.get("entity_type", "")
2599
- content = target_entity + "\n" + description
2600
-
2601
- entity_id = compute_mdhash_id(target_entity, prefix="ent-")
2602
- entity_data_for_vdb = {
2603
- entity_id: {
2604
- "content": content,
2605
- "entity_name": target_entity,
2606
- "source_id": source_id,
2607
- "description": description,
2608
- "entity_type": entity_type,
2609
- }
2610
- }
2611
-
2612
- await self.entities_vdb.upsert(entity_data_for_vdb)
2613
-
2614
- # 8. Update relationship vector representations
2615
- for rel_data in relation_updates.values():
2616
- src = rel_data["src"]
2617
- tgt = rel_data["tgt"]
2618
- edge_data = rel_data["data"]
2619
-
2620
- description = edge_data.get("description", "")
2621
- keywords = edge_data.get("keywords", "")
2622
- source_id = edge_data.get("source_id", "")
2623
- weight = float(edge_data.get("weight", 1.0))
2624
-
2625
- content = f"{keywords}\t{src}\n{tgt}\n{description}"
2626
- relation_id = compute_mdhash_id(src + tgt, prefix="rel-")
2627
-
2628
- relation_data_for_vdb = {
2629
- relation_id: {
2630
- "content": content,
2631
- "src_id": src,
2632
- "tgt_id": tgt,
2633
- "source_id": source_id,
2634
- "description": description,
2635
- "keywords": keywords,
2636
- "weight": weight,
2637
- }
2638
- }
2639
-
2640
- await self.relationships_vdb.upsert(relation_data_for_vdb)
2641
-
2642
- # 9. Delete source entities
2643
- for entity_name in source_entities:
2644
- if entity_name == target_entity:
2645
- logger.info(
2646
- f"Skipping deletion of '{entity_name}' as it's also the target entity"
2647
- )
2648
- continue
2649
-
2650
- # Delete entity node from knowledge graph
2651
- await self.chunk_entity_relation_graph.delete_node(entity_name)
2652
-
2653
- # Delete entity record from vector database
2654
- entity_id = compute_mdhash_id(entity_name, prefix="ent-")
2655
- await self.entities_vdb.delete([entity_id])
2656
-
2657
- logger.info(
2658
- f"Deleted source entity '{entity_name}' and its vector embedding from database"
2659
- )
2660
-
2661
- # 10. Save changes
2662
- await self._merge_entities_done()
2663
 
2664
- logger.info(
2665
- f"Successfully merged {len(source_entities)} entities into '{target_entity}'"
 
 
 
 
 
 
 
 
 
2666
  )
2667
- return await self.get_entity_info(target_entity, include_vector_data=True)
2668
-
2669
- except Exception as e:
2670
- logger.error(f"Error merging entities: {e}")
2671
- raise
2672
 
2673
  async def aexport_data(
2674
  self,
@@ -2688,275 +2025,16 @@ class LightRAG:
2688
  - table: Print formatted tables to console
2689
  include_vector_data: Whether to include data from the vector database.
2690
  """
2691
- # Collect data
2692
- entities_data = []
2693
- relations_data = []
2694
- relationships_data = []
2695
-
2696
- # --- Entities ---
2697
- all_entities = await self.chunk_entity_relation_graph.get_all_labels()
2698
- for entity_name in all_entities:
2699
- entity_info = await self.get_entity_info(
2700
- entity_name, include_vector_data=include_vector_data
2701
- )
2702
- entity_row = {
2703
- "entity_name": entity_name,
2704
- "source_id": entity_info["source_id"],
2705
- "graph_data": str(
2706
- entity_info["graph_data"]
2707
- ), # Convert to string to ensure compatibility
2708
- }
2709
- if include_vector_data and "vector_data" in entity_info:
2710
- entity_row["vector_data"] = str(entity_info["vector_data"])
2711
- entities_data.append(entity_row)
2712
-
2713
- # --- Relations ---
2714
- for src_entity in all_entities:
2715
- for tgt_entity in all_entities:
2716
- if src_entity == tgt_entity:
2717
- continue
2718
-
2719
- edge_exists = await self.chunk_entity_relation_graph.has_edge(
2720
- src_entity, tgt_entity
2721
- )
2722
- if edge_exists:
2723
- relation_info = await self.get_relation_info(
2724
- src_entity, tgt_entity, include_vector_data=include_vector_data
2725
- )
2726
- relation_row = {
2727
- "src_entity": src_entity,
2728
- "tgt_entity": tgt_entity,
2729
- "source_id": relation_info["source_id"],
2730
- "graph_data": str(
2731
- relation_info["graph_data"]
2732
- ), # Convert to string
2733
- }
2734
- if include_vector_data and "vector_data" in relation_info:
2735
- relation_row["vector_data"] = str(relation_info["vector_data"])
2736
- relations_data.append(relation_row)
2737
-
2738
- # --- Relationships (from VectorDB) ---
2739
- all_relationships = await self.relationships_vdb.client_storage
2740
- for rel in all_relationships["data"]:
2741
- relationships_data.append(
2742
- {
2743
- "relationship_id": rel["__id__"],
2744
- "data": str(rel), # Convert to string for compatibility
2745
- }
2746
- )
2747
-
2748
- # Export based on format
2749
- if file_format == "csv":
2750
- # CSV export
2751
- with open(output_path, "w", newline="", encoding="utf-8") as csvfile:
2752
- # Entities
2753
- if entities_data:
2754
- csvfile.write("# ENTITIES\n")
2755
- writer = csv.DictWriter(csvfile, fieldnames=entities_data[0].keys())
2756
- writer.writeheader()
2757
- writer.writerows(entities_data)
2758
- csvfile.write("\n\n")
2759
-
2760
- # Relations
2761
- if relations_data:
2762
- csvfile.write("# RELATIONS\n")
2763
- writer = csv.DictWriter(
2764
- csvfile, fieldnames=relations_data[0].keys()
2765
- )
2766
- writer.writeheader()
2767
- writer.writerows(relations_data)
2768
- csvfile.write("\n\n")
2769
-
2770
- # Relationships
2771
- if relationships_data:
2772
- csvfile.write("# RELATIONSHIPS\n")
2773
- writer = csv.DictWriter(
2774
- csvfile, fieldnames=relationships_data[0].keys()
2775
- )
2776
- writer.writeheader()
2777
- writer.writerows(relationships_data)
2778
-
2779
- elif file_format == "excel":
2780
- # Excel export
2781
- entities_df = (
2782
- pd.DataFrame(entities_data) if entities_data else pd.DataFrame()
2783
- )
2784
- relations_df = (
2785
- pd.DataFrame(relations_data) if relations_data else pd.DataFrame()
2786
- )
2787
- relationships_df = (
2788
- pd.DataFrame(relationships_data)
2789
- if relationships_data
2790
- else pd.DataFrame()
2791
- )
2792
-
2793
- with pd.ExcelWriter(output_path, engine="xlsxwriter") as writer:
2794
- if not entities_df.empty:
2795
- entities_df.to_excel(writer, sheet_name="Entities", index=False)
2796
- if not relations_df.empty:
2797
- relations_df.to_excel(writer, sheet_name="Relations", index=False)
2798
- if not relationships_df.empty:
2799
- relationships_df.to_excel(
2800
- writer, sheet_name="Relationships", index=False
2801
- )
2802
-
2803
- elif file_format == "md":
2804
- # Markdown export
2805
- with open(output_path, "w", encoding="utf-8") as mdfile:
2806
- mdfile.write("# LightRAG Data Export\n\n")
2807
-
2808
- # Entities
2809
- mdfile.write("## Entities\n\n")
2810
- if entities_data:
2811
- # Write header
2812
- mdfile.write("| " + " | ".join(entities_data[0].keys()) + " |\n")
2813
- mdfile.write(
2814
- "| "
2815
- + " | ".join(["---"] * len(entities_data[0].keys()))
2816
- + " |\n"
2817
- )
2818
-
2819
- # Write rows
2820
- for entity in entities_data:
2821
- mdfile.write(
2822
- "| " + " | ".join(str(v) for v in entity.values()) + " |\n"
2823
- )
2824
- mdfile.write("\n\n")
2825
- else:
2826
- mdfile.write("*No entity data available*\n\n")
2827
-
2828
- # Relations
2829
- mdfile.write("## Relations\n\n")
2830
- if relations_data:
2831
- # Write header
2832
- mdfile.write("| " + " | ".join(relations_data[0].keys()) + " |\n")
2833
- mdfile.write(
2834
- "| "
2835
- + " | ".join(["---"] * len(relations_data[0].keys()))
2836
- + " |\n"
2837
- )
2838
-
2839
- # Write rows
2840
- for relation in relations_data:
2841
- mdfile.write(
2842
- "| "
2843
- + " | ".join(str(v) for v in relation.values())
2844
- + " |\n"
2845
- )
2846
- mdfile.write("\n\n")
2847
- else:
2848
- mdfile.write("*No relation data available*\n\n")
2849
-
2850
- # Relationships
2851
- mdfile.write("## Relationships\n\n")
2852
- if relationships_data:
2853
- # Write header
2854
- mdfile.write(
2855
- "| " + " | ".join(relationships_data[0].keys()) + " |\n"
2856
- )
2857
- mdfile.write(
2858
- "| "
2859
- + " | ".join(["---"] * len(relationships_data[0].keys()))
2860
- + " |\n"
2861
- )
2862
-
2863
- # Write rows
2864
- for relationship in relationships_data:
2865
- mdfile.write(
2866
- "| "
2867
- + " | ".join(str(v) for v in relationship.values())
2868
- + " |\n"
2869
- )
2870
- else:
2871
- mdfile.write("*No relationship data available*\n\n")
2872
-
2873
- elif file_format == "txt":
2874
- # Plain text export
2875
- with open(output_path, "w", encoding="utf-8") as txtfile:
2876
- txtfile.write("LIGHTRAG DATA EXPORT\n")
2877
- txtfile.write("=" * 80 + "\n\n")
2878
-
2879
- # Entities
2880
- txtfile.write("ENTITIES\n")
2881
- txtfile.write("-" * 80 + "\n")
2882
- if entities_data:
2883
- # Create fixed width columns
2884
- col_widths = {
2885
- k: max(len(k), max(len(str(e[k])) for e in entities_data))
2886
- for k in entities_data[0]
2887
- }
2888
- header = " ".join(k.ljust(col_widths[k]) for k in entities_data[0])
2889
- txtfile.write(header + "\n")
2890
- txtfile.write("-" * len(header) + "\n")
2891
-
2892
- # Write rows
2893
- for entity in entities_data:
2894
- row = " ".join(
2895
- str(v).ljust(col_widths[k]) for k, v in entity.items()
2896
- )
2897
- txtfile.write(row + "\n")
2898
- txtfile.write("\n\n")
2899
- else:
2900
- txtfile.write("No entity data available\n\n")
2901
-
2902
- # Relations
2903
- txtfile.write("RELATIONS\n")
2904
- txtfile.write("-" * 80 + "\n")
2905
- if relations_data:
2906
- # Create fixed width columns
2907
- col_widths = {
2908
- k: max(len(k), max(len(str(r[k])) for r in relations_data))
2909
- for k in relations_data[0]
2910
- }
2911
- header = " ".join(
2912
- k.ljust(col_widths[k]) for k in relations_data[0]
2913
- )
2914
- txtfile.write(header + "\n")
2915
- txtfile.write("-" * len(header) + "\n")
2916
-
2917
- # Write rows
2918
- for relation in relations_data:
2919
- row = " ".join(
2920
- str(v).ljust(col_widths[k]) for k, v in relation.items()
2921
- )
2922
- txtfile.write(row + "\n")
2923
- txtfile.write("\n\n")
2924
- else:
2925
- txtfile.write("No relation data available\n\n")
2926
-
2927
- # Relationships
2928
- txtfile.write("RELATIONSHIPS\n")
2929
- txtfile.write("-" * 80 + "\n")
2930
- if relationships_data:
2931
- # Create fixed width columns
2932
- col_widths = {
2933
- k: max(len(k), max(len(str(r[k])) for r in relationships_data))
2934
- for k in relationships_data[0]
2935
- }
2936
- header = " ".join(
2937
- k.ljust(col_widths[k]) for k in relationships_data[0]
2938
- )
2939
- txtfile.write(header + "\n")
2940
- txtfile.write("-" * len(header) + "\n")
2941
-
2942
- # Write rows
2943
- for relationship in relationships_data:
2944
- row = " ".join(
2945
- str(v).ljust(col_widths[k]) for k, v in relationship.items()
2946
- )
2947
- txtfile.write(row + "\n")
2948
- else:
2949
- txtfile.write("No relationship data available\n\n")
2950
-
2951
- else:
2952
- raise ValueError(
2953
- f"Unsupported file format: {file_format}. "
2954
- f"Choose from: csv, excel, md, txt"
2955
- )
2956
- if file_format is not None:
2957
- print(f"Data exported to: {output_path} with format: {file_format}")
2958
- else:
2959
- print("Data displayed as table format")
2960
 
2961
  def export_data(
2962
  self,
@@ -2985,153 +2063,3 @@ class LightRAG:
2985
  loop.run_until_complete(
2986
  self.aexport_data(output_path, file_format, include_vector_data)
2987
  )
2988
-
2989
- def merge_entities(
2990
- self,
2991
- source_entities: list[str],
2992
- target_entity: str,
2993
- merge_strategy: dict[str, str] = None,
2994
- target_entity_data: dict[str, Any] = None,
2995
- ) -> dict[str, Any]:
2996
- """Synchronously merge multiple entities into one entity.
2997
-
2998
- Merges multiple source entities into a target entity, handling all relationships,
2999
- and updating both the knowledge graph and vector database.
3000
-
3001
- Args:
3002
- source_entities: List of source entity names to merge
3003
- target_entity: Name of the target entity after merging
3004
- merge_strategy: Merge strategy configuration, e.g. {"description": "concatenate", "entity_type": "keep_first"}
3005
- target_entity_data: Dictionary of specific values to set for the target entity,
3006
- overriding any merged values, e.g. {"description": "custom description", "entity_type": "PERSON"}
3007
-
3008
- Returns:
3009
- Dictionary containing the merged entity information
3010
- """
3011
- loop = always_get_an_event_loop()
3012
- return loop.run_until_complete(
3013
- self.amerge_entities(
3014
- source_entities, target_entity, merge_strategy, target_entity_data
3015
- )
3016
- )
3017
-
3018
- def _merge_entity_attributes(
3019
- self, entity_data_list: list[dict[str, Any]], merge_strategy: dict[str, str]
3020
- ) -> dict[str, Any]:
3021
- """Merge attributes from multiple entities.
3022
-
3023
- Args:
3024
- entity_data_list: List of dictionaries containing entity data
3025
- merge_strategy: Merge strategy for each field
3026
-
3027
- Returns:
3028
- Dictionary containing merged entity data
3029
- """
3030
- merged_data = {}
3031
-
3032
- # Collect all possible keys
3033
- all_keys = set()
3034
- for data in entity_data_list:
3035
- all_keys.update(data.keys())
3036
-
3037
- # Merge values for each key
3038
- for key in all_keys:
3039
- # Get all values for this key
3040
- values = [data.get(key) for data in entity_data_list if data.get(key)]
3041
-
3042
- if not values:
3043
- continue
3044
-
3045
- # Merge values according to strategy
3046
- strategy = merge_strategy.get(key, "keep_first")
3047
-
3048
- if strategy == "concatenate":
3049
- merged_data[key] = "\n\n".join(values)
3050
- elif strategy == "keep_first":
3051
- merged_data[key] = values[0]
3052
- elif strategy == "keep_last":
3053
- merged_data[key] = values[-1]
3054
- elif strategy == "join_unique":
3055
- # Handle fields separated by GRAPH_FIELD_SEP
3056
- unique_items = set()
3057
- for value in values:
3058
- items = value.split(GRAPH_FIELD_SEP)
3059
- unique_items.update(items)
3060
- merged_data[key] = GRAPH_FIELD_SEP.join(unique_items)
3061
- else:
3062
- # Default strategy
3063
- merged_data[key] = values[0]
3064
-
3065
- return merged_data
3066
-
3067
- def _merge_relation_attributes(
3068
- self, relation_data_list: list[dict[str, Any]], merge_strategy: dict[str, str]
3069
- ) -> dict[str, Any]:
3070
- """Merge attributes from multiple relationships.
3071
-
3072
- Args:
3073
- relation_data_list: List of dictionaries containing relationship data
3074
- merge_strategy: Merge strategy for each field
3075
-
3076
- Returns:
3077
- Dictionary containing merged relationship data
3078
- """
3079
- merged_data = {}
3080
-
3081
- # Collect all possible keys
3082
- all_keys = set()
3083
- for data in relation_data_list:
3084
- all_keys.update(data.keys())
3085
-
3086
- # Merge values for each key
3087
- for key in all_keys:
3088
- # Get all values for this key
3089
- values = [
3090
- data.get(key)
3091
- for data in relation_data_list
3092
- if data.get(key) is not None
3093
- ]
3094
-
3095
- if not values:
3096
- continue
3097
-
3098
- # Merge values according to strategy
3099
- strategy = merge_strategy.get(key, "keep_first")
3100
-
3101
- if strategy == "concatenate":
3102
- merged_data[key] = "\n\n".join(str(v) for v in values)
3103
- elif strategy == "keep_first":
3104
- merged_data[key] = values[0]
3105
- elif strategy == "keep_last":
3106
- merged_data[key] = values[-1]
3107
- elif strategy == "join_unique":
3108
- # Handle fields separated by GRAPH_FIELD_SEP
3109
- unique_items = set()
3110
- for value in values:
3111
- items = str(value).split(GRAPH_FIELD_SEP)
3112
- unique_items.update(items)
3113
- merged_data[key] = GRAPH_FIELD_SEP.join(unique_items)
3114
- elif strategy == "max":
3115
- # For numeric fields like weight
3116
- try:
3117
- merged_data[key] = max(float(v) for v in values)
3118
- except (ValueError, TypeError):
3119
- merged_data[key] = values[0]
3120
- else:
3121
- # Default strategy
3122
- merged_data[key] = values[0]
3123
-
3124
- return merged_data
3125
-
3126
- async def _merge_entities_done(self) -> None:
3127
- """Callback after entity merging is complete, ensures updates are persisted"""
3128
- await asyncio.gather(
3129
- *[
3130
- cast(StorageNameSpace, storage_inst).index_done_callback()
3131
- for storage_inst in [ # type: ignore
3132
- self.entities_vdb,
3133
- self.relationships_vdb,
3134
- self.chunk_entity_relation_graph,
3135
- ]
3136
- ]
3137
- )
 
3
  import asyncio
4
  import configparser
5
  import os
 
6
  import warnings
7
  from dataclasses import asdict, dataclass, field
8
  from datetime import datetime
9
  from functools import partial
10
  from typing import Any, AsyncIterator, Callable, Iterator, cast, final, Literal
 
 
11
 
12
  from lightrag.kg import (
13
  STORAGES,
14
  verify_storage_implementation,
15
  )
16
 
17
+ from lightrag.kg.shared_storage import (
18
+ get_namespace_data,
19
+ get_pipeline_status_lock,
20
+ )
21
+
22
  from .base import (
23
  BaseGraphStorage,
24
  BaseKVStorage,
 
781
  3. Process each chunk for entity and relation extraction
782
  4. Update the document status
783
  """
 
 
 
 
784
 
785
  # Get pipeline status shared data and lock
786
  pipeline_status = await get_namespace_data("pipeline_status")
 
1425
  async def _query_done(self):
1426
  await self.llm_response_cache.index_done_callback()
1427
 
1428
+ async def aclear_cache(self, modes: list[str] | None = None) -> None:
1429
+ """Clear cache data from the LLM response cache storage.
 
1430
 
1431
+ Args:
1432
+ modes (list[str] | None): Modes of cache to clear. Options: ["default", "naive", "local", "global", "hybrid", "mix"].
1433
+ "default" represents extraction cache.
1434
+ If None, clears all cache.
 
 
1435
 
1436
+ Example:
1437
+ # Clear all cache
1438
+ await rag.aclear_cache()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1439
 
1440
+ # Clear local mode cache
1441
+ await rag.aclear_cache(modes=["local"])
1442
 
1443
+ # Clear extraction cache
1444
+ await rag.aclear_cache(modes=["default"])
 
1445
  """
1446
+ if not self.llm_response_cache:
1447
+ logger.warning("No cache storage configured")
1448
+ return
 
1449
 
1450
+ valid_modes = ["default", "naive", "local", "global", "hybrid", "mix"]
 
 
1451
 
1452
+ # Validate input
1453
+ if modes and not all(mode in valid_modes for mode in modes):
1454
+ raise ValueError(f"Invalid mode. Valid modes are: {valid_modes}")
 
 
 
 
 
 
 
 
 
 
 
 
1455
 
1456
+ try:
1457
+ # Reset the cache storage for specified mode
1458
+ if modes:
1459
+ success = await self.llm_response_cache.drop_cache_by_modes(modes)
1460
+ if success:
1461
+ logger.info(f"Cleared cache for modes: {modes}")
1462
+ else:
1463
+ logger.warning(f"Failed to clear cache for modes: {modes}")
1464
+ else:
1465
+ # Clear all modes
1466
+ success = await self.llm_response_cache.drop_cache_by_modes(valid_modes)
1467
+ if success:
1468
+ logger.info("Cleared all cache")
1469
+ else:
1470
+ logger.warning("Failed to clear all cache")
1471
 
1472
+ await self.llm_response_cache.index_done_callback()
 
 
 
1473
 
 
 
 
 
1474
  except Exception as e:
1475
+ logger.error(f"Error while clearing cache: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1476
 
1477
+ def clear_cache(self, modes: list[str] | None = None) -> None:
1478
+ """Synchronous version of aclear_cache."""
1479
+ return always_get_an_event_loop().run_until_complete(self.aclear_cache(modes))
 
1480
 
1481
  async def get_docs_by_status(
1482
  self, status: DocStatus
 
1488
  """
1489
  return await self.doc_status.get_docs_by_status(status)
1490
 
1491
+ # TODO: Deprecated (Deleting documents can cause hallucinations in RAG.)
1492
+ # Document delete is not working properly for most of the storage implementations.
1493
  async def adelete_by_doc_id(self, doc_id: str) -> None:
1494
  """Delete a document and all its related data
1495
 
 
1746
  except Exception as e:
1747
  logger.error(f"Error while deleting document {doc_id}: {e}")
1748
 
1749
+ async def adelete_by_entity(self, entity_name: str) -> None:
1750
+ """Asynchronously delete an entity and all its relationships.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1751
 
1752
+ Args:
1753
+ entity_name: Name of the entity to delete
1754
+ """
1755
+ from .utils_graph import adelete_by_entity
1756
 
1757
+ return await adelete_by_entity(
1758
+ self.chunk_entity_relation_graph,
1759
+ self.entities_vdb,
1760
+ self.relationships_vdb,
1761
+ entity_name,
1762
  )
 
 
 
 
 
 
 
 
1763
 
1764
+ def delete_by_entity(self, entity_name: str) -> None:
1765
+ loop = always_get_an_event_loop()
1766
+ return loop.run_until_complete(self.adelete_by_entity(entity_name))
 
 
 
 
1767
 
1768
+ async def adelete_by_relation(self, source_entity: str, target_entity: str) -> None:
1769
+ """Asynchronously delete a relation between two entities.
1770
 
1771
  Args:
1772
+ source_entity: Name of the source entity
1773
+ target_entity: Name of the target entity
 
 
 
 
 
 
 
 
 
 
 
1774
  """
1775
+ from .utils_graph import adelete_by_relation
 
 
1776
 
1777
+ return await adelete_by_relation(
1778
+ self.chunk_entity_relation_graph,
1779
+ self.relationships_vdb,
1780
+ source_entity,
1781
+ target_entity,
1782
+ )
1783
 
1784
+ def delete_by_relation(self, source_entity: str, target_entity: str) -> None:
1785
+ loop = always_get_an_event_loop()
1786
+ return loop.run_until_complete(
1787
+ self.adelete_by_relation(source_entity, target_entity)
1788
+ )
1789
 
1790
+ async def get_processing_status(self) -> dict[str, int]:
1791
+ """Get current document processing status counts
 
 
 
 
 
 
 
 
 
 
 
 
 
1792
 
1793
+ Returns:
1794
+ Dict with counts for each status
1795
+ """
1796
+ return await self.doc_status.get_status_counts()
1797
 
1798
+ async def get_entity_info(
1799
+ self, entity_name: str, include_vector_data: bool = False
1800
+ ) -> dict[str, str | None | dict[str, str]]:
1801
+ """Get detailed information of an entity"""
1802
+ from .utils_graph import get_entity_info
1803
 
1804
+ return await get_entity_info(
1805
+ self.chunk_entity_relation_graph,
1806
+ self.entities_vdb,
1807
+ entity_name,
1808
+ include_vector_data,
1809
+ )
1810
+
1811
+ async def get_relation_info(
1812
+ self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
1813
+ ) -> dict[str, str | None | dict[str, str]]:
1814
+ """Get detailed information of a relationship"""
1815
+ from .utils_graph import get_relation_info
1816
+
1817
+ return await get_relation_info(
1818
+ self.chunk_entity_relation_graph,
1819
+ self.relationships_vdb,
1820
+ src_entity,
1821
+ tgt_entity,
1822
+ include_vector_data,
1823
+ )
1824
 
 
1825
  async def aedit_entity(
1826
  self, entity_name: str, updated_data: dict[str, str], allow_rename: bool = True
1827
  ) -> dict[str, Any]:
 
1837
  Returns:
1838
  Dictionary containing updated entity information
1839
  """
1840
+ from .utils_graph import aedit_entity
1841
+
1842
+ return await aedit_entity(
1843
+ self.chunk_entity_relation_graph,
1844
+ self.entities_vdb,
1845
+ self.relationships_vdb,
1846
+ entity_name,
1847
+ updated_data,
1848
+ allow_rename,
1849
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1850
 
1851
  def edit_entity(
1852
  self, entity_name: str, updated_data: dict[str, str], allow_rename: bool = True
1853
  ) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
1854
  loop = always_get_an_event_loop()
1855
  return loop.run_until_complete(
1856
  self.aedit_entity(entity_name, updated_data, allow_rename)
1857
  )
1858
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1859
  async def aedit_relation(
1860
  self, source_entity: str, target_entity: str, updated_data: dict[str, Any]
1861
  ) -> dict[str, Any]:
 
1871
  Returns:
1872
  Dictionary containing updated relation information
1873
  """
1874
+ from .utils_graph import aedit_relation
1875
+
1876
+ return await aedit_relation(
1877
+ self.chunk_entity_relation_graph,
1878
+ self.entities_vdb,
1879
+ self.relationships_vdb,
1880
+ source_entity,
1881
+ target_entity,
1882
+ updated_data,
1883
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1884
 
1885
  def edit_relation(
1886
  self, source_entity: str, target_entity: str, updated_data: dict[str, Any]
1887
  ) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
1888
  loop = always_get_an_event_loop()
1889
  return loop.run_until_complete(
1890
  self.aedit_relation(source_entity, target_entity, updated_data)
1891
  )
1892
 
 
 
 
 
 
 
 
 
 
 
 
 
1893
  async def acreate_entity(
1894
  self, entity_name: str, entity_data: dict[str, Any]
1895
  ) -> dict[str, Any]:
 
1904
  Returns:
1905
  Dictionary containing created entity information
1906
  """
1907
+ from .utils_graph import acreate_entity
1908
+
1909
+ return await acreate_entity(
1910
+ self.chunk_entity_relation_graph,
1911
+ self.entities_vdb,
1912
+ self.relationships_vdb,
1913
+ entity_name,
1914
+ entity_data,
1915
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1916
 
1917
  def create_entity(
1918
  self, entity_name: str, entity_data: dict[str, Any]
1919
  ) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
1920
  loop = always_get_an_event_loop()
1921
  return loop.run_until_complete(self.acreate_entity(entity_name, entity_data))
1922
 
 
1935
  Returns:
1936
  Dictionary containing created relation information
1937
  """
1938
+ from .utils_graph import acreate_relation
1939
+
1940
+ return await acreate_relation(
1941
+ self.chunk_entity_relation_graph,
1942
+ self.entities_vdb,
1943
+ self.relationships_vdb,
1944
+ source_entity,
1945
+ target_entity,
1946
+ relation_data,
1947
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1948
 
1949
  def create_relation(
1950
  self, source_entity: str, target_entity: str, relation_data: dict[str, Any]
1951
  ) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
1952
  loop = always_get_an_event_loop()
1953
  return loop.run_until_complete(
1954
  self.acreate_relation(source_entity, target_entity, relation_data)
1955
  )
1956
 
 
1957
  async def amerge_entities(
1958
  self,
1959
  source_entities: list[str],
 
1981
  Returns:
1982
  Dictionary containing the merged entity information
1983
  """
1984
+ from .utils_graph import amerge_entities
1985
+
1986
+ return await amerge_entities(
1987
+ self.chunk_entity_relation_graph,
1988
+ self.entities_vdb,
1989
+ self.relationships_vdb,
1990
+ source_entities,
1991
+ target_entity,
1992
+ merge_strategy,
1993
+ target_entity_data,
1994
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1995
 
1996
+ def merge_entities(
1997
+ self,
1998
+ source_entities: list[str],
1999
+ target_entity: str,
2000
+ merge_strategy: dict[str, str] = None,
2001
+ target_entity_data: dict[str, Any] = None,
2002
+ ) -> dict[str, Any]:
2003
+ loop = always_get_an_event_loop()
2004
+ return loop.run_until_complete(
2005
+ self.amerge_entities(
2006
+ source_entities, target_entity, merge_strategy, target_entity_data
2007
  )
2008
+ )
 
 
 
 
2009
 
2010
  async def aexport_data(
2011
  self,
 
2025
  - table: Print formatted tables to console
2026
  include_vector_data: Whether to include data from the vector database.
2027
  """
2028
+ from .utils import aexport_data as utils_aexport_data
2029
+
2030
+ await utils_aexport_data(
2031
+ self.chunk_entity_relation_graph,
2032
+ self.entities_vdb,
2033
+ self.relationships_vdb,
2034
+ output_path,
2035
+ file_format,
2036
+ include_vector_data,
2037
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2038
 
2039
  def export_data(
2040
  self,
 
2063
  loop.run_until_complete(
2064
  self.aexport_data(output_path, file_format, include_vector_data)
2065
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lightrag/utils.py CHANGED
@@ -893,6 +893,351 @@ def always_get_an_event_loop() -> asyncio.AbstractEventLoop:
893
  return new_loop
894
 
895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
  def lazy_external_import(module_name: str, class_name: str) -> Callable[..., Any]:
897
  """Lazily import a class from an external module based on the package of the caller."""
898
  # Get the caller's module and package
 
893
  return new_loop
894
 
895
 
896
+ async def aexport_data(
897
+ chunk_entity_relation_graph,
898
+ entities_vdb,
899
+ relationships_vdb,
900
+ output_path: str,
901
+ file_format: str = "csv",
902
+ include_vector_data: bool = False,
903
+ ) -> None:
904
+ """
905
+ Asynchronously exports all entities, relations, and relationships to various formats.
906
+
907
+ Args:
908
+ chunk_entity_relation_graph: Graph storage instance for entities and relations
909
+ entities_vdb: Vector database storage for entities
910
+ relationships_vdb: Vector database storage for relationships
911
+ output_path: The path to the output file (including extension).
912
+ file_format: Output format - "csv", "excel", "md", "txt".
913
+ - csv: Comma-separated values file
914
+ - excel: Microsoft Excel file with multiple sheets
915
+ - md: Markdown tables
916
+ - txt: Plain text formatted output
917
+ include_vector_data: Whether to include data from the vector database.
918
+ """
919
+ # Collect data
920
+ entities_data = []
921
+ relations_data = []
922
+ relationships_data = []
923
+
924
+ # --- Entities ---
925
+ all_entities = await chunk_entity_relation_graph.get_all_labels()
926
+ for entity_name in all_entities:
927
+ # Get entity information from graph
928
+ node_data = await chunk_entity_relation_graph.get_node(entity_name)
929
+ source_id = node_data.get("source_id") if node_data else None
930
+
931
+ entity_info = {
932
+ "graph_data": node_data,
933
+ "source_id": source_id,
934
+ }
935
+
936
+ # Optional: Get vector database information
937
+ if include_vector_data:
938
+ entity_id = compute_mdhash_id(entity_name, prefix="ent-")
939
+ vector_data = await entities_vdb.get_by_id(entity_id)
940
+ entity_info["vector_data"] = vector_data
941
+
942
+ entity_row = {
943
+ "entity_name": entity_name,
944
+ "source_id": source_id,
945
+ "graph_data": str(
946
+ entity_info["graph_data"]
947
+ ), # Convert to string to ensure compatibility
948
+ }
949
+ if include_vector_data and "vector_data" in entity_info:
950
+ entity_row["vector_data"] = str(entity_info["vector_data"])
951
+ entities_data.append(entity_row)
952
+
953
+ # --- Relations ---
954
+ for src_entity in all_entities:
955
+ for tgt_entity in all_entities:
956
+ if src_entity == tgt_entity:
957
+ continue
958
+
959
+ edge_exists = await chunk_entity_relation_graph.has_edge(
960
+ src_entity, tgt_entity
961
+ )
962
+ if edge_exists:
963
+ # Get edge information from graph
964
+ edge_data = await chunk_entity_relation_graph.get_edge(
965
+ src_entity, tgt_entity
966
+ )
967
+ source_id = edge_data.get("source_id") if edge_data else None
968
+
969
+ relation_info = {
970
+ "graph_data": edge_data,
971
+ "source_id": source_id,
972
+ }
973
+
974
+ # Optional: Get vector database information
975
+ if include_vector_data:
976
+ rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
977
+ vector_data = await relationships_vdb.get_by_id(rel_id)
978
+ relation_info["vector_data"] = vector_data
979
+
980
+ relation_row = {
981
+ "src_entity": src_entity,
982
+ "tgt_entity": tgt_entity,
983
+ "source_id": relation_info["source_id"],
984
+ "graph_data": str(relation_info["graph_data"]), # Convert to string
985
+ }
986
+ if include_vector_data and "vector_data" in relation_info:
987
+ relation_row["vector_data"] = str(relation_info["vector_data"])
988
+ relations_data.append(relation_row)
989
+
990
+ # --- Relationships (from VectorDB) ---
991
+ all_relationships = await relationships_vdb.client_storage
992
+ for rel in all_relationships["data"]:
993
+ relationships_data.append(
994
+ {
995
+ "relationship_id": rel["__id__"],
996
+ "data": str(rel), # Convert to string for compatibility
997
+ }
998
+ )
999
+
1000
+ # Export based on format
1001
+ if file_format == "csv":
1002
+ # CSV export
1003
+ with open(output_path, "w", newline="", encoding="utf-8") as csvfile:
1004
+ # Entities
1005
+ if entities_data:
1006
+ csvfile.write("# ENTITIES\n")
1007
+ writer = csv.DictWriter(csvfile, fieldnames=entities_data[0].keys())
1008
+ writer.writeheader()
1009
+ writer.writerows(entities_data)
1010
+ csvfile.write("\n\n")
1011
+
1012
+ # Relations
1013
+ if relations_data:
1014
+ csvfile.write("# RELATIONS\n")
1015
+ writer = csv.DictWriter(csvfile, fieldnames=relations_data[0].keys())
1016
+ writer.writeheader()
1017
+ writer.writerows(relations_data)
1018
+ csvfile.write("\n\n")
1019
+
1020
+ # Relationships
1021
+ if relationships_data:
1022
+ csvfile.write("# RELATIONSHIPS\n")
1023
+ writer = csv.DictWriter(
1024
+ csvfile, fieldnames=relationships_data[0].keys()
1025
+ )
1026
+ writer.writeheader()
1027
+ writer.writerows(relationships_data)
1028
+
1029
+ elif file_format == "excel":
1030
+ # Excel export
1031
+ import pandas as pd
1032
+
1033
+ entities_df = pd.DataFrame(entities_data) if entities_data else pd.DataFrame()
1034
+ relations_df = (
1035
+ pd.DataFrame(relations_data) if relations_data else pd.DataFrame()
1036
+ )
1037
+ relationships_df = (
1038
+ pd.DataFrame(relationships_data) if relationships_data else pd.DataFrame()
1039
+ )
1040
+
1041
+ with pd.ExcelWriter(output_path, engine="xlsxwriter") as writer:
1042
+ if not entities_df.empty:
1043
+ entities_df.to_excel(writer, sheet_name="Entities", index=False)
1044
+ if not relations_df.empty:
1045
+ relations_df.to_excel(writer, sheet_name="Relations", index=False)
1046
+ if not relationships_df.empty:
1047
+ relationships_df.to_excel(
1048
+ writer, sheet_name="Relationships", index=False
1049
+ )
1050
+
1051
+ elif file_format == "md":
1052
+ # Markdown export
1053
+ with open(output_path, "w", encoding="utf-8") as mdfile:
1054
+ mdfile.write("# LightRAG Data Export\n\n")
1055
+
1056
+ # Entities
1057
+ mdfile.write("## Entities\n\n")
1058
+ if entities_data:
1059
+ # Write header
1060
+ mdfile.write("| " + " | ".join(entities_data[0].keys()) + " |\n")
1061
+ mdfile.write(
1062
+ "| " + " | ".join(["---"] * len(entities_data[0].keys())) + " |\n"
1063
+ )
1064
+
1065
+ # Write rows
1066
+ for entity in entities_data:
1067
+ mdfile.write(
1068
+ "| " + " | ".join(str(v) for v in entity.values()) + " |\n"
1069
+ )
1070
+ mdfile.write("\n\n")
1071
+ else:
1072
+ mdfile.write("*No entity data available*\n\n")
1073
+
1074
+ # Relations
1075
+ mdfile.write("## Relations\n\n")
1076
+ if relations_data:
1077
+ # Write header
1078
+ mdfile.write("| " + " | ".join(relations_data[0].keys()) + " |\n")
1079
+ mdfile.write(
1080
+ "| " + " | ".join(["---"] * len(relations_data[0].keys())) + " |\n"
1081
+ )
1082
+
1083
+ # Write rows
1084
+ for relation in relations_data:
1085
+ mdfile.write(
1086
+ "| " + " | ".join(str(v) for v in relation.values()) + " |\n"
1087
+ )
1088
+ mdfile.write("\n\n")
1089
+ else:
1090
+ mdfile.write("*No relation data available*\n\n")
1091
+
1092
+ # Relationships
1093
+ mdfile.write("## Relationships\n\n")
1094
+ if relationships_data:
1095
+ # Write header
1096
+ mdfile.write("| " + " | ".join(relationships_data[0].keys()) + " |\n")
1097
+ mdfile.write(
1098
+ "| "
1099
+ + " | ".join(["---"] * len(relationships_data[0].keys()))
1100
+ + " |\n"
1101
+ )
1102
+
1103
+ # Write rows
1104
+ for relationship in relationships_data:
1105
+ mdfile.write(
1106
+ "| "
1107
+ + " | ".join(str(v) for v in relationship.values())
1108
+ + " |\n"
1109
+ )
1110
+ else:
1111
+ mdfile.write("*No relationship data available*\n\n")
1112
+
1113
+ elif file_format == "txt":
1114
+ # Plain text export
1115
+ with open(output_path, "w", encoding="utf-8") as txtfile:
1116
+ txtfile.write("LIGHTRAG DATA EXPORT\n")
1117
+ txtfile.write("=" * 80 + "\n\n")
1118
+
1119
+ # Entities
1120
+ txtfile.write("ENTITIES\n")
1121
+ txtfile.write("-" * 80 + "\n")
1122
+ if entities_data:
1123
+ # Create fixed width columns
1124
+ col_widths = {
1125
+ k: max(len(k), max(len(str(e[k])) for e in entities_data))
1126
+ for k in entities_data[0]
1127
+ }
1128
+ header = " ".join(k.ljust(col_widths[k]) for k in entities_data[0])
1129
+ txtfile.write(header + "\n")
1130
+ txtfile.write("-" * len(header) + "\n")
1131
+
1132
+ # Write rows
1133
+ for entity in entities_data:
1134
+ row = " ".join(
1135
+ str(v).ljust(col_widths[k]) for k, v in entity.items()
1136
+ )
1137
+ txtfile.write(row + "\n")
1138
+ txtfile.write("\n\n")
1139
+ else:
1140
+ txtfile.write("No entity data available\n\n")
1141
+
1142
+ # Relations
1143
+ txtfile.write("RELATIONS\n")
1144
+ txtfile.write("-" * 80 + "\n")
1145
+ if relations_data:
1146
+ # Create fixed width columns
1147
+ col_widths = {
1148
+ k: max(len(k), max(len(str(r[k])) for r in relations_data))
1149
+ for k in relations_data[0]
1150
+ }
1151
+ header = " ".join(k.ljust(col_widths[k]) for k in relations_data[0])
1152
+ txtfile.write(header + "\n")
1153
+ txtfile.write("-" * len(header) + "\n")
1154
+
1155
+ # Write rows
1156
+ for relation in relations_data:
1157
+ row = " ".join(
1158
+ str(v).ljust(col_widths[k]) for k, v in relation.items()
1159
+ )
1160
+ txtfile.write(row + "\n")
1161
+ txtfile.write("\n\n")
1162
+ else:
1163
+ txtfile.write("No relation data available\n\n")
1164
+
1165
+ # Relationships
1166
+ txtfile.write("RELATIONSHIPS\n")
1167
+ txtfile.write("-" * 80 + "\n")
1168
+ if relationships_data:
1169
+ # Create fixed width columns
1170
+ col_widths = {
1171
+ k: max(len(k), max(len(str(r[k])) for r in relationships_data))
1172
+ for k in relationships_data[0]
1173
+ }
1174
+ header = " ".join(
1175
+ k.ljust(col_widths[k]) for k in relationships_data[0]
1176
+ )
1177
+ txtfile.write(header + "\n")
1178
+ txtfile.write("-" * len(header) + "\n")
1179
+
1180
+ # Write rows
1181
+ for relationship in relationships_data:
1182
+ row = " ".join(
1183
+ str(v).ljust(col_widths[k]) for k, v in relationship.items()
1184
+ )
1185
+ txtfile.write(row + "\n")
1186
+ else:
1187
+ txtfile.write("No relationship data available\n\n")
1188
+
1189
+ else:
1190
+ raise ValueError(
1191
+ f"Unsupported file format: {file_format}. "
1192
+ f"Choose from: csv, excel, md, txt"
1193
+ )
1194
+ if file_format is not None:
1195
+ print(f"Data exported to: {output_path} with format: {file_format}")
1196
+ else:
1197
+ print("Data displayed as table format")
1198
+
1199
+
1200
+ def export_data(
1201
+ chunk_entity_relation_graph,
1202
+ entities_vdb,
1203
+ relationships_vdb,
1204
+ output_path: str,
1205
+ file_format: str = "csv",
1206
+ include_vector_data: bool = False,
1207
+ ) -> None:
1208
+ """
1209
+ Synchronously exports all entities, relations, and relationships to various formats.
1210
+
1211
+ Args:
1212
+ chunk_entity_relation_graph: Graph storage instance for entities and relations
1213
+ entities_vdb: Vector database storage for entities
1214
+ relationships_vdb: Vector database storage for relationships
1215
+ output_path: The path to the output file (including extension).
1216
+ file_format: Output format - "csv", "excel", "md", "txt".
1217
+ - csv: Comma-separated values file
1218
+ - excel: Microsoft Excel file with multiple sheets
1219
+ - md: Markdown tables
1220
+ - txt: Plain text formatted output
1221
+ include_vector_data: Whether to include data from the vector database.
1222
+ """
1223
+ try:
1224
+ loop = asyncio.get_event_loop()
1225
+ except RuntimeError:
1226
+ loop = asyncio.new_event_loop()
1227
+ asyncio.set_event_loop(loop)
1228
+
1229
+ loop.run_until_complete(
1230
+ aexport_data(
1231
+ chunk_entity_relation_graph,
1232
+ entities_vdb,
1233
+ relationships_vdb,
1234
+ output_path,
1235
+ file_format,
1236
+ include_vector_data,
1237
+ )
1238
+ )
1239
+
1240
+
1241
  def lazy_external_import(module_name: str, class_name: str) -> Callable[..., Any]:
1242
  """Lazily import a class from an external module based on the package of the caller."""
1243
  # Get the caller's module and package
lightrag/utils_graph.py ADDED
@@ -0,0 +1,1066 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, cast
5
+
6
+ from .kg.shared_storage import get_graph_db_lock
7
+ from .prompt import GRAPH_FIELD_SEP
8
+ from .utils import compute_mdhash_id, logger
9
+ from .base import StorageNameSpace
10
+
11
+
12
+ async def adelete_by_entity(
13
+ chunk_entity_relation_graph, entities_vdb, relationships_vdb, entity_name: str
14
+ ) -> None:
15
+ """Asynchronously delete an entity and all its relationships.
16
+
17
+ Args:
18
+ chunk_entity_relation_graph: Graph storage instance
19
+ entities_vdb: Vector database storage for entities
20
+ relationships_vdb: Vector database storage for relationships
21
+ entity_name: Name of the entity to delete
22
+ """
23
+ graph_db_lock = get_graph_db_lock(enable_logging=False)
24
+ # Use graph database lock to ensure atomic graph and vector db operations
25
+ async with graph_db_lock:
26
+ try:
27
+ await entities_vdb.delete_entity(entity_name)
28
+ await relationships_vdb.delete_entity_relation(entity_name)
29
+ await chunk_entity_relation_graph.delete_node(entity_name)
30
+
31
+ logger.info(
32
+ f"Entity '{entity_name}' and its relationships have been deleted."
33
+ )
34
+ await _delete_by_entity_done(
35
+ entities_vdb, relationships_vdb, chunk_entity_relation_graph
36
+ )
37
+ except Exception as e:
38
+ logger.error(f"Error while deleting entity '{entity_name}': {e}")
39
+
40
+
41
+ async def _delete_by_entity_done(
42
+ entities_vdb, relationships_vdb, chunk_entity_relation_graph
43
+ ) -> None:
44
+ """Callback after entity deletion is complete, ensures updates are persisted"""
45
+ await asyncio.gather(
46
+ *[
47
+ cast(StorageNameSpace, storage_inst).index_done_callback()
48
+ for storage_inst in [ # type: ignore
49
+ entities_vdb,
50
+ relationships_vdb,
51
+ chunk_entity_relation_graph,
52
+ ]
53
+ ]
54
+ )
55
+
56
+
57
+ async def adelete_by_relation(
58
+ chunk_entity_relation_graph,
59
+ relationships_vdb,
60
+ source_entity: str,
61
+ target_entity: str,
62
+ ) -> None:
63
+ """Asynchronously delete a relation between two entities.
64
+
65
+ Args:
66
+ chunk_entity_relation_graph: Graph storage instance
67
+ relationships_vdb: Vector database storage for relationships
68
+ source_entity: Name of the source entity
69
+ target_entity: Name of the target entity
70
+ """
71
+ graph_db_lock = get_graph_db_lock(enable_logging=False)
72
+ # Use graph database lock to ensure atomic graph and vector db operations
73
+ async with graph_db_lock:
74
+ try:
75
+ # Check if the relation exists
76
+ edge_exists = await chunk_entity_relation_graph.has_edge(
77
+ source_entity, target_entity
78
+ )
79
+ if not edge_exists:
80
+ logger.warning(
81
+ f"Relation from '{source_entity}' to '{target_entity}' does not exist"
82
+ )
83
+ return
84
+
85
+ # Delete relation from vector database
86
+ relation_id = compute_mdhash_id(
87
+ source_entity + target_entity, prefix="rel-"
88
+ )
89
+ await relationships_vdb.delete([relation_id])
90
+
91
+ # Delete relation from knowledge graph
92
+ await chunk_entity_relation_graph.remove_edges(
93
+ [(source_entity, target_entity)]
94
+ )
95
+
96
+ logger.info(
97
+ f"Successfully deleted relation from '{source_entity}' to '{target_entity}'"
98
+ )
99
+ await _delete_relation_done(relationships_vdb, chunk_entity_relation_graph)
100
+ except Exception as e:
101
+ logger.error(
102
+ f"Error while deleting relation from '{source_entity}' to '{target_entity}': {e}"
103
+ )
104
+
105
+
106
+ async def _delete_relation_done(relationships_vdb, chunk_entity_relation_graph) -> None:
107
+ """Callback after relation deletion is complete, ensures updates are persisted"""
108
+ await asyncio.gather(
109
+ *[
110
+ cast(StorageNameSpace, storage_inst).index_done_callback()
111
+ for storage_inst in [ # type: ignore
112
+ relationships_vdb,
113
+ chunk_entity_relation_graph,
114
+ ]
115
+ ]
116
+ )
117
+
118
+
119
+ async def aedit_entity(
120
+ chunk_entity_relation_graph,
121
+ entities_vdb,
122
+ relationships_vdb,
123
+ entity_name: str,
124
+ updated_data: dict[str, str],
125
+ allow_rename: bool = True,
126
+ ) -> dict[str, Any]:
127
+ """Asynchronously edit entity information.
128
+
129
+ Updates entity information in the knowledge graph and re-embeds the entity in the vector database.
130
+
131
+ Args:
132
+ chunk_entity_relation_graph: Graph storage instance
133
+ entities_vdb: Vector database storage for entities
134
+ relationships_vdb: Vector database storage for relationships
135
+ entity_name: Name of the entity to edit
136
+ updated_data: Dictionary containing updated attributes, e.g. {"description": "new description", "entity_type": "new type"}
137
+ allow_rename: Whether to allow entity renaming, defaults to True
138
+
139
+ Returns:
140
+ Dictionary containing updated entity information
141
+ """
142
+ graph_db_lock = get_graph_db_lock(enable_logging=False)
143
+ # Use graph database lock to ensure atomic graph and vector db operations
144
+ async with graph_db_lock:
145
+ try:
146
+ # 1. Get current entity information
147
+ node_exists = await chunk_entity_relation_graph.has_node(entity_name)
148
+ if not node_exists:
149
+ raise ValueError(f"Entity '{entity_name}' does not exist")
150
+ node_data = await chunk_entity_relation_graph.get_node(entity_name)
151
+
152
+ # Check if entity is being renamed
153
+ new_entity_name = updated_data.get("entity_name", entity_name)
154
+ is_renaming = new_entity_name != entity_name
155
+
156
+ # If renaming, check if new name already exists
157
+ if is_renaming:
158
+ if not allow_rename:
159
+ raise ValueError(
160
+ "Entity renaming is not allowed. Set allow_rename=True to enable this feature"
161
+ )
162
+
163
+ existing_node = await chunk_entity_relation_graph.has_node(
164
+ new_entity_name
165
+ )
166
+ if existing_node:
167
+ raise ValueError(
168
+ f"Entity name '{new_entity_name}' already exists, cannot rename"
169
+ )
170
+
171
+ # 2. Update entity information in the graph
172
+ new_node_data = {**node_data, **updated_data}
173
+ new_node_data["entity_id"] = new_entity_name
174
+
175
+ if "entity_name" in new_node_data:
176
+ del new_node_data[
177
+ "entity_name"
178
+ ] # Node data should not contain entity_name field
179
+
180
+ # If renaming entity
181
+ if is_renaming:
182
+ logger.info(f"Renaming entity '{entity_name}' to '{new_entity_name}'")
183
+
184
+ # Create new entity
185
+ await chunk_entity_relation_graph.upsert_node(
186
+ new_entity_name, new_node_data
187
+ )
188
+
189
+ # Store relationships that need to be updated
190
+ relations_to_update = []
191
+ relations_to_delete = []
192
+ # Get all edges related to the original entity
193
+ edges = await chunk_entity_relation_graph.get_node_edges(entity_name)
194
+ if edges:
195
+ # Recreate edges for the new entity
196
+ for source, target in edges:
197
+ edge_data = await chunk_entity_relation_graph.get_edge(
198
+ source, target
199
+ )
200
+ if edge_data:
201
+ relations_to_delete.append(
202
+ compute_mdhash_id(source + target, prefix="rel-")
203
+ )
204
+ relations_to_delete.append(
205
+ compute_mdhash_id(target + source, prefix="rel-")
206
+ )
207
+ if source == entity_name:
208
+ await chunk_entity_relation_graph.upsert_edge(
209
+ new_entity_name, target, edge_data
210
+ )
211
+ relations_to_update.append(
212
+ (new_entity_name, target, edge_data)
213
+ )
214
+ else: # target == entity_name
215
+ await chunk_entity_relation_graph.upsert_edge(
216
+ source, new_entity_name, edge_data
217
+ )
218
+ relations_to_update.append(
219
+ (source, new_entity_name, edge_data)
220
+ )
221
+
222
+ # Delete old entity
223
+ await chunk_entity_relation_graph.delete_node(entity_name)
224
+
225
+ # Delete old entity record from vector database
226
+ old_entity_id = compute_mdhash_id(entity_name, prefix="ent-")
227
+ await entities_vdb.delete([old_entity_id])
228
+ logger.info(
229
+ f"Deleted old entity '{entity_name}' and its vector embedding from database"
230
+ )
231
+
232
+ # Delete old relation records from vector database
233
+ await relationships_vdb.delete(relations_to_delete)
234
+ logger.info(
235
+ f"Deleted {len(relations_to_delete)} relation records for entity '{entity_name}' from vector database"
236
+ )
237
+
238
+ # Update relationship vector representations
239
+ for src, tgt, edge_data in relations_to_update:
240
+ description = edge_data.get("description", "")
241
+ keywords = edge_data.get("keywords", "")
242
+ source_id = edge_data.get("source_id", "")
243
+ weight = float(edge_data.get("weight", 1.0))
244
+
245
+ # Create new content for embedding
246
+ content = f"{src}\t{tgt}\n{keywords}\n{description}"
247
+
248
+ # Calculate relationship ID
249
+ relation_id = compute_mdhash_id(src + tgt, prefix="rel-")
250
+
251
+ # Prepare data for vector database update
252
+ relation_data = {
253
+ relation_id: {
254
+ "content": content,
255
+ "src_id": src,
256
+ "tgt_id": tgt,
257
+ "source_id": source_id,
258
+ "description": description,
259
+ "keywords": keywords,
260
+ "weight": weight,
261
+ }
262
+ }
263
+
264
+ # Update vector database
265
+ await relationships_vdb.upsert(relation_data)
266
+
267
+ # Update working entity name to new name
268
+ entity_name = new_entity_name
269
+ else:
270
+ # If not renaming, directly update node data
271
+ await chunk_entity_relation_graph.upsert_node(
272
+ entity_name, new_node_data
273
+ )
274
+
275
+ # 3. Recalculate entity's vector representation and update vector database
276
+ description = new_node_data.get("description", "")
277
+ source_id = new_node_data.get("source_id", "")
278
+ entity_type = new_node_data.get("entity_type", "")
279
+ content = entity_name + "\n" + description
280
+
281
+ # Calculate entity ID
282
+ entity_id = compute_mdhash_id(entity_name, prefix="ent-")
283
+
284
+ # Prepare data for vector database update
285
+ entity_data = {
286
+ entity_id: {
287
+ "content": content,
288
+ "entity_name": entity_name,
289
+ "source_id": source_id,
290
+ "description": description,
291
+ "entity_type": entity_type,
292
+ }
293
+ }
294
+
295
+ # Update vector database
296
+ await entities_vdb.upsert(entity_data)
297
+
298
+ # 4. Save changes
299
+ await _edit_entity_done(
300
+ entities_vdb, relationships_vdb, chunk_entity_relation_graph
301
+ )
302
+
303
+ logger.info(f"Entity '{entity_name}' successfully updated")
304
+ return await get_entity_info(
305
+ chunk_entity_relation_graph,
306
+ entities_vdb,
307
+ entity_name,
308
+ include_vector_data=True,
309
+ )
310
+ except Exception as e:
311
+ logger.error(f"Error while editing entity '{entity_name}': {e}")
312
+ raise
313
+
314
+
315
+ async def _edit_entity_done(
316
+ entities_vdb, relationships_vdb, chunk_entity_relation_graph
317
+ ) -> None:
318
+ """Callback after entity editing is complete, ensures updates are persisted"""
319
+ await asyncio.gather(
320
+ *[
321
+ cast(StorageNameSpace, storage_inst).index_done_callback()
322
+ for storage_inst in [ # type: ignore
323
+ entities_vdb,
324
+ relationships_vdb,
325
+ chunk_entity_relation_graph,
326
+ ]
327
+ ]
328
+ )
329
+
330
+
331
+ async def aedit_relation(
332
+ chunk_entity_relation_graph,
333
+ entities_vdb,
334
+ relationships_vdb,
335
+ source_entity: str,
336
+ target_entity: str,
337
+ updated_data: dict[str, Any],
338
+ ) -> dict[str, Any]:
339
+ """Asynchronously edit relation information.
340
+
341
+ Updates relation (edge) information in the knowledge graph and re-embeds the relation in the vector database.
342
+
343
+ Args:
344
+ chunk_entity_relation_graph: Graph storage instance
345
+ entities_vdb: Vector database storage for entities
346
+ relationships_vdb: Vector database storage for relationships
347
+ source_entity: Name of the source entity
348
+ target_entity: Name of the target entity
349
+ updated_data: Dictionary containing updated attributes, e.g. {"description": "new description", "keywords": "new keywords"}
350
+
351
+ Returns:
352
+ Dictionary containing updated relation information
353
+ """
354
+ graph_db_lock = get_graph_db_lock(enable_logging=False)
355
+ # Use graph database lock to ensure atomic graph and vector db operations
356
+ async with graph_db_lock:
357
+ try:
358
+ # 1. Get current relation information
359
+ edge_exists = await chunk_entity_relation_graph.has_edge(
360
+ source_entity, target_entity
361
+ )
362
+ if not edge_exists:
363
+ raise ValueError(
364
+ f"Relation from '{source_entity}' to '{target_entity}' does not exist"
365
+ )
366
+ edge_data = await chunk_entity_relation_graph.get_edge(
367
+ source_entity, target_entity
368
+ )
369
+ # Important: First delete the old relation record from the vector database
370
+ old_relation_id = compute_mdhash_id(
371
+ source_entity + target_entity, prefix="rel-"
372
+ )
373
+ await relationships_vdb.delete([old_relation_id])
374
+ logger.info(
375
+ f"Deleted old relation record from vector database for relation {source_entity} -> {target_entity}"
376
+ )
377
+
378
+ # 2. Update relation information in the graph
379
+ new_edge_data = {**edge_data, **updated_data}
380
+ await chunk_entity_relation_graph.upsert_edge(
381
+ source_entity, target_entity, new_edge_data
382
+ )
383
+
384
+ # 3. Recalculate relation's vector representation and update vector database
385
+ description = new_edge_data.get("description", "")
386
+ keywords = new_edge_data.get("keywords", "")
387
+ source_id = new_edge_data.get("source_id", "")
388
+ weight = float(new_edge_data.get("weight", 1.0))
389
+
390
+ # Create content for embedding
391
+ content = f"{source_entity}\t{target_entity}\n{keywords}\n{description}"
392
+
393
+ # Calculate relation ID
394
+ relation_id = compute_mdhash_id(
395
+ source_entity + target_entity, prefix="rel-"
396
+ )
397
+
398
+ # Prepare data for vector database update
399
+ relation_data = {
400
+ relation_id: {
401
+ "content": content,
402
+ "src_id": source_entity,
403
+ "tgt_id": target_entity,
404
+ "source_id": source_id,
405
+ "description": description,
406
+ "keywords": keywords,
407
+ "weight": weight,
408
+ }
409
+ }
410
+
411
+ # Update vector database
412
+ await relationships_vdb.upsert(relation_data)
413
+
414
+ # 4. Save changes
415
+ await _edit_relation_done(relationships_vdb, chunk_entity_relation_graph)
416
+
417
+ logger.info(
418
+ f"Relation from '{source_entity}' to '{target_entity}' successfully updated"
419
+ )
420
+ return await get_relation_info(
421
+ chunk_entity_relation_graph,
422
+ relationships_vdb,
423
+ source_entity,
424
+ target_entity,
425
+ include_vector_data=True,
426
+ )
427
+ except Exception as e:
428
+ logger.error(
429
+ f"Error while editing relation from '{source_entity}' to '{target_entity}': {e}"
430
+ )
431
+ raise
432
+
433
+
434
+ async def _edit_relation_done(relationships_vdb, chunk_entity_relation_graph) -> None:
435
+ """Callback after relation editing is complete, ensures updates are persisted"""
436
+ await asyncio.gather(
437
+ *[
438
+ cast(StorageNameSpace, storage_inst).index_done_callback()
439
+ for storage_inst in [ # type: ignore
440
+ relationships_vdb,
441
+ chunk_entity_relation_graph,
442
+ ]
443
+ ]
444
+ )
445
+
446
+
447
+ async def acreate_entity(
448
+ chunk_entity_relation_graph,
449
+ entities_vdb,
450
+ relationships_vdb,
451
+ entity_name: str,
452
+ entity_data: dict[str, Any],
453
+ ) -> dict[str, Any]:
454
+ """Asynchronously create a new entity.
455
+
456
+ Creates a new entity in the knowledge graph and adds it to the vector database.
457
+
458
+ Args:
459
+ chunk_entity_relation_graph: Graph storage instance
460
+ entities_vdb: Vector database storage for entities
461
+ relationships_vdb: Vector database storage for relationships
462
+ entity_name: Name of the new entity
463
+ entity_data: Dictionary containing entity attributes, e.g. {"description": "description", "entity_type": "type"}
464
+
465
+ Returns:
466
+ Dictionary containing created entity information
467
+ """
468
+ graph_db_lock = get_graph_db_lock(enable_logging=False)
469
+ # Use graph database lock to ensure atomic graph and vector db operations
470
+ async with graph_db_lock:
471
+ try:
472
+ # Check if entity already exists
473
+ existing_node = await chunk_entity_relation_graph.has_node(entity_name)
474
+ if existing_node:
475
+ raise ValueError(f"Entity '{entity_name}' already exists")
476
+
477
+ # Prepare node data with defaults if missing
478
+ node_data = {
479
+ "entity_id": entity_name,
480
+ "entity_type": entity_data.get("entity_type", "UNKNOWN"),
481
+ "description": entity_data.get("description", ""),
482
+ "source_id": entity_data.get("source_id", "manual"),
483
+ }
484
+
485
+ # Add entity to knowledge graph
486
+ await chunk_entity_relation_graph.upsert_node(entity_name, node_data)
487
+
488
+ # Prepare content for entity
489
+ description = node_data.get("description", "")
490
+ source_id = node_data.get("source_id", "")
491
+ entity_type = node_data.get("entity_type", "")
492
+ content = entity_name + "\n" + description
493
+
494
+ # Calculate entity ID
495
+ entity_id = compute_mdhash_id(entity_name, prefix="ent-")
496
+
497
+ # Prepare data for vector database update
498
+ entity_data_for_vdb = {
499
+ entity_id: {
500
+ "content": content,
501
+ "entity_name": entity_name,
502
+ "source_id": source_id,
503
+ "description": description,
504
+ "entity_type": entity_type,
505
+ }
506
+ }
507
+
508
+ # Update vector database
509
+ await entities_vdb.upsert(entity_data_for_vdb)
510
+
511
+ # Save changes
512
+ await _edit_entity_done(
513
+ entities_vdb, relationships_vdb, chunk_entity_relation_graph
514
+ )
515
+
516
+ logger.info(f"Entity '{entity_name}' successfully created")
517
+ return await get_entity_info(
518
+ chunk_entity_relation_graph,
519
+ entities_vdb,
520
+ entity_name,
521
+ include_vector_data=True,
522
+ )
523
+ except Exception as e:
524
+ logger.error(f"Error while creating entity '{entity_name}': {e}")
525
+ raise
526
+
527
+
528
+ async def acreate_relation(
529
+ chunk_entity_relation_graph,
530
+ entities_vdb,
531
+ relationships_vdb,
532
+ source_entity: str,
533
+ target_entity: str,
534
+ relation_data: dict[str, Any],
535
+ ) -> dict[str, Any]:
536
+ """Asynchronously create a new relation between entities.
537
+
538
+ Creates a new relation (edge) in the knowledge graph and adds it to the vector database.
539
+
540
+ Args:
541
+ chunk_entity_relation_graph: Graph storage instance
542
+ entities_vdb: Vector database storage for entities
543
+ relationships_vdb: Vector database storage for relationships
544
+ source_entity: Name of the source entity
545
+ target_entity: Name of the target entity
546
+ relation_data: Dictionary containing relation attributes, e.g. {"description": "description", "keywords": "keywords"}
547
+
548
+ Returns:
549
+ Dictionary containing created relation information
550
+ """
551
+ graph_db_lock = get_graph_db_lock(enable_logging=False)
552
+ # Use graph database lock to ensure atomic graph and vector db operations
553
+ async with graph_db_lock:
554
+ try:
555
+ # Check if both entities exist
556
+ source_exists = await chunk_entity_relation_graph.has_node(source_entity)
557
+ target_exists = await chunk_entity_relation_graph.has_node(target_entity)
558
+
559
+ if not source_exists:
560
+ raise ValueError(f"Source entity '{source_entity}' does not exist")
561
+ if not target_exists:
562
+ raise ValueError(f"Target entity '{target_entity}' does not exist")
563
+
564
+ # Check if relation already exists
565
+ existing_edge = await chunk_entity_relation_graph.has_edge(
566
+ source_entity, target_entity
567
+ )
568
+ if existing_edge:
569
+ raise ValueError(
570
+ f"Relation from '{source_entity}' to '{target_entity}' already exists"
571
+ )
572
+
573
+ # Prepare edge data with defaults if missing
574
+ edge_data = {
575
+ "description": relation_data.get("description", ""),
576
+ "keywords": relation_data.get("keywords", ""),
577
+ "source_id": relation_data.get("source_id", "manual"),
578
+ "weight": float(relation_data.get("weight", 1.0)),
579
+ }
580
+
581
+ # Add relation to knowledge graph
582
+ await chunk_entity_relation_graph.upsert_edge(
583
+ source_entity, target_entity, edge_data
584
+ )
585
+
586
+ # Prepare content for embedding
587
+ description = edge_data.get("description", "")
588
+ keywords = edge_data.get("keywords", "")
589
+ source_id = edge_data.get("source_id", "")
590
+ weight = edge_data.get("weight", 1.0)
591
+
592
+ # Create content for embedding
593
+ content = f"{keywords}\t{source_entity}\n{target_entity}\n{description}"
594
+
595
+ # Calculate relation ID
596
+ relation_id = compute_mdhash_id(
597
+ source_entity + target_entity, prefix="rel-"
598
+ )
599
+
600
+ # Prepare data for vector database update
601
+ relation_data_for_vdb = {
602
+ relation_id: {
603
+ "content": content,
604
+ "src_id": source_entity,
605
+ "tgt_id": target_entity,
606
+ "source_id": source_id,
607
+ "description": description,
608
+ "keywords": keywords,
609
+ "weight": weight,
610
+ }
611
+ }
612
+
613
+ # Update vector database
614
+ await relationships_vdb.upsert(relation_data_for_vdb)
615
+
616
+ # Save changes
617
+ await _edit_relation_done(relationships_vdb, chunk_entity_relation_graph)
618
+
619
+ logger.info(
620
+ f"Relation from '{source_entity}' to '{target_entity}' successfully created"
621
+ )
622
+ return await get_relation_info(
623
+ chunk_entity_relation_graph,
624
+ relationships_vdb,
625
+ source_entity,
626
+ target_entity,
627
+ include_vector_data=True,
628
+ )
629
+ except Exception as e:
630
+ logger.error(
631
+ f"Error while creating relation from '{source_entity}' to '{target_entity}': {e}"
632
+ )
633
+ raise
634
+
635
+
636
+ async def amerge_entities(
637
+ chunk_entity_relation_graph,
638
+ entities_vdb,
639
+ relationships_vdb,
640
+ source_entities: list[str],
641
+ target_entity: str,
642
+ merge_strategy: dict[str, str] = None,
643
+ target_entity_data: dict[str, Any] = None,
644
+ ) -> dict[str, Any]:
645
+ """Asynchronously merge multiple entities into one entity.
646
+
647
+ Merges multiple source entities into a target entity, handling all relationships,
648
+ and updating both the knowledge graph and vector database.
649
+
650
+ Args:
651
+ chunk_entity_relation_graph: Graph storage instance
652
+ entities_vdb: Vector database storage for entities
653
+ relationships_vdb: Vector database storage for relationships
654
+ source_entities: List of source entity names to merge
655
+ target_entity: Name of the target entity after merging
656
+ merge_strategy: Merge strategy configuration, e.g. {"description": "concatenate", "entity_type": "keep_first"}
657
+ Supported strategies:
658
+ - "concatenate": Concatenate all values (for text fields)
659
+ - "keep_first": Keep the first non-empty value
660
+ - "keep_last": Keep the last non-empty value
661
+ - "join_unique": Join all unique values (for fields separated by delimiter)
662
+ target_entity_data: Dictionary of specific values to set for the target entity,
663
+ overriding any merged values, e.g. {"description": "custom description", "entity_type": "PERSON"}
664
+
665
+ Returns:
666
+ Dictionary containing the merged entity information
667
+ """
668
+ graph_db_lock = get_graph_db_lock(enable_logging=False)
669
+ # Use graph database lock to ensure atomic graph and vector db operations
670
+ async with graph_db_lock:
671
+ try:
672
+ # Default merge strategy
673
+ default_strategy = {
674
+ "description": "concatenate",
675
+ "entity_type": "keep_first",
676
+ "source_id": "join_unique",
677
+ }
678
+
679
+ merge_strategy = (
680
+ default_strategy
681
+ if merge_strategy is None
682
+ else {**default_strategy, **merge_strategy}
683
+ )
684
+ target_entity_data = (
685
+ {} if target_entity_data is None else target_entity_data
686
+ )
687
+
688
+ # 1. Check if all source entities exist
689
+ source_entities_data = {}
690
+ for entity_name in source_entities:
691
+ node_exists = await chunk_entity_relation_graph.has_node(entity_name)
692
+ if not node_exists:
693
+ raise ValueError(f"Source entity '{entity_name}' does not exist")
694
+ node_data = await chunk_entity_relation_graph.get_node(entity_name)
695
+ source_entities_data[entity_name] = node_data
696
+
697
+ # 2. Check if target entity exists and get its data if it does
698
+ target_exists = await chunk_entity_relation_graph.has_node(target_entity)
699
+ existing_target_entity_data = {}
700
+ if target_exists:
701
+ existing_target_entity_data = (
702
+ await chunk_entity_relation_graph.get_node(target_entity)
703
+ )
704
+ logger.info(
705
+ f"Target entity '{target_entity}' already exists, will merge data"
706
+ )
707
+
708
+ # 3. Merge entity data
709
+ merged_entity_data = _merge_entity_attributes(
710
+ list(source_entities_data.values())
711
+ + ([existing_target_entity_data] if target_exists else []),
712
+ merge_strategy,
713
+ )
714
+
715
+ # Apply any explicitly provided target entity data (overrides merged data)
716
+ for key, value in target_entity_data.items():
717
+ merged_entity_data[key] = value
718
+
719
+ # 4. Get all relationships of the source entities
720
+ all_relations = []
721
+ for entity_name in source_entities:
722
+ # Get all relationships of the source entities
723
+ edges = await chunk_entity_relation_graph.get_node_edges(entity_name)
724
+ if edges:
725
+ for src, tgt in edges:
726
+ # Ensure src is the current entity
727
+ if src == entity_name:
728
+ edge_data = await chunk_entity_relation_graph.get_edge(
729
+ src, tgt
730
+ )
731
+ all_relations.append((src, tgt, edge_data))
732
+
733
+ # 5. Create or update the target entity
734
+ merged_entity_data["entity_id"] = target_entity
735
+ if not target_exists:
736
+ await chunk_entity_relation_graph.upsert_node(
737
+ target_entity, merged_entity_data
738
+ )
739
+ logger.info(f"Created new target entity '{target_entity}'")
740
+ else:
741
+ await chunk_entity_relation_graph.upsert_node(
742
+ target_entity, merged_entity_data
743
+ )
744
+ logger.info(f"Updated existing target entity '{target_entity}'")
745
+
746
+ # 6. Recreate all relationships, pointing to the target entity
747
+ relation_updates = {} # Track relationships that need to be merged
748
+ relations_to_delete = []
749
+
750
+ for src, tgt, edge_data in all_relations:
751
+ relations_to_delete.append(compute_mdhash_id(src + tgt, prefix="rel-"))
752
+ relations_to_delete.append(compute_mdhash_id(tgt + src, prefix="rel-"))
753
+ new_src = target_entity if src in source_entities else src
754
+ new_tgt = target_entity if tgt in source_entities else tgt
755
+
756
+ # Skip relationships between source entities to avoid self-loops
757
+ if new_src == new_tgt:
758
+ logger.info(
759
+ f"Skipping relationship between source entities: {src} -> {tgt} to avoid self-loop"
760
+ )
761
+ continue
762
+
763
+ # Check if the same relationship already exists
764
+ relation_key = f"{new_src}|{new_tgt}"
765
+ if relation_key in relation_updates:
766
+ # Merge relationship data
767
+ existing_data = relation_updates[relation_key]["data"]
768
+ merged_relation = _merge_relation_attributes(
769
+ [existing_data, edge_data],
770
+ {
771
+ "description": "concatenate",
772
+ "keywords": "join_unique",
773
+ "source_id": "join_unique",
774
+ "weight": "max",
775
+ },
776
+ )
777
+ relation_updates[relation_key]["data"] = merged_relation
778
+ logger.info(
779
+ f"Merged duplicate relationship: {new_src} -> {new_tgt}"
780
+ )
781
+ else:
782
+ relation_updates[relation_key] = {
783
+ "src": new_src,
784
+ "tgt": new_tgt,
785
+ "data": edge_data.copy(),
786
+ }
787
+
788
+ # Apply relationship updates
789
+ for rel_data in relation_updates.values():
790
+ await chunk_entity_relation_graph.upsert_edge(
791
+ rel_data["src"], rel_data["tgt"], rel_data["data"]
792
+ )
793
+ logger.info(
794
+ f"Created or updated relationship: {rel_data['src']} -> {rel_data['tgt']}"
795
+ )
796
+
797
+ # Delete relationships records from vector database
798
+ await relationships_vdb.delete(relations_to_delete)
799
+ logger.info(
800
+ f"Deleted {len(relations_to_delete)} relation records for entity from vector database"
801
+ )
802
+
803
+ # 7. Update entity vector representation
804
+ description = merged_entity_data.get("description", "")
805
+ source_id = merged_entity_data.get("source_id", "")
806
+ entity_type = merged_entity_data.get("entity_type", "")
807
+ content = target_entity + "\n" + description
808
+
809
+ entity_id = compute_mdhash_id(target_entity, prefix="ent-")
810
+ entity_data_for_vdb = {
811
+ entity_id: {
812
+ "content": content,
813
+ "entity_name": target_entity,
814
+ "source_id": source_id,
815
+ "description": description,
816
+ "entity_type": entity_type,
817
+ }
818
+ }
819
+
820
+ await entities_vdb.upsert(entity_data_for_vdb)
821
+
822
+ # 8. Update relationship vector representations
823
+ for rel_data in relation_updates.values():
824
+ src = rel_data["src"]
825
+ tgt = rel_data["tgt"]
826
+ edge_data = rel_data["data"]
827
+
828
+ description = edge_data.get("description", "")
829
+ keywords = edge_data.get("keywords", "")
830
+ source_id = edge_data.get("source_id", "")
831
+ weight = float(edge_data.get("weight", 1.0))
832
+
833
+ content = f"{keywords}\t{src}\n{tgt}\n{description}"
834
+ relation_id = compute_mdhash_id(src + tgt, prefix="rel-")
835
+
836
+ relation_data_for_vdb = {
837
+ relation_id: {
838
+ "content": content,
839
+ "src_id": src,
840
+ "tgt_id": tgt,
841
+ "source_id": source_id,
842
+ "description": description,
843
+ "keywords": keywords,
844
+ "weight": weight,
845
+ }
846
+ }
847
+
848
+ await relationships_vdb.upsert(relation_data_for_vdb)
849
+
850
+ # 9. Delete source entities
851
+ for entity_name in source_entities:
852
+ if entity_name == target_entity:
853
+ logger.info(
854
+ f"Skipping deletion of '{entity_name}' as it's also the target entity"
855
+ )
856
+ continue
857
+
858
+ # Delete entity node from knowledge graph
859
+ await chunk_entity_relation_graph.delete_node(entity_name)
860
+
861
+ # Delete entity record from vector database
862
+ entity_id = compute_mdhash_id(entity_name, prefix="ent-")
863
+ await entities_vdb.delete([entity_id])
864
+
865
+ logger.info(
866
+ f"Deleted source entity '{entity_name}' and its vector embedding from database"
867
+ )
868
+
869
+ # 10. Save changes
870
+ await _merge_entities_done(
871
+ entities_vdb, relationships_vdb, chunk_entity_relation_graph
872
+ )
873
+
874
+ logger.info(
875
+ f"Successfully merged {len(source_entities)} entities into '{target_entity}'"
876
+ )
877
+ return await get_entity_info(
878
+ chunk_entity_relation_graph,
879
+ entities_vdb,
880
+ target_entity,
881
+ include_vector_data=True,
882
+ )
883
+
884
+ except Exception as e:
885
+ logger.error(f"Error merging entities: {e}")
886
+ raise
887
+
888
+
889
+ def _merge_entity_attributes(
890
+ entity_data_list: list[dict[str, Any]], merge_strategy: dict[str, str]
891
+ ) -> dict[str, Any]:
892
+ """Merge attributes from multiple entities.
893
+
894
+ Args:
895
+ entity_data_list: List of dictionaries containing entity data
896
+ merge_strategy: Merge strategy for each field
897
+
898
+ Returns:
899
+ Dictionary containing merged entity data
900
+ """
901
+ merged_data = {}
902
+
903
+ # Collect all possible keys
904
+ all_keys = set()
905
+ for data in entity_data_list:
906
+ all_keys.update(data.keys())
907
+
908
+ # Merge values for each key
909
+ for key in all_keys:
910
+ # Get all values for this key
911
+ values = [data.get(key) for data in entity_data_list if data.get(key)]
912
+
913
+ if not values:
914
+ continue
915
+
916
+ # Merge values according to strategy
917
+ strategy = merge_strategy.get(key, "keep_first")
918
+
919
+ if strategy == "concatenate":
920
+ merged_data[key] = "\n\n".join(values)
921
+ elif strategy == "keep_first":
922
+ merged_data[key] = values[0]
923
+ elif strategy == "keep_last":
924
+ merged_data[key] = values[-1]
925
+ elif strategy == "join_unique":
926
+ # Handle fields separated by GRAPH_FIELD_SEP
927
+ unique_items = set()
928
+ for value in values:
929
+ items = value.split(GRAPH_FIELD_SEP)
930
+ unique_items.update(items)
931
+ merged_data[key] = GRAPH_FIELD_SEP.join(unique_items)
932
+ else:
933
+ # Default strategy
934
+ merged_data[key] = values[0]
935
+
936
+ return merged_data
937
+
938
+
939
+ def _merge_relation_attributes(
940
+ relation_data_list: list[dict[str, Any]], merge_strategy: dict[str, str]
941
+ ) -> dict[str, Any]:
942
+ """Merge attributes from multiple relationships.
943
+
944
+ Args:
945
+ relation_data_list: List of dictionaries containing relationship data
946
+ merge_strategy: Merge strategy for each field
947
+
948
+ Returns:
949
+ Dictionary containing merged relationship data
950
+ """
951
+ merged_data = {}
952
+
953
+ # Collect all possible keys
954
+ all_keys = set()
955
+ for data in relation_data_list:
956
+ all_keys.update(data.keys())
957
+
958
+ # Merge values for each key
959
+ for key in all_keys:
960
+ # Get all values for this key
961
+ values = [
962
+ data.get(key) for data in relation_data_list if data.get(key) is not None
963
+ ]
964
+
965
+ if not values:
966
+ continue
967
+
968
+ # Merge values according to strategy
969
+ strategy = merge_strategy.get(key, "keep_first")
970
+
971
+ if strategy == "concatenate":
972
+ merged_data[key] = "\n\n".join(str(v) for v in values)
973
+ elif strategy == "keep_first":
974
+ merged_data[key] = values[0]
975
+ elif strategy == "keep_last":
976
+ merged_data[key] = values[-1]
977
+ elif strategy == "join_unique":
978
+ # Handle fields separated by GRAPH_FIELD_SEP
979
+ unique_items = set()
980
+ for value in values:
981
+ items = str(value).split(GRAPH_FIELD_SEP)
982
+ unique_items.update(items)
983
+ merged_data[key] = GRAPH_FIELD_SEP.join(unique_items)
984
+ elif strategy == "max":
985
+ # For numeric fields like weight
986
+ try:
987
+ merged_data[key] = max(float(v) for v in values)
988
+ except (ValueError, TypeError):
989
+ merged_data[key] = values[0]
990
+ else:
991
+ # Default strategy
992
+ merged_data[key] = values[0]
993
+
994
+ return merged_data
995
+
996
+
997
+ async def _merge_entities_done(
998
+ entities_vdb, relationships_vdb, chunk_entity_relation_graph
999
+ ) -> None:
1000
+ """Callback after entity merging is complete, ensures updates are persisted"""
1001
+ await asyncio.gather(
1002
+ *[
1003
+ cast(StorageNameSpace, storage_inst).index_done_callback()
1004
+ for storage_inst in [ # type: ignore
1005
+ entities_vdb,
1006
+ relationships_vdb,
1007
+ chunk_entity_relation_graph,
1008
+ ]
1009
+ ]
1010
+ )
1011
+
1012
+
1013
+ async def get_entity_info(
1014
+ chunk_entity_relation_graph,
1015
+ entities_vdb,
1016
+ entity_name: str,
1017
+ include_vector_data: bool = False,
1018
+ ) -> dict[str, str | None | dict[str, str]]:
1019
+ """Get detailed information of an entity"""
1020
+
1021
+ # Get information from the graph
1022
+ node_data = await chunk_entity_relation_graph.get_node(entity_name)
1023
+ source_id = node_data.get("source_id") if node_data else None
1024
+
1025
+ result: dict[str, str | None | dict[str, str]] = {
1026
+ "entity_name": entity_name,
1027
+ "source_id": source_id,
1028
+ "graph_data": node_data,
1029
+ }
1030
+
1031
+ # Optional: Get vector database information
1032
+ if include_vector_data:
1033
+ entity_id = compute_mdhash_id(entity_name, prefix="ent-")
1034
+ vector_data = await entities_vdb.get_by_id(entity_id)
1035
+ result["vector_data"] = vector_data
1036
+
1037
+ return result
1038
+
1039
+
1040
+ async def get_relation_info(
1041
+ chunk_entity_relation_graph,
1042
+ relationships_vdb,
1043
+ src_entity: str,
1044
+ tgt_entity: str,
1045
+ include_vector_data: bool = False,
1046
+ ) -> dict[str, str | None | dict[str, str]]:
1047
+ """Get detailed information of a relationship"""
1048
+
1049
+ # Get information from the graph
1050
+ edge_data = await chunk_entity_relation_graph.get_edge(src_entity, tgt_entity)
1051
+ source_id = edge_data.get("source_id") if edge_data else None
1052
+
1053
+ result: dict[str, str | None | dict[str, str]] = {
1054
+ "src_entity": src_entity,
1055
+ "tgt_entity": tgt_entity,
1056
+ "source_id": source_id,
1057
+ "graph_data": edge_data,
1058
+ }
1059
+
1060
+ # Optional: Get vector database information
1061
+ if include_vector_data:
1062
+ rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
1063
+ vector_data = await relationships_vdb.get_by_id(rel_id)
1064
+ result["vector_data"] = vector_data
1065
+
1066
+ return result
lightrag_webui/src/api/lightrag.ts CHANGED
@@ -507,3 +507,58 @@ export const loginToServer = async (username: string, password: string): Promise
507
 
508
  return response.data;
509
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
 
508
  return response.data;
509
  }
510
+
511
+ /**
512
+ * Updates an entity's properties in the knowledge graph
513
+ * @param entityName The name of the entity to update
514
+ * @param updatedData Dictionary containing updated attributes
515
+ * @param allowRename Whether to allow renaming the entity (default: false)
516
+ * @returns Promise with the updated entity information
517
+ */
518
+ export const updateEntity = async (
519
+ entityName: string,
520
+ updatedData: Record<string, any>,
521
+ allowRename: boolean = false
522
+ ): Promise<DocActionResponse> => {
523
+ const response = await axiosInstance.post('/graph/entity/edit', {
524
+ entity_name: entityName,
525
+ updated_data: updatedData,
526
+ allow_rename: allowRename
527
+ })
528
+ return response.data
529
+ }
530
+
531
+ /**
532
+ * Updates a relation's properties in the knowledge graph
533
+ * @param sourceEntity The source entity name
534
+ * @param targetEntity The target entity name
535
+ * @param updatedData Dictionary containing updated attributes
536
+ * @returns Promise with the updated relation information
537
+ */
538
+ export const updateRelation = async (
539
+ sourceEntity: string,
540
+ targetEntity: string,
541
+ updatedData: Record<string, any>
542
+ ): Promise<DocActionResponse> => {
543
+ const response = await axiosInstance.post('/graph/relation/edit', {
544
+ source_id: sourceEntity,
545
+ target_id: targetEntity,
546
+ updated_data: updatedData
547
+ })
548
+ return response.data
549
+ }
550
+
551
+ /**
552
+ * Checks if an entity name already exists in the knowledge graph
553
+ * @param entityName The entity name to check
554
+ * @returns Promise with boolean indicating if the entity exists
555
+ */
556
+ export const checkEntityNameExists = async (entityName: string): Promise<boolean> => {
557
+ try {
558
+ const response = await axiosInstance.get(`/graph/entity/exists?name=${encodeURIComponent(entityName)}`)
559
+ return response.data.exists
560
+ } catch (error) {
561
+ console.error('Error checking entity name:', error)
562
+ return false
563
+ }
564
+ }
lightrag_webui/src/components/graph/EditablePropertyRow.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { toast } from 'sonner'
4
+ import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag'
5
+ import { updateGraphNode, updateGraphEdge } from '@/utils/graphOperations'
6
+ import { PropertyName, EditIcon, PropertyValue } from './PropertyRowComponents'
7
+ import PropertyEditDialog from './PropertyEditDialog'
8
+
9
+ /**
10
+ * Interface for the EditablePropertyRow component props
11
+ */
12
+ interface EditablePropertyRowProps {
13
+ name: string // Property name to display and edit
14
+ value: any // Initial value of the property
15
+ onClick?: () => void // Optional click handler for the property value
16
+ entityId?: string // ID of the entity (for node type)
17
+ entityType?: 'node' | 'edge' // Type of graph entity
18
+ sourceId?: string // Source node ID (for edge type)
19
+ targetId?: string // Target node ID (for edge type)
20
+ onValueChange?: (newValue: any) => void // Optional callback when value changes
21
+ isEditable?: boolean // Whether this property can be edited
22
+ }
23
+
24
+ /**
25
+ * EditablePropertyRow component that supports editing property values
26
+ * This component is used in the graph properties panel to display and edit entity properties
27
+ */
28
+ const EditablePropertyRow = ({
29
+ name,
30
+ value: initialValue,
31
+ onClick,
32
+ entityId,
33
+ entityType,
34
+ sourceId,
35
+ targetId,
36
+ onValueChange,
37
+ isEditable = false
38
+ }: EditablePropertyRowProps) => {
39
+ const { t } = useTranslation()
40
+ const [isEditing, setIsEditing] = useState(false)
41
+ const [isSubmitting, setIsSubmitting] = useState(false)
42
+ const [currentValue, setCurrentValue] = useState(initialValue)
43
+
44
+ useEffect(() => {
45
+ setCurrentValue(initialValue)
46
+ }, [initialValue])
47
+
48
+ const handleEditClick = () => {
49
+ if (isEditable && !isEditing) {
50
+ setIsEditing(true)
51
+ }
52
+ }
53
+
54
+ const handleCancel = () => {
55
+ setIsEditing(false)
56
+ }
57
+
58
+ const handleSave = async (value: string) => {
59
+ if (isSubmitting || value === String(currentValue)) {
60
+ setIsEditing(false)
61
+ return
62
+ }
63
+
64
+ setIsSubmitting(true)
65
+
66
+ try {
67
+ if (entityType === 'node' && entityId) {
68
+ let updatedData = { [name]: value }
69
+
70
+ if (name === 'entity_id') {
71
+ const exists = await checkEntityNameExists(value)
72
+ if (exists) {
73
+ toast.error(t('graphPanel.propertiesView.errors.duplicateName'))
74
+ return
75
+ }
76
+ updatedData = { 'entity_name': value }
77
+ }
78
+
79
+ await updateEntity(entityId, updatedData, true)
80
+ await updateGraphNode(entityId, name, value)
81
+ toast.success(t('graphPanel.propertiesView.success.entityUpdated'))
82
+ } else if (entityType === 'edge' && sourceId && targetId) {
83
+ const updatedData = { [name]: value }
84
+ await updateRelation(sourceId, targetId, updatedData)
85
+ await updateGraphEdge(sourceId, targetId, name, value)
86
+ toast.success(t('graphPanel.propertiesView.success.relationUpdated'))
87
+ }
88
+
89
+ setIsEditing(false)
90
+ setCurrentValue(value)
91
+ onValueChange?.(value)
92
+ } catch (error) {
93
+ console.error('Error updating property:', error)
94
+ toast.error(t('graphPanel.propertiesView.errors.updateFailed'))
95
+ } finally {
96
+ setIsSubmitting(false)
97
+ }
98
+ }
99
+
100
+ return (
101
+ <div className="flex items-center gap-1">
102
+ <PropertyName name={name} />
103
+ <EditIcon onClick={handleEditClick} />:
104
+ <PropertyValue value={currentValue} onClick={onClick} />
105
+ <PropertyEditDialog
106
+ isOpen={isEditing}
107
+ onClose={handleCancel}
108
+ onSave={handleSave}
109
+ propertyName={name}
110
+ initialValue={String(currentValue)}
111
+ isSubmitting={isSubmitting}
112
+ />
113
+ </div>
114
+ )
115
+ }
116
+
117
+ export default EditablePropertyRow
lightrag_webui/src/components/graph/GraphControl.tsx CHANGED
@@ -99,7 +99,10 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
99
  const events: Record<string, any> = {
100
  enterNode: (event: NodeEvent) => {
101
  if (!isButtonPressed(event.event.original)) {
102
- setFocusedNode(event.node)
 
 
 
103
  }
104
  },
105
  leaveNode: (event: NodeEvent) => {
@@ -108,8 +111,11 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
108
  }
109
  },
110
  clickNode: (event: NodeEvent) => {
111
- setSelectedNode(event.node)
112
- setSelectedEdge(null)
 
 
 
113
  },
114
  clickStage: () => clearSelection()
115
  }
 
99
  const events: Record<string, any> = {
100
  enterNode: (event: NodeEvent) => {
101
  if (!isButtonPressed(event.event.original)) {
102
+ const graph = sigma.getGraph()
103
+ if (graph.hasNode(event.node)) {
104
+ setFocusedNode(event.node)
105
+ }
106
  }
107
  },
108
  leaveNode: (event: NodeEvent) => {
 
111
  }
112
  },
113
  clickNode: (event: NodeEvent) => {
114
+ const graph = sigma.getGraph()
115
+ if (graph.hasNode(event.node)) {
116
+ setSelectedNode(event.node)
117
+ setSelectedEdge(null)
118
+ }
119
  },
120
  clickStage: () => clearSelection()
121
  }
lightrag_webui/src/components/graph/PropertiesView.tsx CHANGED
@@ -5,6 +5,7 @@ import Button from '@/components/ui/Button'
5
  import useLightragGraph from '@/hooks/useLightragGraph'
6
  import { useTranslation } from 'react-i18next'
7
  import { GitBranchPlus, Scissors } from 'lucide-react'
 
8
 
9
  /**
10
  * Component that view properties of elements in graph.
@@ -169,12 +170,22 @@ const PropertyRow = ({
169
  name,
170
  value,
171
  onClick,
172
- tooltip
 
 
 
 
 
173
  }: {
174
  name: string
175
  value: any
176
  onClick?: () => void
177
  tooltip?: string
 
 
 
 
 
178
  }) => {
179
  const { t } = useTranslation()
180
 
@@ -184,8 +195,23 @@ const PropertyRow = ({
184
  return translation === translationKey ? name : translation
185
  }
186
 
187
- // Since Text component uses a label internally, we'll use a span here instead of a label
188
- // to avoid nesting labels which is not recommended for accessibility
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  return (
190
  <div className="flex items-center gap-2">
191
  <span className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</span>:
@@ -253,7 +279,16 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
253
  {Object.keys(node.properties)
254
  .sort()
255
  .map((name) => {
256
- return <PropertyRow key={name} name={name} value={node.properties[name]} />
 
 
 
 
 
 
 
 
 
257
  })}
258
  </div>
259
  {node.relationships.length > 0 && (
@@ -309,7 +344,18 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
309
  {Object.keys(edge.properties)
310
  .sort()
311
  .map((name) => {
312
- return <PropertyRow key={name} name={name} value={edge.properties[name]} />
 
 
 
 
 
 
 
 
 
 
 
313
  })}
314
  </div>
315
  </div>
 
5
  import useLightragGraph from '@/hooks/useLightragGraph'
6
  import { useTranslation } from 'react-i18next'
7
  import { GitBranchPlus, Scissors } from 'lucide-react'
8
+ import EditablePropertyRow from './EditablePropertyRow'
9
 
10
  /**
11
  * Component that view properties of elements in graph.
 
170
  name,
171
  value,
172
  onClick,
173
+ tooltip,
174
+ entityId,
175
+ entityType,
176
+ sourceId,
177
+ targetId,
178
+ isEditable = false
179
  }: {
180
  name: string
181
  value: any
182
  onClick?: () => void
183
  tooltip?: string
184
+ entityId?: string
185
+ entityType?: 'node' | 'edge'
186
+ sourceId?: string
187
+ targetId?: string
188
+ isEditable?: boolean
189
  }) => {
190
  const { t } = useTranslation()
191
 
 
195
  return translation === translationKey ? name : translation
196
  }
197
 
198
+ // Use EditablePropertyRow for editable fields (description, entity_id and keywords)
199
+ if (isEditable && (name === 'description' || name === 'entity_id' || name === 'keywords')) {
200
+ return (
201
+ <EditablePropertyRow
202
+ name={name}
203
+ value={value}
204
+ onClick={onClick}
205
+ entityId={entityId}
206
+ entityType={entityType}
207
+ sourceId={sourceId}
208
+ targetId={targetId}
209
+ isEditable={true}
210
+ />
211
+ )
212
+ }
213
+
214
+ // For non-editable fields, use the regular Text component
215
  return (
216
  <div className="flex items-center gap-2">
217
  <span className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</span>:
 
279
  {Object.keys(node.properties)
280
  .sort()
281
  .map((name) => {
282
+ return (
283
+ <PropertyRow
284
+ key={name}
285
+ name={name}
286
+ value={node.properties[name]}
287
+ entityId={node.properties['entity_id'] || node.id}
288
+ entityType="node"
289
+ isEditable={name === 'description' || name === 'entity_id'}
290
+ />
291
+ )
292
  })}
293
  </div>
294
  {node.relationships.length > 0 && (
 
344
  {Object.keys(edge.properties)
345
  .sort()
346
  .map((name) => {
347
+ return (
348
+ <PropertyRow
349
+ key={name}
350
+ name={name}
351
+ value={edge.properties[name]}
352
+ entityId={edge.id}
353
+ entityType="edge"
354
+ sourceId={edge.source}
355
+ targetId={edge.target}
356
+ isEditable={name === 'description' || name === 'keywords'}
357
+ />
358
+ )
359
  })}
360
  </div>
361
  </div>
lightrag_webui/src/components/graph/PropertyEditDialog.tsx ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ DialogFooter,
9
+ DialogDescription
10
+ } from '@/components/ui/Dialog'
11
+ import Button from '@/components/ui/Button'
12
+
13
+ interface PropertyEditDialogProps {
14
+ isOpen: boolean
15
+ onClose: () => void
16
+ onSave: (value: string) => void
17
+ propertyName: string
18
+ initialValue: string
19
+ isSubmitting?: boolean
20
+ }
21
+
22
+ /**
23
+ * Dialog component for editing property values
24
+ * Provides a modal with a title, multi-line text input, and save/cancel buttons
25
+ */
26
+ const PropertyEditDialog = ({
27
+ isOpen,
28
+ onClose,
29
+ onSave,
30
+ propertyName,
31
+ initialValue,
32
+ isSubmitting = false
33
+ }: PropertyEditDialogProps) => {
34
+ const { t } = useTranslation()
35
+ const [value, setValue] = useState('')
36
+
37
+ // Initialize value when dialog opens
38
+ useEffect(() => {
39
+ if (isOpen) {
40
+ setValue(initialValue)
41
+ }
42
+ }, [isOpen, initialValue])
43
+
44
+ // Get translated property name
45
+ const getPropertyNameTranslation = (name: string) => {
46
+ const translationKey = `graphPanel.propertiesView.node.propertyNames.${name}`
47
+ const translation = t(translationKey)
48
+ return translation === translationKey ? name : translation
49
+ }
50
+
51
+ // Get textarea configuration based on property name
52
+ const getTextareaConfig = (propertyName: string) => {
53
+ switch (propertyName) {
54
+ case 'description':
55
+ return {
56
+ // No rows attribute for description to allow auto-sizing
57
+ className: 'max-h-[50vh] min-h-[10em] resize-y', // Maximum height 70% of viewport, minimum height ~20 lines, allow vertical resizing
58
+ style: {
59
+ height: '70vh', // Set initial height to 70% of viewport
60
+ minHeight: '20em', // Minimum height ~20 lines
61
+ resize: 'vertical' as const // Allow vertical resizing, using 'as const' to fix type
62
+ }
63
+ };
64
+ case 'entity_id':
65
+ return {
66
+ rows: 2,
67
+ className: '',
68
+ style: {}
69
+ };
70
+ case 'keywords':
71
+ return {
72
+ rows: 4,
73
+ className: '',
74
+ style: {}
75
+ };
76
+ default:
77
+ return {
78
+ rows: 5,
79
+ className: '',
80
+ style: {}
81
+ };
82
+ }
83
+ };
84
+
85
+ const handleSave = () => {
86
+ if (value.trim() !== '') {
87
+ onSave(value)
88
+ onClose()
89
+ }
90
+ }
91
+
92
+ return (
93
+ <Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
94
+ <DialogContent className="sm:max-w-md">
95
+ <DialogHeader>
96
+ <DialogTitle>
97
+ {t('graphPanel.propertiesView.editProperty', {
98
+ property: getPropertyNameTranslation(propertyName)
99
+ })}
100
+ </DialogTitle>
101
+ <DialogDescription>
102
+ {t('graphPanel.propertiesView.editPropertyDescription')}
103
+ </DialogDescription>
104
+ </DialogHeader>
105
+
106
+ {/* Multi-line text input using textarea */}
107
+ <div className="grid gap-4 py-4">
108
+ {(() => {
109
+ const config = getTextareaConfig(propertyName);
110
+ return propertyName === 'description' ? (
111
+ <textarea
112
+ value={value}
113
+ onChange={(e) => setValue(e.target.value)}
114
+ className={`border-input focus-visible:ring-ring flex w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${config.className}`}
115
+ style={config.style}
116
+ disabled={isSubmitting}
117
+ />
118
+ ) : (
119
+ <textarea
120
+ value={value}
121
+ onChange={(e) => setValue(e.target.value)}
122
+ rows={config.rows}
123
+ className={`border-input focus-visible:ring-ring flex w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${config.className}`}
124
+ disabled={isSubmitting}
125
+ />
126
+ );
127
+ })()}
128
+ </div>
129
+
130
+ <DialogFooter>
131
+ <Button
132
+ type="button"
133
+ variant="outline"
134
+ onClick={onClose}
135
+ disabled={isSubmitting}
136
+ >
137
+ {t('common.cancel')}
138
+ </Button>
139
+ <Button
140
+ type="button"
141
+ onClick={handleSave}
142
+ disabled={isSubmitting}
143
+ >
144
+ {t('common.save')}
145
+ </Button>
146
+ </DialogFooter>
147
+ </DialogContent>
148
+ </Dialog>
149
+ )
150
+ }
151
+
152
+ export default PropertyEditDialog
lightrag_webui/src/components/graph/PropertyRowComponents.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PencilIcon } from 'lucide-react'
2
+ import Text from '@/components/ui/Text'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ interface PropertyNameProps {
6
+ name: string
7
+ }
8
+
9
+ export const PropertyName = ({ name }: PropertyNameProps) => {
10
+ const { t } = useTranslation()
11
+
12
+ const getPropertyNameTranslation = (propName: string) => {
13
+ const translationKey = `graphPanel.propertiesView.node.propertyNames.${propName}`
14
+ const translation = t(translationKey)
15
+ return translation === translationKey ? propName : translation
16
+ }
17
+
18
+ return (
19
+ <span className="text-primary/60 tracking-wide whitespace-nowrap">
20
+ {getPropertyNameTranslation(name)}
21
+ </span>
22
+ )
23
+ }
24
+
25
+ interface EditIconProps {
26
+ onClick: () => void
27
+ }
28
+
29
+ export const EditIcon = ({ onClick }: EditIconProps) => (
30
+ <div>
31
+ <PencilIcon
32
+ className="h-3 w-3 text-gray-500 hover:text-gray-700 cursor-pointer"
33
+ onClick={onClick}
34
+ />
35
+ </div>
36
+ )
37
+
38
+ interface PropertyValueProps {
39
+ value: any
40
+ onClick?: () => void
41
+ }
42
+
43
+ export const PropertyValue = ({ value, onClick }: PropertyValueProps) => (
44
+ <div className="flex items-center gap-1">
45
+ <Text
46
+ className="hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis"
47
+ tooltipClassName="max-w-80"
48
+ text={value}
49
+ side="left"
50
+ onClick={onClick}
51
+ />
52
+ </div>
53
+ )
lightrag_webui/src/locales/ar.json CHANGED
@@ -33,7 +33,8 @@
33
  "guestMode": "وضع بدون تسجيل دخول"
34
  },
35
  "common": {
36
- "cancel": "إلغاء"
 
37
  },
38
  "documentPanel": {
39
  "clearDocuments": {
@@ -236,6 +237,17 @@
236
  "vectorStorage": "تخزين المتجهات"
237
  },
238
  "propertiesView": {
 
 
 
 
 
 
 
 
 
 
 
239
  "node": {
240
  "title": "عقدة",
241
  "id": "المعرف",
 
33
  "guestMode": "وضع بدون تسجيل دخول"
34
  },
35
  "common": {
36
+ "cancel": "إلغاء",
37
+ "save": "حفظ"
38
  },
39
  "documentPanel": {
40
  "clearDocuments": {
 
237
  "vectorStorage": "تخزين المتجهات"
238
  },
239
  "propertiesView": {
240
+ "editProperty": "تعديل {{property}}",
241
+ "editPropertyDescription": "قم بتحرير قيمة الخاصية في منطقة النص أدناه.",
242
+ "errors": {
243
+ "duplicateName": "اسم العقدة موجود بالفعل",
244
+ "updateFailed": "فشل تحديث العقدة",
245
+ "tryAgainLater": "يرجى المحاولة مرة أخرى لاحقًا"
246
+ },
247
+ "success": {
248
+ "entityUpdated": "تم تحديث العقدة بنجاح",
249
+ "relationUpdated": "تم تحديث العلاقة بنجاح"
250
+ },
251
  "node": {
252
  "title": "عقدة",
253
  "id": "المعرف",
lightrag_webui/src/locales/en.json CHANGED
@@ -33,7 +33,8 @@
33
  "guestMode": "Login Free"
34
  },
35
  "common": {
36
- "cancel": "Cancel"
 
37
  },
38
  "documentPanel": {
39
  "clearDocuments": {
@@ -156,6 +157,7 @@
156
  "animal": "Animal",
157
  "unknown": "Unknown",
158
  "object": "Object",
 
159
  "technology": "Technology"
160
  },
161
  "sideBar": {
@@ -235,6 +237,17 @@
235
  "vectorStorage": "Vector Storage"
236
  },
237
  "propertiesView": {
 
 
 
 
 
 
 
 
 
 
 
238
  "node": {
239
  "title": "Node",
240
  "id": "ID",
 
33
  "guestMode": "Login Free"
34
  },
35
  "common": {
36
+ "cancel": "Cancel",
37
+ "save": "Save"
38
  },
39
  "documentPanel": {
40
  "clearDocuments": {
 
157
  "animal": "Animal",
158
  "unknown": "Unknown",
159
  "object": "Object",
160
+ "group": "Group",
161
  "technology": "Technology"
162
  },
163
  "sideBar": {
 
237
  "vectorStorage": "Vector Storage"
238
  },
239
  "propertiesView": {
240
+ "editProperty": "Edit {{property}}",
241
+ "editPropertyDescription": "Edit the property value in the text area below.",
242
+ "errors": {
243
+ "duplicateName": "Node name already exists",
244
+ "updateFailed": "Failed to update node",
245
+ "tryAgainLater": "Please try again later"
246
+ },
247
+ "success": {
248
+ "entityUpdated": "Node updated successfully",
249
+ "relationUpdated": "Relation updated successfully"
250
+ },
251
  "node": {
252
  "title": "Node",
253
  "id": "ID",
lightrag_webui/src/locales/fr.json CHANGED
@@ -33,7 +33,8 @@
33
  "guestMode": "Mode sans connexion"
34
  },
35
  "common": {
36
- "cancel": "Annuler"
 
37
  },
38
  "documentPanel": {
39
  "clearDocuments": {
@@ -236,6 +237,17 @@
236
  "vectorStorage": "Stockage vectoriel"
237
  },
238
  "propertiesView": {
 
 
 
 
 
 
 
 
 
 
 
239
  "node": {
240
  "title": "Nœud",
241
  "id": "ID",
 
33
  "guestMode": "Mode sans connexion"
34
  },
35
  "common": {
36
+ "cancel": "Annuler",
37
+ "save": "Sauvegarder"
38
  },
39
  "documentPanel": {
40
  "clearDocuments": {
 
237
  "vectorStorage": "Stockage vectoriel"
238
  },
239
  "propertiesView": {
240
+ "editProperty": "Modifier {{property}}",
241
+ "editPropertyDescription": "Modifiez la valeur de la propriété dans la zone de texte ci-dessous.",
242
+ "errors": {
243
+ "duplicateName": "Le nom du nœud existe déjà",
244
+ "updateFailed": "Échec de la mise à jour du nœud",
245
+ "tryAgainLater": "Veuillez réessayer plus tard"
246
+ },
247
+ "success": {
248
+ "entityUpdated": "Nœud mis à jour avec succès",
249
+ "relationUpdated": "Relation mise à jour avec succès"
250
+ },
251
  "node": {
252
  "title": "Nœud",
253
  "id": "ID",
lightrag_webui/src/locales/zh.json CHANGED
@@ -33,7 +33,8 @@
33
  "guestMode": "无需登陆"
34
  },
35
  "common": {
36
- "cancel": "取消"
 
37
  },
38
  "documentPanel": {
39
  "clearDocuments": {
@@ -236,6 +237,17 @@
236
  "vectorStorage": "向量存储"
237
  },
238
  "propertiesView": {
 
 
 
 
 
 
 
 
 
 
 
239
  "node": {
240
  "title": "节点",
241
  "id": "ID",
 
33
  "guestMode": "无需登陆"
34
  },
35
  "common": {
36
+ "cancel": "取消",
37
+ "save": "保存"
38
  },
39
  "documentPanel": {
40
  "clearDocuments": {
 
237
  "vectorStorage": "向量存储"
238
  },
239
  "propertiesView": {
240
+ "editProperty": "编辑{{property}}",
241
+ "editPropertyDescription": "在下方文本区域编辑属性值。",
242
+ "errors": {
243
+ "duplicateName": "节点名称已存在",
244
+ "updateFailed": "更新节点失败",
245
+ "tryAgainLater": "请稍后重试"
246
+ },
247
+ "success": {
248
+ "entityUpdated": "节点更新成功",
249
+ "relationUpdated": "关系更新成功"
250
+ },
251
  "node": {
252
  "title": "节点",
253
  "id": "ID",
lightrag_webui/src/locales/zh_TW.json CHANGED
@@ -33,7 +33,8 @@
33
  "guestMode": "免登入"
34
  },
35
  "common": {
36
- "cancel": "取消"
 
37
  },
38
  "documentPanel": {
39
  "clearDocuments": {
@@ -236,6 +237,17 @@
236
  "vectorStorage": "向量儲存"
237
  },
238
  "propertiesView": {
 
 
 
 
 
 
 
 
 
 
 
239
  "node": {
240
  "title": "節點",
241
  "id": "ID",
@@ -296,13 +308,14 @@
296
  "parametersTitle": "參數",
297
  "parametersDescription": "設定查詢參數",
298
  "queryMode": "查詢模式",
299
- "queryModeTooltip": "選擇檢索策略:\n• Naive:基礎搜尋,無進階技術\n• Local:上下文相關資訊檢索\n• Global:利用全域知識庫\n• Hybrid:結合本地和全域檢索\n• Mix:整合知識圖譜和向量檢索",
300
  "queryModeOptions": {
301
  "naive": "Naive",
302
  "local": "Local",
303
  "global": "Global",
304
  "hybrid": "Hybrid",
305
- "mix": "Mix"
 
306
  },
307
  "responseFormat": "回應格式",
308
  "responseFormatTooltip": "定義回應格式。例如:\n• 多段落\n• 單段落\n• 重點",
 
33
  "guestMode": "免登入"
34
  },
35
  "common": {
36
+ "cancel": "取消",
37
+ "save": "儲存"
38
  },
39
  "documentPanel": {
40
  "clearDocuments": {
 
237
  "vectorStorage": "向量儲存"
238
  },
239
  "propertiesView": {
240
+ "editProperty": "編輯{{property}}",
241
+ "editPropertyDescription": "在下方文字區域編輯屬性值。",
242
+ "errors": {
243
+ "duplicateName": "節點名稱已存在",
244
+ "updateFailed": "更新節點失敗",
245
+ "tryAgainLater": "請稍後重試"
246
+ },
247
+ "success": {
248
+ "entityUpdated": "節點更新成功",
249
+ "relationUpdated": "關係更新成功"
250
+ },
251
  "node": {
252
  "title": "節點",
253
  "id": "ID",
 
308
  "parametersTitle": "參數",
309
  "parametersDescription": "設定查詢參數",
310
  "queryMode": "查詢模式",
311
+ "queryModeTooltip": "選擇檢索策略:\n• Naive:基礎搜尋,無進階技術\n• Local:上下文相關資訊檢索\n• Global:利用全域知識庫\n• Hybrid:結合本地和全域檢索\n• Mix:整合知識圖譜和向量檢索\n• Bypass:直接傳遞查詢到LLM,不進行檢索",
312
  "queryModeOptions": {
313
  "naive": "Naive",
314
  "local": "Local",
315
  "global": "Global",
316
  "hybrid": "Hybrid",
317
+ "mix": "Mix",
318
+ "bypass": "Bypass"
319
  },
320
  "responseFormat": "回應格式",
321
  "responseFormatTooltip": "定義回應格式。例如:\n• 多段落\n• 單段落\n• 重點",
lightrag_webui/src/stores/graph.ts CHANGED
@@ -116,6 +116,10 @@ interface GraphState {
116
  // Node operation state
117
  nodeToExpand: string | null
118
  nodeToPrune: string | null
 
 
 
 
119
  }
120
 
121
  const useGraphStoreBase = create<GraphState>()((set) => ({
@@ -219,6 +223,10 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
219
  triggerNodeExpand: (nodeId: string | null) => set({ nodeToExpand: nodeId }),
220
  triggerNodePrune: (nodeId: string | null) => set({ nodeToPrune: nodeId }),
221
 
 
 
 
 
222
  }))
223
 
224
  const useGraphStore = createSelectors(useGraphStoreBase)
 
116
  // Node operation state
117
  nodeToExpand: string | null
118
  nodeToPrune: string | null
119
+
120
+ // Version counter to trigger data refresh
121
+ graphDataVersion: number
122
+ incrementGraphDataVersion: () => void
123
  }
124
 
125
  const useGraphStoreBase = create<GraphState>()((set) => ({
 
223
  triggerNodeExpand: (nodeId: string | null) => set({ nodeToExpand: nodeId }),
224
  triggerNodePrune: (nodeId: string | null) => set({ nodeToPrune: nodeId }),
225
 
226
+ // Version counter implementation
227
+ graphDataVersion: 0,
228
+ incrementGraphDataVersion: () => set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })),
229
+
230
  }))
231
 
232
  const useGraphStore = createSelectors(useGraphStoreBase)
lightrag_webui/src/utils/graphOperations.ts ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useGraphStore } from '@/stores/graph'
2
+
3
+ /**
4
+ * Interface for tracking edges that need updating when a node ID changes
5
+ */
6
+ interface EdgeToUpdate {
7
+ originalDynamicId: string
8
+ newEdgeId: string
9
+ edgeIndex: number
10
+ }
11
+
12
+ /**
13
+ * Update node in the graph visualization
14
+ * Handles both property updates and entity ID changes
15
+ *
16
+ * @param nodeId - ID of the node to update
17
+ * @param propertyName - Name of the property being updated
18
+ * @param newValue - New value for the property
19
+ */
20
+ export const updateGraphNode = async (nodeId: string, propertyName: string, newValue: string) => {
21
+ // Get graph state from store
22
+ const sigmaGraph = useGraphStore.getState().sigmaGraph
23
+ const rawGraph = useGraphStore.getState().rawGraph
24
+
25
+ // Validate graph state
26
+ if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(String(nodeId))) {
27
+ return
28
+ }
29
+
30
+ try {
31
+ const nodeAttributes = sigmaGraph.getNodeAttributes(String(nodeId))
32
+
33
+ // Special handling for entity_id changes (node renaming)
34
+ if (propertyName === 'entity_id') {
35
+ // Create new node with updated ID but same attributes
36
+ sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue })
37
+
38
+ const edgesToUpdate: EdgeToUpdate[] = []
39
+
40
+ // Process all edges connected to this node
41
+ sigmaGraph.forEachEdge(String(nodeId), (edge, attributes, source, target) => {
42
+ const otherNode = source === String(nodeId) ? target : source
43
+ const isOutgoing = source === String(nodeId)
44
+
45
+ // Get original edge dynamic ID for later reference
46
+ const originalEdgeDynamicId = edge
47
+ const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId]
48
+
49
+ // Create new edge with updated node reference
50
+ const newEdgeId = sigmaGraph.addEdge(
51
+ isOutgoing ? newValue : otherNode,
52
+ isOutgoing ? otherNode : newValue,
53
+ attributes
54
+ )
55
+
56
+ // Track edges that need updating in the raw graph
57
+ if (edgeIndexInRawGraph !== undefined) {
58
+ edgesToUpdate.push({
59
+ originalDynamicId: originalEdgeDynamicId,
60
+ newEdgeId: newEdgeId,
61
+ edgeIndex: edgeIndexInRawGraph
62
+ })
63
+ }
64
+
65
+ // Remove the old edge
66
+ sigmaGraph.dropEdge(edge)
67
+ })
68
+
69
+ // Remove the old node after all edges are processed
70
+ sigmaGraph.dropNode(String(nodeId))
71
+
72
+ // Update node reference in raw graph data
73
+ const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
74
+ if (nodeIndex !== undefined) {
75
+ rawGraph.nodes[nodeIndex].id = newValue
76
+ rawGraph.nodes[nodeIndex].properties.entity_id = newValue
77
+ delete rawGraph.nodeIdMap[String(nodeId)]
78
+ rawGraph.nodeIdMap[newValue] = nodeIndex
79
+ }
80
+
81
+ // Update all edge references in raw graph data
82
+ edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => {
83
+ if (rawGraph.edges[edgeIndex]) {
84
+ // Update source/target references
85
+ if (rawGraph.edges[edgeIndex].source === String(nodeId)) {
86
+ rawGraph.edges[edgeIndex].source = newValue
87
+ }
88
+ if (rawGraph.edges[edgeIndex].target === String(nodeId)) {
89
+ rawGraph.edges[edgeIndex].target = newValue
90
+ }
91
+
92
+ // Update dynamic ID mappings
93
+ rawGraph.edges[edgeIndex].dynamicId = newEdgeId
94
+ delete rawGraph.edgeDynamicIdMap[originalDynamicId]
95
+ rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex
96
+ }
97
+ })
98
+
99
+ // Update selected node in store
100
+ useGraphStore.getState().setSelectedNode(newValue)
101
+ } else {
102
+ // For other properties, just update the property in raw graph
103
+ const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
104
+ if (nodeIndex !== undefined) {
105
+ rawGraph.nodes[nodeIndex].properties[propertyName] = newValue
106
+ }
107
+ }
108
+ } catch (error) {
109
+ console.error('Error updating node in graph:', error)
110
+ throw new Error('Failed to update node in graph')
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Update edge in the graph visualization
116
+ *
117
+ * @param sourceId - ID of the source node
118
+ * @param targetId - ID of the target node
119
+ * @param propertyName - Name of the property being updated
120
+ * @param newValue - New value for the property
121
+ */
122
+ export const updateGraphEdge = async (sourceId: string, targetId: string, propertyName: string, newValue: string) => {
123
+ // Get graph state from store
124
+ const sigmaGraph = useGraphStore.getState().sigmaGraph
125
+ const rawGraph = useGraphStore.getState().rawGraph
126
+
127
+ // Validate graph state
128
+ if (!sigmaGraph || !rawGraph) {
129
+ return
130
+ }
131
+
132
+ try {
133
+ // Find the edge between source and target nodes
134
+ const allEdges = sigmaGraph.edges()
135
+ let keyToUse = null
136
+
137
+ for (const edge of allEdges) {
138
+ const edgeSource = sigmaGraph.source(edge)
139
+ const edgeTarget = sigmaGraph.target(edge)
140
+
141
+ // Match edge in either direction (undirected graph support)
142
+ if ((edgeSource === sourceId && edgeTarget === targetId) ||
143
+ (edgeSource === targetId && edgeTarget === sourceId)) {
144
+ keyToUse = edge
145
+ break
146
+ }
147
+ }
148
+
149
+ if (keyToUse !== null) {
150
+ // Special handling for keywords property (updates edge label)
151
+ if(propertyName === 'keywords') {
152
+ sigmaGraph.setEdgeAttribute(keyToUse, 'label', newValue)
153
+ } else {
154
+ sigmaGraph.setEdgeAttribute(keyToUse, propertyName, newValue)
155
+ }
156
+
157
+ // Update edge in raw graph data using dynamic ID mapping
158
+ if (keyToUse && rawGraph.edgeDynamicIdMap[keyToUse] !== undefined) {
159
+ const edgeIndex = rawGraph.edgeDynamicIdMap[keyToUse]
160
+ if (rawGraph.edges[edgeIndex]) {
161
+ rawGraph.edges[edgeIndex].properties[propertyName] = newValue
162
+ }
163
+ } else if (keyToUse !== null) {
164
+ // Fallback: try to find edge by key in edge ID map
165
+ const edgeIndexByKey = rawGraph.edgeIdMap[keyToUse]
166
+ if (edgeIndexByKey !== undefined && rawGraph.edges[edgeIndexByKey]) {
167
+ rawGraph.edges[edgeIndexByKey].properties[propertyName] = newValue
168
+ }
169
+ }
170
+ }
171
+ } catch (error) {
172
+ console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error)
173
+ throw new Error('Failed to update edge in graph')
174
+ }
175
+ }