Merge pull request #1367 from danielaskdd/add-graph-db-lock
Browse filesAdd graph_db_lock to ensure consistency across multiple processes for node and edge edition jobs
- lightrag/api/__init__.py +1 -1
- lightrag/api/routers/graph_routes.py +129 -8
- lightrag/api/webui/assets/index-BJDb04H1.css +0 -0
- lightrag/api/webui/assets/{index-vzpYU2q3.js → index-CIRM3gxn.js} +0 -0
- lightrag/api/webui/assets/index-CTB4Vp_z.css +0 -0
- lightrag/api/webui/index.html +0 -0
- lightrag/lightrag.py +180 -1252
- lightrag/utils.py +345 -0
- lightrag/utils_graph.py +1066 -0
- lightrag_webui/src/api/lightrag.ts +55 -0
- lightrag_webui/src/components/graph/EditablePropertyRow.tsx +117 -0
- lightrag_webui/src/components/graph/GraphControl.tsx +9 -3
- lightrag_webui/src/components/graph/PropertiesView.tsx +51 -5
- lightrag_webui/src/components/graph/PropertyEditDialog.tsx +152 -0
- lightrag_webui/src/components/graph/PropertyRowComponents.tsx +53 -0
- lightrag_webui/src/locales/ar.json +13 -1
- lightrag_webui/src/locales/en.json +14 -1
- lightrag_webui/src/locales/fr.json +13 -1
- lightrag_webui/src/locales/zh.json +13 -1
- lightrag_webui/src/locales/zh_TW.json +16 -3
- lightrag_webui/src/stores/graph.ts +8 -0
- lightrag_webui/src/utils/graphOperations.ts +175 -0
lightrag/api/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1 |
-
__api_version__ = "
|
|
|
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 |
-
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
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
|
1431 |
-
|
1432 |
-
return loop.run_until_complete(self.adelete_by_entity(entity_name))
|
1433 |
|
1434 |
-
|
1435 |
-
|
1436 |
-
|
1437 |
-
|
1438 |
-
await self.relationships_vdb.delete_entity_relation(entity_name)
|
1439 |
-
await self.chunk_entity_relation_graph.delete_node(entity_name)
|
1440 |
|
1441 |
-
|
1442 |
-
|
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 |
-
|
1461 |
-
|
1462 |
|
1463 |
-
|
1464 |
-
|
1465 |
-
target_entity: Name of the target entity
|
1466 |
"""
|
1467 |
-
|
1468 |
-
|
1469 |
-
|
1470 |
-
)
|
1471 |
|
1472 |
-
|
1473 |
-
async def adelete_by_relation(self, source_entity: str, target_entity: str) -> None:
|
1474 |
-
"""Asynchronously delete a relation between two entities.
|
1475 |
|
1476 |
-
|
1477 |
-
|
1478 |
-
|
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 |
-
|
1493 |
-
|
1494 |
-
|
1495 |
-
|
1496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1497 |
|
1498 |
-
|
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 |
-
|
1528 |
-
|
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:
|
|
|
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
|
1800 |
-
|
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 |
-
|
1823 |
-
|
1824 |
-
|
1825 |
-
|
1826 |
|
1827 |
-
|
1828 |
-
|
1829 |
-
|
|
|
|
|
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 |
-
|
1841 |
-
|
1842 |
-
|
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
|
1849 |
-
"""
|
1850 |
|
1851 |
Args:
|
1852 |
-
|
1853 |
-
|
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 |
-
|
1867 |
-
logger.warning("No cache storage configured")
|
1868 |
-
return
|
1869 |
|
1870 |
-
|
|
|
|
|
|
|
|
|
|
|
1871 |
|
1872 |
-
|
1873 |
-
|
1874 |
-
|
|
|
|
|
1875 |
|
1876 |
-
|
1877 |
-
|
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 |
-
|
|
|
|
|
|
|
1893 |
|
1894 |
-
|
1895 |
-
|
|
|
|
|
|
|
1896 |
|
1897 |
-
|
1898 |
-
|
1899 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
1918 |
-
|
1919 |
-
|
1920 |
-
|
1921 |
-
|
1922 |
-
|
1923 |
-
|
1924 |
-
|
1925 |
-
|
1926 |
-
|
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 |
-
|
2131 |
-
|
2132 |
-
|
2133 |
-
|
2134 |
-
|
2135 |
-
|
2136 |
-
|
2137 |
-
|
2138 |
-
|
2139 |
-
|
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 |
-
|
2249 |
-
|
2250 |
-
|
2251 |
-
|
2252 |
-
|
2253 |
-
|
2254 |
-
|
2255 |
-
|
2256 |
-
|
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 |
-
|
2329 |
-
|
2330 |
-
|
2331 |
-
|
2332 |
-
|
2333 |
-
|
2334 |
-
|
2335 |
-
|
2336 |
-
|
2337 |
-
|
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 |
-
|
2458 |
-
|
2459 |
-
|
2460 |
-
|
2461 |
-
|
2462 |
-
|
2463 |
-
|
2464 |
-
|
2465 |
-
merge_strategy
|
2466 |
-
|
2467 |
-
|
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 |
-
|
2665 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2666 |
)
|
2667 |
-
|
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 |
-
|
2692 |
-
|
2693 |
-
|
2694 |
-
|
2695 |
-
|
2696 |
-
|
2697 |
-
|
2698 |
-
|
2699 |
-
|
2700 |
-
|
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 |
-
|
|
|
|
|
|
|
103 |
}
|
104 |
},
|
105 |
leaveNode: (event: NodeEvent) => {
|
@@ -108,8 +111,11 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|
108 |
}
|
109 |
},
|
110 |
clickNode: (event: NodeEvent) => {
|
111 |
-
|
112 |
-
|
|
|
|
|
|
|
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 |
-
//
|
188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|