Skip to content

Commit d2f6cbc

Browse files
authored
feat: refine node merge (#1092)
* feat: update readme kg * feat: refine node merge * feat: refine node merge * feat: refine node merge
1 parent 2b4f2bf commit d2f6cbc

16 files changed

Lines changed: 998 additions & 100 deletions

File tree

aperag/api/components/schemas/graph.yaml

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -195,32 +195,22 @@ targetEntityDataResponse:
195195
nodeMergeRequest:
196196
type: object
197197
description: |
198-
Request to merge multiple graph nodes.
199-
200-
You can provide either:
201-
- entity_ids: List of entity IDs to merge directly
202-
- suggestion_id: ID of a suggestion to merge
203-
204-
If both are provided, suggestion_id takes precedence and entity_ids will be ignored.
198+
Request to merge multiple graph nodes directly using entity IDs.
205199
properties:
206200
entity_ids:
207201
type: array
208202
items:
209203
type: string
210-
description: List of entity IDs to merge (supports 1 or more entities). Ignored if suggestion_id is also provided.
204+
description: List of entity IDs to merge directly
211205
example: ["墨香居", "书店", "旧书店"]
212206
minItems: 1
213-
nullable: true
214-
suggestion_id:
215-
type: string
216-
description: Single suggestion ID to merge. If provided, takes precedence over entity_ids.
217-
example: "msug123"
218-
nullable: true
219207
target_entity_data:
220208
$ref: '#/targetEntityDataRequest'
209+
required:
210+
- entity_ids
221211
additionalProperties: false
222212
example:
223-
suggestion_id: "msug123"
213+
entity_ids: ["墨香居", "书店", "旧书店"]
224214

225215
nodeMergeResponse:
226216
type: object
@@ -506,4 +496,54 @@ mergeSuggestionsResponse:
506496
- pending_count
507497
- accepted_count
508498
- rejected_count
509-
- expired_count
499+
- expired_count
500+
501+
suggestionActionRequest:
502+
type: object
503+
description: Request to take action on a merge suggestion
504+
properties:
505+
action:
506+
type: string
507+
enum: ["accept", "reject"]
508+
description: Action to take on the suggestion (case-insensitive, e.g., 'Accept', 'REJECT', 'accept')
509+
example: "accept"
510+
target_entity_data:
511+
$ref: '#/targetEntityDataRequest'
512+
description: Optional override for target entity data (only used when action is 'accept')
513+
required:
514+
- action
515+
additionalProperties: false
516+
example:
517+
action: "accept"
518+
519+
suggestionActionResponse:
520+
type: object
521+
description: Response containing suggestion action results
522+
properties:
523+
status:
524+
type: string
525+
description: Status of the action operation
526+
example: "success"
527+
enum: ["success", "error"]
528+
message:
529+
type: string
530+
description: Detailed message about the action operation
531+
example: "Suggestion msug123 has been accepted and merge completed"
532+
suggestion_id:
533+
type: string
534+
description: The suggestion ID that was processed
535+
example: "msug123"
536+
action:
537+
type: string
538+
enum: ["accept", "reject"]
539+
description: The action that was performed (normalized to lowercase)
540+
example: "accept"
541+
merge_result:
542+
$ref: '#/nodeMergeResponse'
543+
description: Merge operation result (only present when action is 'accept')
544+
nullable: true
545+
required:
546+
- status
547+
- message
548+
- suggestion_id
549+
- action

aperag/api/openapi.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ paths:
6767
$ref: './paths/collections.yaml#/graph_nodes_merge'
6868
/collections/{collection_id}/graphs/merge-suggestions:
6969
$ref: './paths/collections.yaml#/graph_merge_suggestions'
70+
/collections/{collection_id}/graphs/merge-suggestions/{suggestion_id}/action:
71+
$ref: './paths/collections.yaml#/graph_suggestion_action'
7072

7173
# apikeys
7274
/apikeys:

aperag/api/paths/collections.yaml

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,129 @@ graph_merge_suggestions:
753753
schema:
754754
$ref: '../components/schemas/common.yaml#/failResponse'
755755

756+
graph_suggestion_action:
757+
post:
758+
summary: Accept or reject a merge suggestion
759+
description: |
760+
Take action on a specific merge suggestion.
761+
762+
Actions:
763+
- accept: Accept the suggestion and perform the merge operation
764+
- reject: Reject the suggestion and mark it as rejected
765+
766+
When accepting, the system will:
767+
1. Update suggestion status to ACCEPTED
768+
2. Perform the actual node merge using suggested entity IDs
769+
3. Mark related suggestions involving the same entities as EXPIRED
770+
771+
When rejecting, the system will:
772+
1. Update suggestion status to REJECTED
773+
2. No merge operation is performed
774+
775+
The target entity data can be optionally overridden when accepting a suggestion.
776+
tags:
777+
- graph
778+
security:
779+
- BearerAuth: []
780+
parameters:
781+
- name: collection_id
782+
in: path
783+
required: true
784+
schema:
785+
type: string
786+
description: Collection ID
787+
- name: suggestion_id
788+
in: path
789+
required: true
790+
schema:
791+
type: string
792+
description: Suggestion ID
793+
requestBody:
794+
required: true
795+
content:
796+
application/json:
797+
schema:
798+
$ref: '../components/schemas/graph.yaml#/suggestionActionRequest'
799+
examples:
800+
accept:
801+
summary: Accept suggestion
802+
description: Accept the suggestion and perform merge
803+
value:
804+
action: "accept"
805+
reject:
806+
summary: Reject suggestion
807+
description: Reject the suggestion without merging
808+
value:
809+
action: "reject"
810+
accept_with_override:
811+
summary: Accept with custom target data
812+
description: Accept suggestion but override target entity data
813+
value:
814+
action: "accept"
815+
target_entity_data:
816+
entity_name: "Custom Name"
817+
description: "Custom description for merged entity"
818+
responses:
819+
'200':
820+
description: Action completed successfully
821+
content:
822+
application/json:
823+
schema:
824+
$ref: '../components/schemas/graph.yaml#/suggestionActionResponse'
825+
examples:
826+
accept_success:
827+
summary: Accept action successful
828+
description: Suggestion accepted and merge completed
829+
value:
830+
status: "success"
831+
message: "Suggestion msug123 has been accepted and merge completed"
832+
suggestion_id: "msug123"
833+
action: "accept"
834+
merge_result:
835+
status: "success"
836+
message: "Successfully merged 2 entities into 墨香居"
837+
entity_ids: ["墨香居", "书店"]
838+
target_entity_data:
839+
entity_name: "墨香居"
840+
entity_type: "ORGANIZATION"
841+
description: "墨香居是这条老巷子里唯一的旧书店,经营着各种书籍"
842+
source_entities: ["书店"]
843+
redirected_edges: 5
844+
merged_description_length: 45
845+
reject_success:
846+
summary: Reject action successful
847+
description: Suggestion rejected successfully
848+
value:
849+
status: "success"
850+
message: "Suggestion msug123 has been rejected"
851+
suggestion_id: "msug123"
852+
action: "reject"
853+
merge_result: null
854+
'400':
855+
description: Bad request - invalid action or suggestion already processed
856+
content:
857+
application/json:
858+
schema:
859+
$ref: '../components/schemas/common.yaml#/failResponse'
860+
'401':
861+
description: Unauthorized
862+
content:
863+
application/json:
864+
schema:
865+
$ref: '../components/schemas/common.yaml#/failResponse'
866+
'404':
867+
description: Collection or suggestion not found
868+
content:
869+
application/json:
870+
schema:
871+
$ref: '../components/schemas/common.yaml#/failResponse'
872+
'500':
873+
description: Internal server error
874+
content:
875+
application/json:
876+
schema:
877+
$ref: '../components/schemas/common.yaml#/failResponse'
878+
756879
test_mineru_token:
757880
post:
758881
summary: Test MinerU API Token

aperag/db/repositories/merge_suggestion.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from datetime import datetime, timedelta
1616
from typing import List, Optional
1717

18-
from sqlalchemy import and_, select, update
18+
from sqlalchemy import and_, select, text, update
1919

2020
from aperag.db.models import MergeSuggestion, MergeSuggestionStatus
2121
from aperag.db.repositories.base import AsyncRepositoryProtocol
@@ -77,11 +77,11 @@ async def _query(session):
7777
and_(
7878
MergeSuggestion.collection_id == collection_id,
7979
MergeSuggestion.gmt_deleted.is_(None),
80-
MergeSuggestion.entity_ids.overlap(entity_ids), # PostgreSQL array overlap
80+
text("entity_ids && :entity_ids"), # PostgreSQL array overlap operator
8181
)
8282
)
8383

84-
result = await session.execute(stmt)
84+
result = await session.execute(stmt, {"entity_ids": entity_ids})
8585
return result.scalars().all()
8686

8787
return await self._execute_query(_query)

aperag/graph/lightrag/lightrag.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,11 +1356,14 @@ async def amerge_nodes(
13561356

13571357
# Apply target entity data overrides (from target_entity_data parameter)
13581358
user_provided_description = None
1359-
for key, value in target_entity_data.items():
1360-
if key != "entity_name": # Don't override entity_name in the data
1361-
merged_entity_data[key] = value
1362-
if key == "description" and value: # Track if user provided description
1363-
user_provided_description = value
1359+
if target_entity_data: # Only iterate if target_entity_data is not None
1360+
for key, value in target_entity_data.items():
1361+
if key != "entity_name": # Don't override entity_name in the data
1362+
merged_entity_data[key] = value
1363+
if (
1364+
key == "description" and value is not None
1365+
): # Track if user provided description (including empty strings)
1366+
user_provided_description = value
13641367

13651368
merged_entity_data["entity_id"] = target_entity_name
13661369

aperag/migration/versions/20250711123338-694591d5df94.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""empty message
22
33
Revision ID: 694591d5df94
4-
Revises: c09204152091
4+
Revises: add_index_audit_resource
55
Create Date: 2025-07-11 12:33:38.799759
66
77
"""

aperag/schema/view_models.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
# generated by datamodel-codegen:
1616
# filename: openapi.merged.yaml
17-
# timestamp: 2025-07-11T05:56:53+00:00
17+
# timestamp: 2025-07-11T06:38:38+00:00
1818

1919
from __future__ import annotations
2020

@@ -859,30 +859,19 @@ class TargetEntityDataRequest(BaseModel):
859859

860860
class NodeMergeRequest(BaseModel):
861861
"""
862-
Request to merge multiple graph nodes.
863-
864-
You can provide either:
865-
- entity_ids: List of entity IDs to merge directly
866-
- suggestion_id: ID of a suggestion to merge
867-
868-
If both are provided, suggestion_id takes precedence and entity_ids will be ignored.
862+
Request to merge multiple graph nodes directly using entity IDs.
869863
870864
"""
871865

872866
class Config:
873867
extra = Extra.forbid
874868

875-
entity_ids: Optional[list[str]] = Field(
876-
None,
877-
description='List of entity IDs to merge (supports 1 or more entities). Ignored if suggestion_id is also provided.',
869+
entity_ids: list[str] = Field(
870+
...,
871+
description='List of entity IDs to merge directly',
878872
example=['墨香居', '书店', '旧书店'],
879873
min_items=1,
880874
)
881-
suggestion_id: Optional[str] = Field(
882-
None,
883-
description='Single suggestion ID to merge. If provided, takes precedence over entity_ids.',
884-
example='msug123',
885-
)
886875
target_entity_data: Optional[TargetEntityDataRequest] = None
887876

888877

@@ -1058,6 +1047,52 @@ class MergeSuggestionsResponse(BaseModel):
10581047
)
10591048

10601049

1050+
class SuggestionActionRequest(BaseModel):
1051+
"""
1052+
Request to take action on a merge suggestion
1053+
"""
1054+
1055+
class Config:
1056+
extra = Extra.forbid
1057+
1058+
action: Literal['accept', 'reject'] = Field(
1059+
...,
1060+
description="Action to take on the suggestion (case-insensitive, e.g., 'Accept', 'REJECT', 'accept')",
1061+
example='accept',
1062+
)
1063+
target_entity_data: Optional[TargetEntityDataRequest] = Field(
1064+
None,
1065+
description="Optional override for target entity data (only used when action is 'accept')",
1066+
)
1067+
1068+
1069+
class SuggestionActionResponse(BaseModel):
1070+
"""
1071+
Response containing suggestion action results
1072+
"""
1073+
1074+
status: Literal['success', 'error'] = Field(
1075+
..., description='Status of the action operation', example='success'
1076+
)
1077+
message: str = Field(
1078+
...,
1079+
description='Detailed message about the action operation',
1080+
example='Suggestion msug123 has been accepted and merge completed',
1081+
)
1082+
suggestion_id: str = Field(
1083+
..., description='The suggestion ID that was processed', example='msug123'
1084+
)
1085+
action: Literal['accept', 'reject'] = Field(
1086+
...,
1087+
description='The action that was performed (normalized to lowercase)',
1088+
example='accept',
1089+
)
1090+
merge_result: Optional[NodeMergeResponse] = Field(
1091+
None,
1092+
description="Merge operation result (only present when action is 'accept')",
1093+
)
1094+
1095+
10611096
class ApiKey(BaseModel):
10621097
id: Optional[str] = None
10631098
key: Optional[str] = None

0 commit comments

Comments
 (0)