567 lines
22 KiB
Python
567 lines
22 KiB
Python
"""
|
|
This module contains all graph-related routes for the LightRAG API.
|
|
"""
|
|
|
|
from typing import Optional, Dict, Any
|
|
import traceback
|
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
|
|
from lightrag.utils import logger
|
|
from ..utils_api import get_combined_auth_dependency
|
|
|
|
router = APIRouter(tags=["graph"])
|
|
|
|
|
|
class EntityUpdateRequest(BaseModel):
|
|
entity_name: str
|
|
updated_data: Dict[str, Any]
|
|
allow_rename: bool = False
|
|
allow_merge: bool = False
|
|
|
|
|
|
class RelationUpdateRequest(BaseModel):
|
|
source_id: str
|
|
target_id: str
|
|
updated_data: Dict[str, Any]
|
|
|
|
|
|
class EntityMergeRequest(BaseModel):
|
|
entities_to_change: list[str] = Field(
|
|
...,
|
|
description="List of entity names to be merged and deleted. These are typically duplicate or misspelled entities.",
|
|
min_length=1,
|
|
examples=[["Elon Msk", "Ellon Musk"]],
|
|
)
|
|
entity_to_change_into: str = Field(
|
|
...,
|
|
description="Target entity name that will receive all relationships from the source entities. This entity will be preserved.",
|
|
min_length=1,
|
|
examples=["Elon Musk"],
|
|
)
|
|
|
|
|
|
class EntityCreateRequest(BaseModel):
|
|
entity_name: str = Field(
|
|
...,
|
|
description="Unique name for the new entity",
|
|
min_length=1,
|
|
examples=["Tesla"],
|
|
)
|
|
entity_data: Dict[str, Any] = Field(
|
|
...,
|
|
description="Dictionary containing entity properties. Common fields include 'description' and 'entity_type'.",
|
|
examples=[
|
|
{
|
|
"description": "Electric vehicle manufacturer",
|
|
"entity_type": "ORGANIZATION",
|
|
}
|
|
],
|
|
)
|
|
|
|
|
|
class RelationCreateRequest(BaseModel):
|
|
source_entity: str = Field(
|
|
...,
|
|
description="Name of the source entity. This entity must already exist in the knowledge graph.",
|
|
min_length=1,
|
|
examples=["Elon Musk"],
|
|
)
|
|
target_entity: str = Field(
|
|
...,
|
|
description="Name of the target entity. This entity must already exist in the knowledge graph.",
|
|
min_length=1,
|
|
examples=["Tesla"],
|
|
)
|
|
relation_data: Dict[str, Any] = Field(
|
|
...,
|
|
description="Dictionary containing relationship properties. Common fields include 'description', 'keywords', and 'weight'.",
|
|
examples=[
|
|
{
|
|
"description": "Elon Musk is the CEO of Tesla",
|
|
"keywords": "CEO, founder",
|
|
"weight": 1.0,
|
|
}
|
|
],
|
|
)
|
|
|
|
|
|
def create_graph_routes(rag, api_key: Optional[str] = None):
|
|
combined_auth = get_combined_auth_dependency(api_key)
|
|
|
|
@router.get("/graph/label/list", dependencies=[Depends(combined_auth)])
|
|
async def get_graph_labels():
|
|
"""
|
|
Get all graph labels
|
|
|
|
Returns:
|
|
List[str]: List of graph labels
|
|
"""
|
|
try:
|
|
return await rag.get_graph_labels()
|
|
except Exception as e:
|
|
logger.error(f"Error getting graph labels: {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error getting graph labels: {str(e)}"
|
|
)
|
|
|
|
@router.get("/graph/label/popular", dependencies=[Depends(combined_auth)])
|
|
async def get_popular_labels(
|
|
limit: int = Query(
|
|
300, description="Maximum number of popular labels to return", ge=1, le=1000
|
|
),
|
|
):
|
|
"""
|
|
Get popular labels by node degree (most connected entities)
|
|
|
|
Args:
|
|
limit (int): Maximum number of labels to return (default: 300, max: 1000)
|
|
|
|
Returns:
|
|
List[str]: List of popular labels sorted by degree (highest first)
|
|
"""
|
|
try:
|
|
return await rag.chunk_entity_relation_graph.get_popular_labels(limit)
|
|
except Exception as e:
|
|
logger.error(f"Error getting popular labels: {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error getting popular labels: {str(e)}"
|
|
)
|
|
|
|
@router.get("/graph/label/search", dependencies=[Depends(combined_auth)])
|
|
async def search_labels(
|
|
q: str = Query(..., description="Search query string"),
|
|
limit: int = Query(
|
|
50, description="Maximum number of search results to return", ge=1, le=100
|
|
),
|
|
):
|
|
"""
|
|
Search labels with fuzzy matching
|
|
|
|
Args:
|
|
q (str): Search query string
|
|
limit (int): Maximum number of results to return (default: 50, max: 100)
|
|
|
|
Returns:
|
|
List[str]: List of matching labels sorted by relevance
|
|
"""
|
|
try:
|
|
return await rag.chunk_entity_relation_graph.search_labels(q, limit)
|
|
except Exception as e:
|
|
logger.error(f"Error searching labels with query '{q}': {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error searching labels: {str(e)}"
|
|
)
|
|
|
|
@router.get("/graphs", dependencies=[Depends(combined_auth)])
|
|
async def get_knowledge_graph(
|
|
label: str = Query(..., description="Label to get knowledge graph for"),
|
|
max_depth: int = Query(3, description="Maximum depth of graph", ge=1),
|
|
max_nodes: int = Query(1000, description="Maximum nodes to return", ge=1),
|
|
):
|
|
"""
|
|
Retrieve a connected subgraph of nodes where the label includes the specified label.
|
|
When reducing the number of nodes, the prioritization criteria are as follows:
|
|
1. Hops(path) to the staring node take precedence
|
|
2. Followed by the degree of the nodes
|
|
|
|
Args:
|
|
label (str): Label of the starting node
|
|
max_depth (int, optional): Maximum depth of the subgraph,Defaults to 3
|
|
max_nodes: Maxiumu nodes to return
|
|
|
|
Returns:
|
|
Dict[str, List[str]]: Knowledge graph for label
|
|
"""
|
|
try:
|
|
# Log the label parameter to check for leading spaces
|
|
logger.debug(
|
|
f"get_knowledge_graph called with label: '{label}' (length: {len(label)}, repr: {repr(label)})"
|
|
)
|
|
|
|
return await rag.get_knowledge_graph(
|
|
node_label=label,
|
|
max_depth=max_depth,
|
|
max_nodes=max_nodes,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error getting knowledge graph for label '{label}': {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error getting knowledge graph: {str(e)}"
|
|
)
|
|
|
|
@router.get("/graph/entity/exists", dependencies=[Depends(combined_auth)])
|
|
async def check_entity_exists(
|
|
name: str = Query(..., description="Entity name to check"),
|
|
):
|
|
"""
|
|
Check if an entity with the given name exists in the knowledge graph
|
|
|
|
Args:
|
|
name (str): Name of the entity to check
|
|
|
|
Returns:
|
|
Dict[str, bool]: Dictionary with 'exists' key indicating if entity exists
|
|
"""
|
|
try:
|
|
exists = await rag.chunk_entity_relation_graph.has_node(name)
|
|
return {"exists": exists}
|
|
except Exception as e:
|
|
logger.error(f"Error checking entity existence for '{name}': {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error checking entity existence: {str(e)}"
|
|
)
|
|
|
|
@router.post("/graph/entity/edit", dependencies=[Depends(combined_auth)])
|
|
async def update_entity(request: EntityUpdateRequest):
|
|
"""
|
|
Update an entity's properties in the knowledge graph
|
|
|
|
This endpoint allows updating entity properties, including renaming entities.
|
|
When renaming to an existing entity name, the behavior depends on allow_merge:
|
|
|
|
Args:
|
|
request (EntityUpdateRequest): Request containing:
|
|
- entity_name (str): Name of the entity to update
|
|
- updated_data (Dict[str, Any]): Dictionary of properties to update
|
|
- allow_rename (bool): Whether to allow entity renaming (default: False)
|
|
- allow_merge (bool): Whether to merge into existing entity when renaming
|
|
causes name conflict (default: False)
|
|
|
|
Returns:
|
|
Dict: Updated entity information with status
|
|
|
|
Behavior when renaming to an existing entity:
|
|
- If allow_merge=False: Raises ValueError with 400 status (default behavior)
|
|
- If allow_merge=True: Automatically merges the source entity into the existing target entity,
|
|
preserving all relationships and applying non-name updates first
|
|
|
|
Example Request (simple update):
|
|
POST /graph/entity/edit
|
|
{
|
|
"entity_name": "Tesla",
|
|
"updated_data": {"description": "Updated description"},
|
|
"allow_rename": false,
|
|
"allow_merge": false
|
|
}
|
|
|
|
Example Request (rename with auto-merge):
|
|
POST /graph/entity/edit
|
|
{
|
|
"entity_name": "Elon Msk",
|
|
"updated_data": {
|
|
"entity_name": "Elon Musk",
|
|
"description": "Corrected description"
|
|
},
|
|
"allow_rename": true,
|
|
"allow_merge": true
|
|
}
|
|
"""
|
|
try:
|
|
result = await rag.aedit_entity(
|
|
entity_name=request.entity_name,
|
|
updated_data=request.updated_data,
|
|
allow_rename=request.allow_rename,
|
|
allow_merge=request.allow_merge,
|
|
)
|
|
return {
|
|
"status": "success",
|
|
"message": "Entity updated successfully",
|
|
"data": result,
|
|
}
|
|
except ValueError as ve:
|
|
logger.error(
|
|
f"Validation error updating entity '{request.entity_name}': {str(ve)}"
|
|
)
|
|
raise HTTPException(status_code=400, detail=str(ve))
|
|
except Exception as e:
|
|
logger.error(f"Error updating entity '{request.entity_name}': {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
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:
|
|
logger.error(
|
|
f"Validation error updating relation between '{request.source_id}' and '{request.target_id}': {str(ve)}"
|
|
)
|
|
raise HTTPException(status_code=400, detail=str(ve))
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error updating relation between '{request.source_id}' and '{request.target_id}': {str(e)}"
|
|
)
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error updating relation: {str(e)}"
|
|
)
|
|
|
|
@router.post("/graph/entity/create", dependencies=[Depends(combined_auth)])
|
|
async def create_entity(request: EntityCreateRequest):
|
|
"""
|
|
Create a new entity in the knowledge graph
|
|
|
|
This endpoint creates a new entity node in the knowledge graph with the specified
|
|
properties. The system automatically generates vector embeddings for the entity
|
|
to enable semantic search and retrieval.
|
|
|
|
Request Body:
|
|
entity_name (str): Unique name identifier for the entity
|
|
entity_data (dict): Entity properties including:
|
|
- description (str): Textual description of the entity
|
|
- entity_type (str): Category/type of the entity (e.g., PERSON, ORGANIZATION, LOCATION)
|
|
- source_id (str): Related chunk_id from which the description originates
|
|
- Additional custom properties as needed
|
|
|
|
Response Schema:
|
|
{
|
|
"status": "success",
|
|
"message": "Entity 'Tesla' created successfully",
|
|
"data": {
|
|
"entity_name": "Tesla",
|
|
"description": "Electric vehicle manufacturer",
|
|
"entity_type": "ORGANIZATION",
|
|
"source_id": "chunk-123<SEP>chunk-456"
|
|
... (other entity properties)
|
|
}
|
|
}
|
|
|
|
HTTP Status Codes:
|
|
200: Entity created successfully
|
|
400: Invalid request (e.g., missing required fields, duplicate entity)
|
|
500: Internal server error
|
|
|
|
Example Request:
|
|
POST /graph/entity/create
|
|
{
|
|
"entity_name": "Tesla",
|
|
"entity_data": {
|
|
"description": "Electric vehicle manufacturer",
|
|
"entity_type": "ORGANIZATION"
|
|
}
|
|
}
|
|
"""
|
|
try:
|
|
# Use the proper acreate_entity method which handles:
|
|
# - Graph lock for concurrency
|
|
# - Vector embedding creation in entities_vdb
|
|
# - Metadata population and defaults
|
|
# - Index consistency via _edit_entity_done
|
|
result = await rag.acreate_entity(
|
|
entity_name=request.entity_name,
|
|
entity_data=request.entity_data,
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": f"Entity '{request.entity_name}' created successfully",
|
|
"data": result,
|
|
}
|
|
except ValueError as ve:
|
|
logger.error(
|
|
f"Validation error creating entity '{request.entity_name}': {str(ve)}"
|
|
)
|
|
raise HTTPException(status_code=400, detail=str(ve))
|
|
except Exception as e:
|
|
logger.error(f"Error creating entity '{request.entity_name}': {str(e)}")
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error creating entity: {str(e)}"
|
|
)
|
|
|
|
@router.post("/graph/relation/create", dependencies=[Depends(combined_auth)])
|
|
async def create_relation(request: RelationCreateRequest):
|
|
"""
|
|
Create a new relationship between two entities in the knowledge graph
|
|
|
|
This endpoint establishes an undirected relationship between two existing entities.
|
|
The provided source/target order is accepted for convenience, but the backend
|
|
stored edge is undirected and may be returned with the entities swapped.
|
|
Both entities must already exist in the knowledge graph. The system automatically
|
|
generates vector embeddings for the relationship to enable semantic search and graph traversal.
|
|
|
|
Prerequisites:
|
|
- Both source_entity and target_entity must exist in the knowledge graph
|
|
- Use /graph/entity/create to create entities first if they don't exist
|
|
|
|
Request Body:
|
|
source_entity (str): Name of the source entity (relationship origin)
|
|
target_entity (str): Name of the target entity (relationship destination)
|
|
relation_data (dict): Relationship properties including:
|
|
- description (str): Textual description of the relationship
|
|
- keywords (str): Comma-separated keywords describing the relationship type
|
|
- source_id (str): Related chunk_id from which the description originates
|
|
- weight (float): Relationship strength/importance (default: 1.0)
|
|
- Additional custom properties as needed
|
|
|
|
Response Schema:
|
|
{
|
|
"status": "success",
|
|
"message": "Relation created successfully between 'Elon Musk' and 'Tesla'",
|
|
"data": {
|
|
"src_id": "Elon Musk",
|
|
"tgt_id": "Tesla",
|
|
"description": "Elon Musk is the CEO of Tesla",
|
|
"keywords": "CEO, founder",
|
|
"source_id": "chunk-123<SEP>chunk-456"
|
|
"weight": 1.0,
|
|
... (other relationship properties)
|
|
}
|
|
}
|
|
|
|
HTTP Status Codes:
|
|
200: Relationship created successfully
|
|
400: Invalid request (e.g., missing entities, invalid data, duplicate relationship)
|
|
500: Internal server error
|
|
|
|
Example Request:
|
|
POST /graph/relation/create
|
|
{
|
|
"source_entity": "Elon Musk",
|
|
"target_entity": "Tesla",
|
|
"relation_data": {
|
|
"description": "Elon Musk is the CEO of Tesla",
|
|
"keywords": "CEO, founder",
|
|
"weight": 1.0
|
|
}
|
|
}
|
|
"""
|
|
try:
|
|
# Use the proper acreate_relation method which handles:
|
|
# - Graph lock for concurrency
|
|
# - Entity existence validation
|
|
# - Duplicate relation checks
|
|
# - Vector embedding creation in relationships_vdb
|
|
# - Index consistency via _edit_relation_done
|
|
result = await rag.acreate_relation(
|
|
source_entity=request.source_entity,
|
|
target_entity=request.target_entity,
|
|
relation_data=request.relation_data,
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": f"Relation created successfully between '{request.source_entity}' and '{request.target_entity}'",
|
|
"data": result,
|
|
}
|
|
except ValueError as ve:
|
|
logger.error(
|
|
f"Validation error creating relation between '{request.source_entity}' and '{request.target_entity}': {str(ve)}"
|
|
)
|
|
raise HTTPException(status_code=400, detail=str(ve))
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error creating relation between '{request.source_entity}' and '{request.target_entity}': {str(e)}"
|
|
)
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error creating relation: {str(e)}"
|
|
)
|
|
|
|
@router.post("/graph/entities/merge", dependencies=[Depends(combined_auth)])
|
|
async def merge_entities(request: EntityMergeRequest):
|
|
"""
|
|
Merge multiple entities into a single entity, preserving all relationships
|
|
|
|
This endpoint consolidates duplicate or misspelled entities while preserving the entire
|
|
graph structure. It's particularly useful for cleaning up knowledge graphs after document
|
|
processing or correcting entity name variations.
|
|
|
|
What the Merge Operation Does:
|
|
1. Deletes the specified source entities from the knowledge graph
|
|
2. Transfers all relationships from source entities to the target entity
|
|
3. Intelligently merges duplicate relationships (if multiple sources have the same relationship)
|
|
4. Updates vector embeddings for accurate retrieval and search
|
|
5. Preserves the complete graph structure and connectivity
|
|
6. Maintains relationship properties and metadata
|
|
|
|
Use Cases:
|
|
- Fixing spelling errors in entity names (e.g., "Elon Msk" -> "Elon Musk")
|
|
- Consolidating duplicate entities discovered after document processing
|
|
- Merging name variations (e.g., "NY", "New York", "New York City")
|
|
- Cleaning up the knowledge graph for better query performance
|
|
- Standardizing entity names across the knowledge base
|
|
|
|
Request Body:
|
|
entities_to_change (list[str]): List of entity names to be merged and deleted
|
|
entity_to_change_into (str): Target entity that will receive all relationships
|
|
|
|
Response Schema:
|
|
{
|
|
"status": "success",
|
|
"message": "Successfully merged 2 entities into 'Elon Musk'",
|
|
"data": {
|
|
"merged_entity": "Elon Musk",
|
|
"deleted_entities": ["Elon Msk", "Ellon Musk"],
|
|
"relationships_transferred": 15,
|
|
... (merge operation details)
|
|
}
|
|
}
|
|
|
|
HTTP Status Codes:
|
|
200: Entities merged successfully
|
|
400: Invalid request (e.g., empty entity list, target entity doesn't exist)
|
|
500: Internal server error
|
|
|
|
Example Request:
|
|
POST /graph/entities/merge
|
|
{
|
|
"entities_to_change": ["Elon Msk", "Ellon Musk"],
|
|
"entity_to_change_into": "Elon Musk"
|
|
}
|
|
|
|
Note:
|
|
- The target entity (entity_to_change_into) must exist in the knowledge graph
|
|
- Source entities will be permanently deleted after the merge
|
|
- This operation cannot be undone, so verify entity names before merging
|
|
"""
|
|
try:
|
|
result = await rag.amerge_entities(
|
|
source_entities=request.entities_to_change,
|
|
target_entity=request.entity_to_change_into,
|
|
)
|
|
return {
|
|
"status": "success",
|
|
"message": f"Successfully merged {len(request.entities_to_change)} entities into '{request.entity_to_change_into}'",
|
|
"data": result,
|
|
}
|
|
except ValueError as ve:
|
|
logger.error(
|
|
f"Validation error merging entities {request.entities_to_change} into '{request.entity_to_change_into}': {str(ve)}"
|
|
)
|
|
raise HTTPException(status_code=400, detail=str(ve))
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error merging entities {request.entities_to_change} into '{request.entity_to_change_into}': {str(e)}"
|
|
)
|
|
logger.error(traceback.format_exc())
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Error merging entities: {str(e)}"
|
|
)
|
|
|
|
return router
|