graph_routes.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. """
  2. This module contains all graph-related routes for the LightRAG API.
  3. """
  4. from typing import Optional, Dict, Any
  5. import traceback
  6. from fastapi import APIRouter, Depends, Query, HTTPException
  7. from pydantic import BaseModel, Field
  8. from lightrag.utils import logger
  9. from ..utils_api import get_combined_auth_dependency
  10. from .document_routes import check_pipeline_busy_or_raise
  11. class EntityUpdateRequest(BaseModel):
  12. entity_name: str
  13. updated_data: Dict[str, Any]
  14. allow_rename: bool = False
  15. allow_merge: bool = False
  16. class RelationUpdateRequest(BaseModel):
  17. source_id: str
  18. target_id: str
  19. updated_data: Dict[str, Any]
  20. class EntityMergeRequest(BaseModel):
  21. entities_to_change: list[str] = Field(
  22. ...,
  23. description="List of entity names to be merged and deleted. These are typically duplicate or misspelled entities.",
  24. min_length=1,
  25. examples=[["Elon Msk", "Ellon Musk"]],
  26. )
  27. entity_to_change_into: str = Field(
  28. ...,
  29. description="Target entity name that will receive all relationships from the source entities. This entity will be preserved.",
  30. min_length=1,
  31. examples=["Elon Musk"],
  32. )
  33. class EntityCreateRequest(BaseModel):
  34. entity_name: str = Field(
  35. ...,
  36. description="Unique name for the new entity",
  37. min_length=1,
  38. examples=["Tesla"],
  39. )
  40. entity_data: Dict[str, Any] = Field(
  41. ...,
  42. description="Dictionary containing entity properties. Common fields include 'description' and 'entity_type'.",
  43. examples=[
  44. {
  45. "description": "Electric vehicle manufacturer",
  46. "entity_type": "ORGANIZATION",
  47. }
  48. ],
  49. )
  50. class RelationCreateRequest(BaseModel):
  51. source_entity: str = Field(
  52. ...,
  53. description="Name of the source entity. This entity must already exist in the knowledge graph.",
  54. min_length=1,
  55. examples=["Elon Musk"],
  56. )
  57. target_entity: str = Field(
  58. ...,
  59. description="Name of the target entity. This entity must already exist in the knowledge graph.",
  60. min_length=1,
  61. examples=["Tesla"],
  62. )
  63. relation_data: Dict[str, Any] = Field(
  64. ...,
  65. description="Dictionary containing relationship properties. Common fields include 'description', 'keywords', and 'weight'.",
  66. examples=[
  67. {
  68. "description": "Elon Musk is the CEO of Tesla",
  69. "keywords": "CEO, founder",
  70. "weight": 1.0,
  71. }
  72. ],
  73. )
  74. def create_graph_routes(rag, api_key: Optional[str] = None):
  75. # Fresh router per call. A module-level instance would accumulate
  76. # duplicate routes when the factory is invoked more than once in the
  77. # same process (e.g. across tests), which triggers FastAPI's
  78. # "Duplicate Operation ID" warnings.
  79. router = APIRouter(tags=["graph"])
  80. combined_auth = get_combined_auth_dependency(api_key)
  81. @router.get("/graph/label/list", dependencies=[Depends(combined_auth)])
  82. async def get_graph_labels():
  83. """
  84. Get all graph labels
  85. Returns:
  86. List[str]: List of graph labels
  87. """
  88. try:
  89. return await rag.get_graph_labels()
  90. except Exception as e:
  91. logger.error(f"Error getting graph labels: {str(e)}")
  92. logger.error(traceback.format_exc())
  93. raise HTTPException(
  94. status_code=500, detail=f"Error getting graph labels: {str(e)}"
  95. )
  96. @router.get("/graph/label/popular", dependencies=[Depends(combined_auth)])
  97. async def get_popular_labels(
  98. limit: int = Query(
  99. 300, description="Maximum number of popular labels to return", ge=1, le=1000
  100. ),
  101. ):
  102. """
  103. Get popular labels by node degree (most connected entities)
  104. Args:
  105. limit (int): Maximum number of labels to return (default: 300, max: 1000)
  106. Returns:
  107. List[str]: List of popular labels sorted by degree (highest first)
  108. """
  109. try:
  110. return await rag.chunk_entity_relation_graph.get_popular_labels(limit)
  111. except Exception as e:
  112. logger.error(f"Error getting popular labels: {str(e)}")
  113. logger.error(traceback.format_exc())
  114. raise HTTPException(
  115. status_code=500, detail=f"Error getting popular labels: {str(e)}"
  116. )
  117. @router.get("/graph/label/search", dependencies=[Depends(combined_auth)])
  118. async def search_labels(
  119. q: str = Query(..., description="Search query string"),
  120. limit: int = Query(
  121. 50, description="Maximum number of search results to return", ge=1, le=100
  122. ),
  123. ):
  124. """
  125. Search labels with fuzzy matching
  126. Args:
  127. q (str): Search query string
  128. limit (int): Maximum number of results to return (default: 50, max: 100)
  129. Returns:
  130. List[str]: List of matching labels sorted by relevance
  131. """
  132. try:
  133. return await rag.chunk_entity_relation_graph.search_labels(q, limit)
  134. except Exception as e:
  135. logger.error(f"Error searching labels with query '{q}': {str(e)}")
  136. logger.error(traceback.format_exc())
  137. raise HTTPException(
  138. status_code=500, detail=f"Error searching labels: {str(e)}"
  139. )
  140. @router.get("/graphs", dependencies=[Depends(combined_auth)])
  141. async def get_knowledge_graph(
  142. label: str = Query(..., description="Label to get knowledge graph for"),
  143. max_depth: int = Query(3, description="Maximum depth of graph", ge=1),
  144. max_nodes: int = Query(1000, description="Maximum nodes to return", ge=1),
  145. ):
  146. """
  147. Retrieve a connected subgraph of nodes where the label includes the specified label.
  148. When reducing the number of nodes, the prioritization criteria are as follows:
  149. 1. Hops(path) to the staring node take precedence
  150. 2. Followed by the degree of the nodes
  151. Args:
  152. label (str): Label of the starting node
  153. max_depth (int, optional): Maximum depth of the subgraph,Defaults to 3
  154. max_nodes: Maxiumu nodes to return
  155. Returns:
  156. Dict[str, List[str]]: Knowledge graph for label
  157. """
  158. try:
  159. # Log the label parameter to check for leading spaces
  160. logger.debug(
  161. f"get_knowledge_graph called with label: '{label}' (length: {len(label)}, repr: {repr(label)})"
  162. )
  163. return await rag.get_knowledge_graph(
  164. node_label=label,
  165. max_depth=max_depth,
  166. max_nodes=max_nodes,
  167. )
  168. except Exception as e:
  169. logger.error(f"Error getting knowledge graph for label '{label}': {str(e)}")
  170. logger.error(traceback.format_exc())
  171. raise HTTPException(
  172. status_code=500, detail=f"Error getting knowledge graph: {str(e)}"
  173. )
  174. @router.get("/graph/entity/exists", dependencies=[Depends(combined_auth)])
  175. async def check_entity_exists(
  176. name: str = Query(..., description="Entity name to check"),
  177. ):
  178. """
  179. Check if an entity with the given name exists in the knowledge graph
  180. Args:
  181. name (str): Name of the entity to check
  182. Returns:
  183. Dict[str, bool]: Dictionary with 'exists' key indicating if entity exists
  184. """
  185. try:
  186. exists = await rag.chunk_entity_relation_graph.has_node(name)
  187. return {"exists": exists}
  188. except Exception as e:
  189. logger.error(f"Error checking entity existence for '{name}': {str(e)}")
  190. logger.error(traceback.format_exc())
  191. raise HTTPException(
  192. status_code=500, detail=f"Error checking entity existence: {str(e)}"
  193. )
  194. @router.post("/graph/entity/edit", dependencies=[Depends(combined_auth)])
  195. async def update_entity(request: EntityUpdateRequest):
  196. """
  197. Update an entity's properties in the knowledge graph
  198. This endpoint allows updating entity properties, including renaming entities.
  199. When renaming to an existing entity name, the behavior depends on allow_merge:
  200. Args:
  201. request (EntityUpdateRequest): Request containing:
  202. - entity_name (str): Name of the entity to update
  203. - updated_data (Dict[str, Any]): Dictionary of properties to update
  204. - allow_rename (bool): Whether to allow entity renaming (default: False)
  205. - allow_merge (bool): Whether to merge into existing entity when renaming
  206. causes name conflict (default: False)
  207. Returns:
  208. Dict with the following structure:
  209. {
  210. "status": "success",
  211. "message": "Entity updated successfully" | "Entity merged successfully into 'target_name'",
  212. "data": {
  213. "entity_name": str, # Final entity name
  214. "description": str, # Entity description
  215. "entity_type": str, # Entity type
  216. "source_id": str, # Source chunk IDs
  217. ... # Other entity properties
  218. },
  219. "operation_summary": {
  220. "merged": bool, # Whether entity was merged into another
  221. "merge_status": str, # "success" | "failed" | "not_attempted"
  222. "merge_error": str | None, # Error message if merge failed
  223. "operation_status": str, # "success" | "partial_success" | "failure"
  224. "target_entity": str | None, # Target entity name if renaming/merging
  225. "final_entity": str, # Final entity name after operation
  226. "renamed": bool # Whether entity was renamed
  227. }
  228. }
  229. operation_status values explained:
  230. - "success": All operations completed successfully
  231. * For simple updates: entity properties updated
  232. * For renames: entity renamed successfully
  233. * For merges: non-name updates applied AND merge completed
  234. - "partial_success": Update succeeded but merge failed
  235. * Non-name property updates were applied successfully
  236. * Merge operation failed (entity not merged)
  237. * Original entity still exists with updated properties
  238. * Use merge_error for failure details
  239. - "failure": Operation failed completely
  240. * If merge_status == "failed": Merge attempted but both update and merge failed
  241. * If merge_status == "not_attempted": Regular update failed
  242. * No changes were applied to the entity
  243. merge_status values explained:
  244. - "success": Entity successfully merged into target entity
  245. - "failed": Merge operation was attempted but failed
  246. - "not_attempted": No merge was attempted (normal update/rename)
  247. Behavior when renaming to an existing entity:
  248. - If allow_merge=False: Raises ValueError with 400 status (default behavior)
  249. - If allow_merge=True: Automatically merges the source entity into the existing target entity,
  250. preserving all relationships and applying non-name updates first
  251. Example Request (simple update):
  252. POST /graph/entity/edit
  253. {
  254. "entity_name": "Tesla",
  255. "updated_data": {"description": "Updated description"},
  256. "allow_rename": false,
  257. "allow_merge": false
  258. }
  259. Example Response (simple update success):
  260. {
  261. "status": "success",
  262. "message": "Entity updated successfully",
  263. "data": { ... },
  264. "operation_summary": {
  265. "merged": false,
  266. "merge_status": "not_attempted",
  267. "merge_error": null,
  268. "operation_status": "success",
  269. "target_entity": null,
  270. "final_entity": "Tesla",
  271. "renamed": false
  272. }
  273. }
  274. Example Request (rename with auto-merge):
  275. POST /graph/entity/edit
  276. {
  277. "entity_name": "Elon Msk",
  278. "updated_data": {
  279. "entity_name": "Elon Musk",
  280. "description": "Corrected description"
  281. },
  282. "allow_rename": true,
  283. "allow_merge": true
  284. }
  285. Example Response (merge success):
  286. {
  287. "status": "success",
  288. "message": "Entity merged successfully into 'Elon Musk'",
  289. "data": { ... },
  290. "operation_summary": {
  291. "merged": true,
  292. "merge_status": "success",
  293. "merge_error": null,
  294. "operation_status": "success",
  295. "target_entity": "Elon Musk",
  296. "final_entity": "Elon Musk",
  297. "renamed": true
  298. }
  299. }
  300. Example Response (partial success - update succeeded but merge failed):
  301. {
  302. "status": "success",
  303. "message": "Entity updated successfully",
  304. "data": { ... }, # Data reflects updated "Elon Msk" entity
  305. "operation_summary": {
  306. "merged": false,
  307. "merge_status": "failed",
  308. "merge_error": "Target entity locked by another operation",
  309. "operation_status": "partial_success",
  310. "target_entity": "Elon Musk",
  311. "final_entity": "Elon Msk", # Original entity still exists
  312. "renamed": true
  313. }
  314. }
  315. """
  316. try:
  317. await check_pipeline_busy_or_raise(rag)
  318. result = await rag.aedit_entity(
  319. entity_name=request.entity_name,
  320. updated_data=request.updated_data,
  321. allow_rename=request.allow_rename,
  322. allow_merge=request.allow_merge,
  323. )
  324. # Extract operation_summary from result, with fallback for backward compatibility
  325. operation_summary = result.get(
  326. "operation_summary",
  327. {
  328. "merged": False,
  329. "merge_status": "not_attempted",
  330. "merge_error": None,
  331. "operation_status": "success",
  332. "target_entity": None,
  333. "final_entity": request.updated_data.get(
  334. "entity_name", request.entity_name
  335. ),
  336. "renamed": request.updated_data.get(
  337. "entity_name", request.entity_name
  338. )
  339. != request.entity_name,
  340. },
  341. )
  342. # Separate entity data from operation_summary for clean response
  343. entity_data = dict(result)
  344. entity_data.pop("operation_summary", None)
  345. # Generate appropriate response message based on merge status
  346. response_message = (
  347. f"Entity merged successfully into '{operation_summary['final_entity']}'"
  348. if operation_summary.get("merged")
  349. else "Entity updated successfully"
  350. )
  351. return {
  352. "status": "success",
  353. "message": response_message,
  354. "data": entity_data,
  355. "operation_summary": operation_summary,
  356. }
  357. except HTTPException:
  358. raise
  359. except ValueError as ve:
  360. logger.error(
  361. f"Validation error updating entity '{request.entity_name}': {str(ve)}"
  362. )
  363. raise HTTPException(status_code=400, detail=str(ve))
  364. except Exception as e:
  365. logger.error(f"Error updating entity '{request.entity_name}': {str(e)}")
  366. logger.error(traceback.format_exc())
  367. raise HTTPException(
  368. status_code=500, detail=f"Error updating entity: {str(e)}"
  369. )
  370. @router.post("/graph/relation/edit", dependencies=[Depends(combined_auth)])
  371. async def update_relation(request: RelationUpdateRequest):
  372. """Update a relation's properties in the knowledge graph
  373. Args:
  374. request (RelationUpdateRequest): Request containing source ID, target ID and updated data
  375. Returns:
  376. Dict: Updated relation information
  377. """
  378. try:
  379. await check_pipeline_busy_or_raise(rag)
  380. result = await rag.aedit_relation(
  381. source_entity=request.source_id,
  382. target_entity=request.target_id,
  383. updated_data=request.updated_data,
  384. )
  385. return {
  386. "status": "success",
  387. "message": "Relation updated successfully",
  388. "data": result,
  389. }
  390. except HTTPException:
  391. raise
  392. except ValueError as ve:
  393. logger.error(
  394. f"Validation error updating relation between '{request.source_id}' and '{request.target_id}': {str(ve)}"
  395. )
  396. raise HTTPException(status_code=400, detail=str(ve))
  397. except Exception as e:
  398. logger.error(
  399. f"Error updating relation between '{request.source_id}' and '{request.target_id}': {str(e)}"
  400. )
  401. logger.error(traceback.format_exc())
  402. raise HTTPException(
  403. status_code=500, detail=f"Error updating relation: {str(e)}"
  404. )
  405. @router.post("/graph/entity/create", dependencies=[Depends(combined_auth)])
  406. async def create_entity(request: EntityCreateRequest):
  407. """
  408. Create a new entity in the knowledge graph
  409. This endpoint creates a new entity node in the knowledge graph with the specified
  410. properties. The system automatically generates vector embeddings for the entity
  411. to enable semantic search and retrieval.
  412. Request Body:
  413. entity_name (str): Unique name identifier for the entity
  414. entity_data (dict): Entity properties including:
  415. - description (str): Textual description of the entity
  416. - entity_type (str): Category/type of the entity (e.g., PERSON, ORGANIZATION, LOCATION)
  417. - source_id (str): Related chunk_id from which the description originates
  418. - Additional custom properties as needed
  419. Response Schema:
  420. {
  421. "status": "success",
  422. "message": "Entity 'Tesla' created successfully",
  423. "data": {
  424. "entity_name": "Tesla",
  425. "description": "Electric vehicle manufacturer",
  426. "entity_type": "ORGANIZATION",
  427. "source_id": "chunk-123<SEP>chunk-456"
  428. ... (other entity properties)
  429. }
  430. }
  431. HTTP Status Codes:
  432. 200: Entity created successfully
  433. 400: Invalid request (e.g., missing required fields, duplicate entity)
  434. 500: Internal server error
  435. Example Request:
  436. POST /graph/entity/create
  437. {
  438. "entity_name": "Tesla",
  439. "entity_data": {
  440. "description": "Electric vehicle manufacturer",
  441. "entity_type": "ORGANIZATION"
  442. }
  443. }
  444. """
  445. try:
  446. await check_pipeline_busy_or_raise(rag)
  447. # Use the proper acreate_entity method which handles:
  448. # - Graph lock for concurrency
  449. # - Vector embedding creation in entities_vdb
  450. # - Metadata population and defaults
  451. # - Index consistency via _edit_entity_done
  452. result = await rag.acreate_entity(
  453. entity_name=request.entity_name,
  454. entity_data=request.entity_data,
  455. )
  456. return {
  457. "status": "success",
  458. "message": f"Entity '{request.entity_name}' created successfully",
  459. "data": result,
  460. }
  461. except HTTPException:
  462. raise
  463. except ValueError as ve:
  464. logger.error(
  465. f"Validation error creating entity '{request.entity_name}': {str(ve)}"
  466. )
  467. raise HTTPException(status_code=400, detail=str(ve))
  468. except Exception as e:
  469. logger.error(f"Error creating entity '{request.entity_name}': {str(e)}")
  470. logger.error(traceback.format_exc())
  471. raise HTTPException(
  472. status_code=500, detail=f"Error creating entity: {str(e)}"
  473. )
  474. @router.post("/graph/relation/create", dependencies=[Depends(combined_auth)])
  475. async def create_relation(request: RelationCreateRequest):
  476. """
  477. Create a new relationship between two entities in the knowledge graph
  478. This endpoint establishes an undirected relationship between two existing entities.
  479. The provided source/target order is accepted for convenience, but the backend
  480. stored edge is undirected and may be returned with the entities swapped.
  481. Both entities must already exist in the knowledge graph. The system automatically
  482. generates vector embeddings for the relationship to enable semantic search and graph traversal.
  483. Prerequisites:
  484. - Both source_entity and target_entity must exist in the knowledge graph
  485. - Use /graph/entity/create to create entities first if they don't exist
  486. Request Body:
  487. source_entity (str): Name of the source entity (relationship origin)
  488. target_entity (str): Name of the target entity (relationship destination)
  489. relation_data (dict): Relationship properties including:
  490. - description (str): Textual description of the relationship
  491. - keywords (str): Comma-separated keywords describing the relationship type
  492. - source_id (str): Related chunk_id from which the description originates
  493. - weight (float): Relationship strength/importance (default: 1.0)
  494. - Additional custom properties as needed
  495. Response Schema:
  496. {
  497. "status": "success",
  498. "message": "Relation created successfully between 'Elon Musk' and 'Tesla'",
  499. "data": {
  500. "src_id": "Elon Musk",
  501. "tgt_id": "Tesla",
  502. "description": "Elon Musk is the CEO of Tesla",
  503. "keywords": "CEO, founder",
  504. "source_id": "chunk-123<SEP>chunk-456"
  505. "weight": 1.0,
  506. ... (other relationship properties)
  507. }
  508. }
  509. HTTP Status Codes:
  510. 200: Relationship created successfully
  511. 400: Invalid request (e.g., missing entities, invalid data, duplicate relationship)
  512. 500: Internal server error
  513. Example Request:
  514. POST /graph/relation/create
  515. {
  516. "source_entity": "Elon Musk",
  517. "target_entity": "Tesla",
  518. "relation_data": {
  519. "description": "Elon Musk is the CEO of Tesla",
  520. "keywords": "CEO, founder",
  521. "weight": 1.0
  522. }
  523. }
  524. """
  525. try:
  526. await check_pipeline_busy_or_raise(rag)
  527. # Use the proper acreate_relation method which handles:
  528. # - Graph lock for concurrency
  529. # - Entity existence validation
  530. # - Duplicate relation checks
  531. # - Vector embedding creation in relationships_vdb
  532. # - Index consistency via _edit_relation_done
  533. result = await rag.acreate_relation(
  534. source_entity=request.source_entity,
  535. target_entity=request.target_entity,
  536. relation_data=request.relation_data,
  537. )
  538. return {
  539. "status": "success",
  540. "message": f"Relation created successfully between '{request.source_entity}' and '{request.target_entity}'",
  541. "data": result,
  542. }
  543. except HTTPException:
  544. raise
  545. except ValueError as ve:
  546. logger.error(
  547. f"Validation error creating relation between '{request.source_entity}' and '{request.target_entity}': {str(ve)}"
  548. )
  549. raise HTTPException(status_code=400, detail=str(ve))
  550. except Exception as e:
  551. logger.error(
  552. f"Error creating relation between '{request.source_entity}' and '{request.target_entity}': {str(e)}"
  553. )
  554. logger.error(traceback.format_exc())
  555. raise HTTPException(
  556. status_code=500, detail=f"Error creating relation: {str(e)}"
  557. )
  558. @router.post("/graph/entities/merge", dependencies=[Depends(combined_auth)])
  559. async def merge_entities(request: EntityMergeRequest):
  560. """
  561. Merge multiple entities into a single entity, preserving all relationships
  562. This endpoint consolidates duplicate or misspelled entities while preserving the entire
  563. graph structure. It's particularly useful for cleaning up knowledge graphs after document
  564. processing or correcting entity name variations.
  565. What the Merge Operation Does:
  566. 1. Deletes the specified source entities from the knowledge graph
  567. 2. Transfers all relationships from source entities to the target entity
  568. 3. Intelligently merges duplicate relationships (if multiple sources have the same relationship)
  569. 4. Updates vector embeddings for accurate retrieval and search
  570. 5. Preserves the complete graph structure and connectivity
  571. 6. Maintains relationship properties and metadata
  572. Use Cases:
  573. - Fixing spelling errors in entity names (e.g., "Elon Msk" -> "Elon Musk")
  574. - Consolidating duplicate entities discovered after document processing
  575. - Merging name variations (e.g., "NY", "New York", "New York City")
  576. - Cleaning up the knowledge graph for better query performance
  577. - Standardizing entity names across the knowledge base
  578. Request Body:
  579. entities_to_change (list[str]): List of entity names to be merged and deleted
  580. entity_to_change_into (str): Target entity that will receive all relationships
  581. Response Schema:
  582. {
  583. "status": "success",
  584. "message": "Successfully merged 2 entities into 'Elon Musk'",
  585. "data": {
  586. "merged_entity": "Elon Musk",
  587. "deleted_entities": ["Elon Msk", "Ellon Musk"],
  588. "relationships_transferred": 15,
  589. ... (merge operation details)
  590. }
  591. }
  592. HTTP Status Codes:
  593. 200: Entities merged successfully
  594. 400: Invalid request (e.g., empty entity list, target entity doesn't exist)
  595. 500: Internal server error
  596. Example Request:
  597. POST /graph/entities/merge
  598. {
  599. "entities_to_change": ["Elon Msk", "Ellon Musk"],
  600. "entity_to_change_into": "Elon Musk"
  601. }
  602. Note:
  603. - The target entity (entity_to_change_into) must exist in the knowledge graph
  604. - Source entities will be permanently deleted after the merge
  605. - This operation cannot be undone, so verify entity names before merging
  606. """
  607. try:
  608. await check_pipeline_busy_or_raise(rag)
  609. result = await rag.amerge_entities(
  610. source_entities=request.entities_to_change,
  611. target_entity=request.entity_to_change_into,
  612. )
  613. return {
  614. "status": "success",
  615. "message": f"Successfully merged {len(request.entities_to_change)} entities into '{request.entity_to_change_into}'",
  616. "data": result,
  617. }
  618. except HTTPException:
  619. raise
  620. except ValueError as ve:
  621. logger.error(
  622. f"Validation error merging entities {request.entities_to_change} into '{request.entity_to_change_into}': {str(ve)}"
  623. )
  624. raise HTTPException(status_code=400, detail=str(ve))
  625. except Exception as e:
  626. logger.error(
  627. f"Error merging entities {request.entities_to_change} into '{request.entity_to_change_into}': {str(e)}"
  628. )
  629. logger.error(traceback.format_exc())
  630. raise HTTPException(
  631. status_code=500, detail=f"Error merging entities: {str(e)}"
  632. )
  633. return router