diff --git a/lightrag/api/routers/graph_routes.py b/lightrag/api/routers/graph_routes.py index 97a397d4..e77e959a 100644 --- a/lightrag/api/routers/graph_routes.py +++ b/lightrag/api/routers/graph_routes.py @@ -17,6 +17,12 @@ class EntityUpdateRequest(BaseModel): allow_rename: bool = False +class RelationUpdateRequest(BaseModel): + source_id: str + target_id: str + updated_data: Dict[str, Any] + + def create_graph_routes(rag, api_key: Optional[str] = None): combined_auth = get_combined_auth_dependency(api_key) @@ -107,4 +113,32 @@ def create_graph_routes(rag, api_key: Optional[str] = None): status_code=500, detail=f"Error updating entity: {str(e)}" ) + @router.post("/graph/relation/edit", dependencies=[Depends(combined_auth)]) + async def update_relation(request: RelationUpdateRequest): + """Update a relation's properties in the knowledge graph + + Args: + request (RelationUpdateRequest): Request containing source ID, target ID and updated data + + Returns: + Dict: Updated relation information + """ + try: + result = await rag.aedit_relation( + source_entity=request.source_id, + target_entity=request.target_id, + updated_data=request.updated_data, + ) + return { + "status": "success", + "message": "Relation updated successfully", + "data": result, + } + except ValueError as ve: + raise HTTPException(status_code=400, detail=str(ve)) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error updating relation: {str(e)}" + ) + return router diff --git a/lightrag_webui/src/api/lightrag.ts b/lightrag_webui/src/api/lightrag.ts index 63f4e2f6..d0ca7d63 100644 --- a/lightrag_webui/src/api/lightrag.ts +++ b/lightrag_webui/src/api/lightrag.ts @@ -540,8 +540,8 @@ export const updateRelation = async ( updatedData: Record ): Promise => { const response = await axiosInstance.post('/graph/relation/edit', { - source_entity: sourceEntity, - target_entity: targetEntity, + source_id: sourceEntity, + target_id: targetEntity, updated_data: updatedData }) return response.data diff --git a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx index bf583a0e..ac92b335 100644 --- a/lightrag_webui/src/components/graph/EditablePropertyRow.tsx +++ b/lightrag_webui/src/components/graph/EditablePropertyRow.tsx @@ -6,6 +6,7 @@ import { toast } from 'sonner' import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag' import { useGraphStore } from '@/stores/graph' import { PencilIcon } from 'lucide-react' +import { tr } from '@faker-js/faker' interface EditablePropertyRowProps { name: string @@ -26,7 +27,7 @@ interface EditablePropertyRowProps { */ const EditablePropertyRow = ({ name, - value, + value: initialValue, onClick, tooltip, entityId, @@ -40,12 +41,18 @@ const EditablePropertyRow = ({ const [isEditing, setIsEditing] = useState(false) const [editValue, setEditValue] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) + const [currentValue, setCurrentValue] = useState(initialValue) const inputRef = useRef(null) + // Update currentValue when initialValue changes + useEffect(() => { + setCurrentValue(initialValue) + }, [initialValue]) + // Initialize edit value when entering edit mode useEffect(() => { if (isEditing) { - setEditValue(String(value)) + setEditValue(String(currentValue)) // Focus the input element when entering edit mode setTimeout(() => { if (inputRef.current) { @@ -54,7 +61,7 @@ const EditablePropertyRow = ({ } }, 50) } - }, [isEditing, value]) + }, [isEditing, currentValue]) const getPropertyNameTranslation = (propName: string) => { const translationKey = `graphPanel.propertiesView.node.propertyNames.${propName}` @@ -91,10 +98,34 @@ const EditablePropertyRow = ({ if (propertyName === 'entity_id') { sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue }) + interface EdgeToUpdate { + originalDynamicId: string; + newEdgeId: string; + edgeIndex: number; + } + + const edgesToUpdate: EdgeToUpdate[] = []; + sigmaGraph.forEachEdge(String(nodeId), (edge, attributes, source, target) => { const otherNode = source === String(nodeId) ? target : source const isOutgoing = source === String(nodeId) - sigmaGraph.addEdge(isOutgoing ? newValue : otherNode, isOutgoing ? otherNode : newValue, attributes) + + // 获取原始边的dynamicId,以便后续更新edgeDynamicIdMap + const originalEdgeDynamicId = edge + const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId] + + // 创建新边并获取新边的ID + const newEdgeId = sigmaGraph.addEdge(isOutgoing ? newValue : otherNode, isOutgoing ? otherNode : newValue, attributes) + + // 存储需要更新的边信息 + if (edgeIndexInRawGraph !== undefined) { + edgesToUpdate.push({ + originalDynamicId: originalEdgeDynamicId, + newEdgeId: newEdgeId, + edgeIndex: edgeIndexInRawGraph + }) + } + sigmaGraph.dropEdge(edge) }) @@ -107,42 +138,112 @@ const EditablePropertyRow = ({ delete rawGraph.nodeIdMap[String(nodeId)] rawGraph.nodeIdMap[newValue] = nodeIndex } - } else { - const updatedAttributes = { ...nodeAttributes } - if (propertyName === 'description') { - updatedAttributes.description = newValue - } - Object.entries(updatedAttributes).forEach(([key, value]) => { - sigmaGraph.setNodeAttribute(String(nodeId), key, value) + + // 更新边的引用关系 + edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => { + // 更新边的source和target + if (rawGraph.edges[edgeIndex]) { + if (rawGraph.edges[edgeIndex].source === String(nodeId)) { + rawGraph.edges[edgeIndex].source = newValue + } + if (rawGraph.edges[edgeIndex].target === String(nodeId)) { + rawGraph.edges[edgeIndex].target = newValue + } + + // 更新dynamicId映射 + rawGraph.edges[edgeIndex].dynamicId = newEdgeId + delete rawGraph.edgeDynamicIdMap[originalDynamicId] + rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex + } }) + useGraphStore.getState().setSelectedNode(editValue) + } else { + // const updatedAttributes = { ...nodeAttributes } + // if (propertyName === 'description') { + // updatedAttributes.description = newValue + // } + // Object.entries(updatedAttributes).forEach(([key, value]) => { + // sigmaGraph.setNodeAttribute(String(nodeId), key, value) + // }) + const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] if (nodeIndex !== undefined) { rawGraph.nodes[nodeIndex].properties[propertyName] = newValue } } - - const selectedNode = useGraphStore.getState().selectedNode - if (selectedNode === String(nodeId)) { - useGraphStore.getState().setSelectedNode(newValue) - } - - const focusedNode = useGraphStore.getState().focusedNode - if (focusedNode === String(nodeId)) { - useGraphStore.getState().setFocusedNode(newValue) - } - - sigmaInstance.refresh() } catch (error) { console.error('Error updating node in graph:', error) throw new Error('Failed to update node in graph') } } + const updateGraphEdge = async (sourceId: string, targetId: string, propertyName: string, newValue: string) => { + const sigmaInstance = useGraphStore.getState().sigmaInstance + const sigmaGraph = useGraphStore.getState().sigmaGraph + const rawGraph = useGraphStore.getState().rawGraph + + if (!sigmaInstance || !sigmaGraph || !rawGraph) { + return + } + + try { + const allEdges = sigmaGraph.edges() + let keyToUse = null + + for (const edge of allEdges) { + const edgeSource = sigmaGraph.source(edge) + const edgeTarget = sigmaGraph.target(edge) + + if ((edgeSource === sourceId && edgeTarget === targetId) || + (edgeSource === targetId && edgeTarget === sourceId)) { + keyToUse = edge + break + } + } + + if (keyToUse !== null) { + if(propertyName === 'keywords') { + sigmaGraph.setEdgeAttribute(keyToUse, 'label', newValue); + } else { + sigmaGraph.setEdgeAttribute(keyToUse, propertyName, newValue); + } + + if (keyToUse && rawGraph.edgeDynamicIdMap[keyToUse] !== undefined) { + const edgeIndex = rawGraph.edgeDynamicIdMap[keyToUse]; + if (rawGraph.edges[edgeIndex]) { + rawGraph.edges[edgeIndex].properties[propertyName] = newValue; + } else { + console.warn(`Edge index ${edgeIndex} found but edge data missing in rawGraph for dynamicId ${entityId}`); + } + } else { + console.warn(`Could not find edge with dynamicId ${entityId} in rawGraph.edgeDynamicIdMap to update properties.`); + if (keyToUse !== null) { + const edgeIndexByKey = rawGraph.edgeIdMap[keyToUse]; + if (edgeIndexByKey !== undefined && rawGraph.edges[edgeIndexByKey]) { + rawGraph.edges[edgeIndexByKey].properties[propertyName] = newValue; + console.log(`Updated rawGraph edge using constructed key ${keyToUse}`); + } else { + console.warn(`Could not find edge in rawGraph using key ${keyToUse} either.`); + } + } else { + console.warn('Cannot update edge properties: edge key is null'); + } + } + } else { + console.warn(`Edge not found in sigmaGraph with key ${keyToUse}`); + } + } catch (error) { + // Log the specific edge key that caused the error if possible + console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error); + throw new Error('Failed to update edge in graph') + } + } + const handleSave = async () => { if (isSubmitting) return - if (editValue === String(value)) { + if (editValue === String(currentValue)) { setIsEditing(false) return } @@ -150,112 +251,71 @@ const EditablePropertyRow = ({ setIsSubmitting(true) try { - const updatedData: Record = {} - if (entityType === 'node' && entityId) { + let updatedData = { [name]: editValue } + if (name === 'entity_id') { - if (editValue !== String(value)) { - const exists = await checkEntityNameExists(editValue) - if (exists) { - toast.error(t('graphPanel.propertiesView.errors.duplicateName')) - return - } - } - updatedData['entity_name'] = editValue - await updateEntity(String(value), updatedData, true) - await updateGraphNode(String(value), 'entity_id', editValue) - } else { - updatedData[name] = editValue - await updateEntity(entityId, updatedData) - if (name === 'description') { - await updateGraphNode(entityId, name, editValue) + const exists = await checkEntityNameExists(editValue) + if (exists) { + toast.error(t('graphPanel.propertiesView.errors.duplicateName')) + setIsSubmitting(false) + return } + updatedData = { 'entity_name': editValue } } + await updateEntity(entityId, updatedData, true) + await updateGraphNode(entityId, name, editValue) toast.success(t('graphPanel.propertiesView.success.entityUpdated')) } else if (entityType === 'edge' && sourceId && targetId) { - updatedData[name] = editValue + const updatedData = { [name]: editValue } await updateRelation(sourceId, targetId, updatedData) + await updateGraphEdge(sourceId, targetId, name, editValue) toast.success(t('graphPanel.propertiesView.success.relationUpdated')) } - if (onValueChange) { - onValueChange(editValue) - } - - useGraphStore.getState().setGraphDataFetchAttempted(false) - useGraphStore.getState().setLabelsFetchAttempted(false) - - const currentNodeId = name === 'entity_id' ? editValue : (entityId || '') - useGraphStore.getState().setSelectedNode(null) - useGraphStore.getState().setSelectedNode(currentNodeId) - } catch (error: any) { + setIsEditing(false) + setCurrentValue(editValue) + } catch (error) { console.error('Error updating property:', error) - let detailMessage = t('graphPanel.propertiesView.errors.updateFailed') - - if (error.response?.data?.detail) { - detailMessage = error.response.data.detail - } else if (error.response?.data?.message) { - detailMessage = error.response.data.message - } else if (error.message) { - detailMessage = error.message - } - - console.error('Update failed:', { - entityType, - entityId, - propertyName: name, - newValue: editValue, - error: error.response?.data || error.message - }) - - toast.error(detailMessage, { - description: t('graphPanel.propertiesView.errors.tryAgainLater') - }) + toast.error(t('graphPanel.propertiesView.errors.updateFailed')) } finally { setIsSubmitting(false) - setIsEditing(false) } } - // Determine if this property should be editable - // Currently only 'description' and 'entity_id' fields are editable - const isEditableField = isEditable && (name === 'description' || name === 'entity_id') - + // Always render the property name label and edit icon, regardless of edit state return ( -
-
- {getPropertyNameTranslation(name)} - {isEditableField && ( -
- -
- {t('graphPanel.propertiesView.doubleClickToEdit')} -
-
- )} +
+ {getPropertyNameTranslation(name)} +
+ setIsEditing(true)} + /> +
+ {t('graphPanel.propertiesView.doubleClickToEdit')} +
: {isEditing ? ( -
- setEditValue(e.target.value)} - onBlur={handleSave} - onKeyDown={handleKeyDown} - disabled={isSubmitting} - /> -
+ // Render input field when editing + setEditValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSave} + className="h-6 text-xs" + disabled={isSubmitting} + /> ) : ( -
+ // Render text component when not editing +
diff --git a/lightrag_webui/src/components/graph/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx index c2cda263..257b69ec 100644 --- a/lightrag_webui/src/components/graph/PropertiesView.tsx +++ b/lightrag_webui/src/components/graph/PropertiesView.tsx @@ -195,8 +195,8 @@ const PropertyRow = ({ return translation === translationKey ? name : translation } - // Use EditablePropertyRow for editable fields (description and entity_id) - if (isEditable && (name === 'description' || name === 'entity_id')) { + // Use EditablePropertyRow for editable fields (description, entity_id and keywords) + if (isEditable && (name === 'description' || name === 'entity_id' || name === 'keywords')) { return ( { value={edge.properties[name]} entityId={edge.id} entityType="edge" - sourceId={edge.sourceNode?.properties['entity_id'] || edge.source} - targetId={edge.targetNode?.properties['entity_id'] || edge.target} - isEditable={name === 'description'} + sourceId={edge.source} + targetId={edge.target} + isEditable={name === 'description' || name === 'keywords'} /> ) })}