Files
AI/참고/ontocast-main/ontocast/tool/graph_diff.py
2026-05-12 19:40:31 +09:00

376 lines
12 KiB
Python

"""Graph diff generation and application system.
This module provides functionality for generating and applying diffs between
graph versions, enabling incremental updates and efficient context passing.
"""
import logging
from datetime import datetime
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, Field
from ontocast.onto.rdfgraph import RDFGraph
logger = logging.getLogger(__name__)
class DiffOperation(StrEnum):
"""Enumeration of diff operations."""
ADD = "add"
REMOVE = "remove"
MODIFY = "modify"
UNCHANGED = "unchanged"
class TripleDiff(BaseModel):
"""Represents a diff for a single triple."""
subject: str = Field(description="Subject of the triple")
predicate: str = Field(description="Predicate of the triple")
object: str = Field(description="Object of the triple")
operation: DiffOperation = Field(description="Operation performed on this triple")
old_value: str | None = Field(
default=None, description="Old value for modifications"
)
new_value: str | None = Field(
default=None, description="New value for modifications"
)
metadata: dict[str, Any] = Field(
default_factory=dict, description="Additional metadata"
)
class GraphDiff(BaseModel):
"""Represents differences between two graph versions."""
# Diff metadata
diff_id: str = Field(description="Unique identifier for this diff")
source_version_id: str = Field(description="ID of the source version")
target_version_id: str = Field(description="ID of the target version")
created_at: datetime = Field(
default_factory=datetime.now, description="When this diff was created"
)
# Diff content
triple_diffs: list[TripleDiff] = Field(
default_factory=list, description="List of triple differences"
)
# Summary statistics
added_triples: int = Field(default=0, description="Number of added triples")
removed_triples: int = Field(default=0, description="Number of removed triples")
modified_triples: int = Field(default=0, description="Number of modified triples")
unchanged_triples: int = Field(default=0, description="Number of unchanged triples")
# Context information
context_metadata: dict[str, Any] = Field(
default_factory=dict, description="Context metadata for this diff"
)
def get_summary(self) -> str:
"""Get a summary of this diff.
Returns:
str: Human-readable summary of the diff
"""
return f"""
Graph Diff Summary:
- Diff ID: {self.diff_id}
- Source: {self.source_version_id} → Target: {self.target_version_id}
- Created: {self.created_at.isoformat()}
- Added: {self.added_triples} triples
- Removed: {self.removed_triples} triples
- Modified: {self.modified_triples} triples
- Unchanged: {self.unchanged_triples} triples
- Total changes: {self.added_triples + self.removed_triples + self.modified_triples}
"""
def get_sparql_operations(self) -> list[str]:
"""Get SPARQL operations for applying this diff.
Returns:
list[str]: List of SPARQL queries to apply the diff
"""
operations = []
for triple_diff in self.triple_diffs:
if triple_diff.operation == DiffOperation.ADD:
# Generate INSERT operation
sparql = f"INSERT DATA {{ {triple_diff.subject} {triple_diff.predicate} {triple_diff.object} . }}"
operations.append(sparql)
elif triple_diff.operation == DiffOperation.REMOVE:
# Generate DELETE operation
sparql = f"DELETE DATA {{ {triple_diff.subject} {triple_diff.predicate} {triple_diff.object} . }}"
operations.append(sparql)
elif triple_diff.operation == DiffOperation.MODIFY:
# Generate DELETE + INSERT for modifications
if triple_diff.old_value:
delete_sparql = f"DELETE DATA {{ {triple_diff.subject} {triple_diff.predicate} {triple_diff.old_value} . }}"
operations.append(delete_sparql)
if triple_diff.new_value:
insert_sparql = f"INSERT DATA {{ {triple_diff.subject} {triple_diff.predicate} {triple_diff.new_value} . }}"
operations.append(insert_sparql)
return operations
def is_empty(self) -> bool:
"""Check if this diff is empty (no changes).
Returns:
bool: True if no changes, False otherwise
"""
return (
self.added_triples == 0
and self.removed_triples == 0
and self.modified_triples == 0
)
def get_changed_subjects(self) -> set[str]:
"""Get all subjects that have changes.
Returns:
set[str]: Set of subject URIs that have changes
"""
return {
triple_diff.subject
for triple_diff in self.triple_diffs
if triple_diff.operation != DiffOperation.UNCHANGED
}
def get_changed_predicates(self) -> set[str]:
"""Get all predicates that have changes.
Returns:
set[str]: Set of predicate URIs that have changes
"""
return {
triple_diff.predicate
for triple_diff in self.triple_diffs
if triple_diff.operation != DiffOperation.UNCHANGED
}
class DiffTool:
"""Tool for generating and applying graph diffs."""
def __init__(self):
"""Initialize the diff tool."""
self.logger = logging.getLogger(__name__)
def generate_diff(
self,
source_graph: RDFGraph,
target_graph: RDFGraph,
source_version_id: str,
target_version_id: str,
context_metadata: dict[str, Any] | None = None,
) -> GraphDiff:
"""Generate a diff between two graphs.
Args:
source_graph: The source graph to compare from
target_graph: The target graph to compare to
source_version_id: ID of the source version
target_version_id: ID of the target version
context_metadata: Optional context metadata
Returns:
GraphDiff: The generated diff
"""
self.logger.info(
f"Generating diff from {source_version_id} to {target_version_id}"
)
# Get triples from both graphs
source_triples = self._get_triples_set(source_graph)
target_triples = self._get_triples_set(target_graph)
# Find differences
triple_diffs = []
added_triples = 0
removed_triples = 0
modified_triples = 0
unchanged_triples = 0
# Find added triples
for triple in target_triples - source_triples:
triple_diff = TripleDiff(
subject=triple[0],
predicate=triple[1],
object=triple[2],
operation=DiffOperation.ADD,
)
triple_diffs.append(triple_diff)
added_triples += 1
# Find removed triples
for triple in source_triples - target_triples:
triple_diff = TripleDiff(
subject=triple[0],
predicate=triple[1],
object=triple[2],
operation=DiffOperation.REMOVE,
)
triple_diffs.append(triple_diff)
removed_triples += 1
# Find unchanged triples
for triple in source_triples & target_triples:
triple_diff = TripleDiff(
subject=triple[0],
predicate=triple[1],
object=triple[2],
operation=DiffOperation.UNCHANGED,
)
triple_diffs.append(triple_diff)
unchanged_triples += 1
# Create diff
diff = GraphDiff(
diff_id=f"diff_{source_version_id}_{target_version_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
source_version_id=source_version_id,
target_version_id=target_version_id,
triple_diffs=triple_diffs,
added_triples=added_triples,
removed_triples=removed_triples,
modified_triples=modified_triples,
unchanged_triples=unchanged_triples,
context_metadata=context_metadata or {},
)
self.logger.info(
f"Generated diff with {added_triples} additions, {removed_triples} removals, {modified_triples} modifications"
)
return diff
def apply_diff(self, graph: RDFGraph, diff: GraphDiff) -> RDFGraph:
"""Apply a diff to a graph.
Args:
graph: The graph to apply the diff to
diff: The diff to apply
Returns:
RDFGraph: The updated graph
"""
self.logger.info(f"Applying diff {diff.diff_id} to graph")
# Create a copy of the graph
updated_graph = RDFGraph()
updated_graph += graph
# Apply each triple diff
for triple_diff in diff.triple_diffs:
if triple_diff.operation == DiffOperation.ADD:
# Add the triple
updated_graph.add_triple(
triple_diff.subject,
triple_diff.predicate,
triple_diff.object,
)
elif triple_diff.operation == DiffOperation.REMOVE:
# Remove the triple
updated_graph.remove_triple(
triple_diff.subject,
triple_diff.predicate,
triple_diff.object,
)
elif triple_diff.operation == DiffOperation.MODIFY:
# Remove old, add new
if triple_diff.old_value:
updated_graph.remove_triple(
triple_diff.subject,
triple_diff.predicate,
triple_diff.old_value,
)
if triple_diff.new_value:
updated_graph.add_triple(
triple_diff.subject,
triple_diff.predicate,
triple_diff.new_value,
)
self.logger.info(
f"Applied diff to graph, new triple count: {len(updated_graph)}"
)
return updated_graph
def _get_triples_set(self, graph: RDFGraph) -> set[tuple[str, str, str]]:
"""Get a set of triples from a graph.
Args:
graph: The graph to extract triples from
Returns:
set[tuple[str, str, str]]: Set of (subject, predicate, object) tuples
"""
triples = set()
for triple in graph:
triples.add((str(triple[0]), str(triple[1]), str(triple[2])))
return triples
def get_diff_summary(self, diff: GraphDiff) -> str:
"""Get a human-readable summary of a diff.
Args:
diff: The diff to summarize
Returns:
str: Human-readable summary
"""
return f"""
Diff Summary:
- ID: {diff.diff_id}
- Source: {diff.source_version_id} → Target: {diff.target_version_id}
- Created: {diff.created_at.isoformat()}
- Changes: {diff.added_triples} added, {diff.removed_triples} removed, {diff.modified_triples} modified
- Total triples: {len(diff.triple_diffs)}
- Changed subjects: {len(diff.get_changed_subjects())}
- Changed predicates: {len(diff.get_changed_predicates())}
"""
def merge_diffs(self, diffs: list[GraphDiff]) -> GraphDiff:
"""Merge multiple diffs into a single diff.
Args:
diffs: List of diffs to merge
Returns:
GraphDiff: Merged diff
"""
if not diffs:
raise ValueError("Cannot merge empty list of diffs")
if len(diffs) == 1:
return diffs[0]
# Start with the first diff
merged_diff = diffs[0]
# Merge each subsequent diff
for diff in diffs[1:]:
# Combine triple diffs
merged_diff.triple_diffs.extend(diff.triple_diffs)
# Update statistics
merged_diff.added_triples += diff.added_triples
merged_diff.removed_triples += diff.removed_triples
merged_diff.modified_triples += diff.modified_triples
merged_diff.unchanged_triples += diff.unchanged_triples
# Update target version
merged_diff.target_version_id = diff.target_version_id
# Update diff ID
merged_diff.diff_id = f"merged_{merged_diff.source_version_id}_{merged_diff.target_version_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
self.logger.info(f"Merged {len(diffs)} diffs into single diff")
return merged_diff