Skip to content

Graph

Graph is the primary aggregate root of the domain. It owns collections of Node and Edge instances and exposes structural query helpers. Unlike Node and Edge, Graph is mutable — adding or removing elements changes its internal state and increments its version.

Lifecycle

stateDiagram-v2
    [*] --> Empty : Graph.create()
    Empty --> HasNodes : add_node()
    HasNodes --> HasNodes : add_node() / update_node() / remove_node()
    HasNodes --> HasEdges : add_edge()
    HasEdges --> HasEdges : add_edge() / remove_edge()
    HasEdges --> HasNodes : remove_edge()
    HasNodes --> Empty : remove_node() (last)

Creating a Graph

Prefer using GraphEngine.create_graph() or GraphService.create_graph() rather than calling Graph.create() directly — the higher-level methods also persist the record and register the graph in cache.

from knowledge_platform.core.graph import Graph
from knowledge_platform.core.identifiers import new_workspace_id

ws_id = new_workspace_id()
graph = Graph.create(ws_id, type_name="outline", name="My Document")
print(graph.node_count())  # 0
print(graph.version)       # 1

Node Operations

from knowledge_platform.core.node import Node

node = Node.create(graph.id, "OutlineItem", {"title": "Intro"})
graph.add_node(node)
print(graph.node_count())  # 1

# Retrieve a specific node
same_node = graph.get_node(node.id)

# Iterate all nodes of a specific type
for n in graph.nodes(type_name="OutlineItem"):
    print(n.attributes["title"])

# Remove cascades incident edges automatically
graph.remove_node(node.id)

Edge Operations

from knowledge_platform.core.edge import Edge

edge = Edge.create(graph.id, parent.id, child.id, "ParentOf")
graph.add_edge(edge)

# Query edges by type
for e in graph.edges(type_name="ParentOf"):
    print(f"{e.source_id} -> {e.target_id}")

# Adjacency helpers
for e in graph.outgoing_edges(parent.id, type_name="ParentOf"):
    print(e.target_id)

for e in graph.incoming_edges(child.id):
    print(e.source_id)

Error Handling

Method Raises Condition
add_node(node) ValueError Node belongs to a different graph or already exists
update_node(node) KeyError Node not found
remove_node(node_id) KeyError Node not found
get_node(node_id) KeyError Node not found
add_edge(edge) ValueError Edge belongs to a different graph or already exists
add_edge(edge) KeyError Source or target node not found
remove_edge(edge_id) KeyError Edge not found
get_edge(edge_id) KeyError Edge not found

API Reference

knowledge_platform.core.graph.Graph dataclass

An in-memory graph governed by a single graph type.

:class:Graph is the primary aggregate root of the domain. It owns collections of :class:~knowledge_platform.core.node.Node and :class:~knowledge_platform.core.edge.Edge instances and exposes structural query helpers.

Attributes:

Name Type Description
id GraphId

Unique identifier.

workspace_id WorkspaceId

Owning workspace.

type_name str

The graph type controlling semantics.

name str

Human-readable display name.

version int

Incremented on each structural change.

created_at datetime

UTC creation timestamp.

updated_at datetime

UTC last-modified timestamp.

Source code in src/knowledge_platform/core/graph.py
@dataclass
class Graph:
    """An in-memory graph governed by a single graph type.

    :class:`Graph` is the primary aggregate root of the domain.  It owns
    collections of :class:`~knowledge_platform.core.node.Node` and
    :class:`~knowledge_platform.core.edge.Edge` instances and exposes
    structural query helpers.

    Attributes:
        id: Unique identifier.
        workspace_id: Owning workspace.
        type_name: The graph type controlling semantics.
        name: Human-readable display name.
        version: Incremented on each structural change.
        created_at: UTC creation timestamp.
        updated_at: UTC last-modified timestamp.
    """

    id: GraphId
    workspace_id: WorkspaceId
    type_name: str
    name: str = ""
    version: int = 1
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    # Internal storage – keyed by ID for O(1) lookup.
    _nodes: dict[NodeId, Node] = field(default_factory=dict, repr=False)
    _edges: dict[EdgeId, Edge] = field(default_factory=dict, repr=False)

    @classmethod
    def create(cls, workspace_id: WorkspaceId, type_name: str, name: str = "") -> "Graph":
        """Create a new empty graph.

        Args:
            workspace_id: Owning workspace.
            type_name: Semantic graph type name.
            name: Optional display name.

        Returns:
            Empty :class:`Graph`.
        """
        now = datetime.now(timezone.utc)
        return cls(
            id=new_graph_id(),
            workspace_id=workspace_id,
            type_name=type_name,
            name=name,
            version=1,
            created_at=now,
            updated_at=now,
        )

    # ------------------------------------------------------------------
    # Node mutations
    # ------------------------------------------------------------------

    def add_node(self, node: Node) -> None:
        """Insert *node* into this graph.

        Args:
            node: Must have :attr:`~Node.graph_id` matching this graph.

        Raises:
            ValueError: If the node belongs to a different graph or already exists.
        """
        if node.graph_id != self.id:
            raise ValueError(f"Node graph_id {node.graph_id!r} != {self.id!r}")
        if node.id in self._nodes:
            raise ValueError(f"Node {node.id!r} already exists in graph {self.id!r}")
        self._nodes[node.id] = node
        self._touch()

    def update_node(self, node: Node) -> None:
        """Replace an existing node with an updated version.

        Args:
            node: Updated node (same ID, higher version).

        Raises:
            KeyError: If the node does not exist in this graph.
        """
        if node.id not in self._nodes:
            raise KeyError(f"Node {node.id!r} not found in graph {self.id!r}")
        self._nodes[node.id] = node
        self._touch()

    def remove_node(self, node_id: NodeId) -> None:
        """Delete a node and all its incident edges.

        Args:
            node_id: ID of the node to remove.

        Raises:
            KeyError: If the node does not exist.
        """
        if node_id not in self._nodes:
            raise KeyError(f"Node {node_id!r} not found in graph {self.id!r}")
        del self._nodes[node_id]
        # Cascade-delete incident edges.
        to_remove = [
            eid
            for eid, e in self._edges.items()
            if e.source_id == node_id or e.target_id == node_id
        ]
        for eid in to_remove:
            del self._edges[eid]
        self._touch()

    def get_node(self, node_id: NodeId) -> Node:
        """Retrieve a node by ID.

        Args:
            node_id: Target node identifier.

        Returns:
            The :class:`~knowledge_platform.core.node.Node`.

        Raises:
            KeyError: If not found.
        """
        return self._nodes[node_id]

    def nodes(self, type_name: str | None = None) -> Iterator[Node]:
        """Iterate over nodes, optionally filtered by *type_name*.

        Args:
            type_name: If given, yield only nodes with this type.

        Yields:
            Matching :class:`~knowledge_platform.core.node.Node` instances.
        """
        for node in self._nodes.values():
            if type_name is None or node.type_name == type_name:
                yield node

    # ------------------------------------------------------------------
    # Edge mutations
    # ------------------------------------------------------------------

    def add_edge(self, edge: Edge) -> None:
        """Insert *edge* into this graph.

        Args:
            edge: Must reference nodes within this graph.

        Raises:
            ValueError: If the edge belongs to a different graph or already exists.
            KeyError: If either endpoint node is missing.
        """
        if edge.graph_id != self.id:
            raise ValueError(f"Edge graph_id {edge.graph_id!r} != {self.id!r}")
        if edge.id in self._edges:
            raise ValueError(f"Edge {edge.id!r} already exists in graph {self.id!r}")
        if edge.source_id not in self._nodes:
            raise KeyError(f"Source node {edge.source_id!r} not found")
        if edge.target_id not in self._nodes:
            raise KeyError(f"Target node {edge.target_id!r} not found")
        self._edges[edge.id] = edge
        self._touch()

    def remove_edge(self, edge_id: EdgeId) -> None:
        """Delete an edge by ID.

        Args:
            edge_id: Target edge identifier.

        Raises:
            KeyError: If not found.
        """
        if edge_id not in self._edges:
            raise KeyError(f"Edge {edge_id!r} not found in graph {self.id!r}")
        del self._edges[edge_id]
        self._touch()

    def get_edge(self, edge_id: EdgeId) -> Edge:
        """Retrieve an edge by ID.

        Args:
            edge_id: Target edge identifier.

        Returns:
            The :class:`~knowledge_platform.core.edge.Edge`.

        Raises:
            KeyError: If not found.
        """
        return self._edges[edge_id]

    def edges(self, type_name: str | None = None) -> Iterator[Edge]:
        """Iterate over edges, optionally filtered by *type_name*.

        Args:
            type_name: If given, yield only edges with this type.

        Yields:
            Matching :class:`~knowledge_platform.core.edge.Edge` instances.
        """
        for edge in self._edges.values():
            if type_name is None or edge.type_name == type_name:
                yield edge

    def outgoing_edges(self, node_id: NodeId, type_name: str | None = None) -> Iterator[Edge]:
        """Yield edges where *node_id* is the source.

        Args:
            node_id: Source node identifier.
            type_name: Optional type filter.

        Yields:
            Matching outgoing :class:`~knowledge_platform.core.edge.Edge` instances.
        """
        for edge in self._edges.values():
            if edge.source_id == node_id:
                if type_name is None or edge.type_name == type_name:
                    yield edge

    def incoming_edges(self, node_id: NodeId, type_name: str | None = None) -> Iterator[Edge]:
        """Yield edges where *node_id* is the target.

        Args:
            node_id: Target node identifier.
            type_name: Optional type filter.

        Yields:
            Matching incoming :class:`~knowledge_platform.core.edge.Edge` instances.
        """
        for edge in self._edges.values():
            if edge.target_id == node_id:
                if type_name is None or edge.type_name == type_name:
                    yield edge

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def node_count(self) -> int:
        """Return the number of nodes."""
        return len(self._nodes)

    def edge_count(self) -> int:
        """Return the number of edges."""
        return len(self._edges)

    def _touch(self) -> None:
        self.updated_at = datetime.now(timezone.utc)
        self.version += 1

    def __repr__(self) -> str:  # pragma: no cover
        return (
            f"Graph(id={self.id!r}, type={self.type_name!r}, "
            f"nodes={self.node_count()}, edges={self.edge_count()}, v{self.version})"
        )

Functions

add_edge
add_edge(edge: Edge) -> None

Insert edge into this graph.

Parameters:

Name Type Description Default
edge Edge

Must reference nodes within this graph.

required

Raises:

Type Description
ValueError

If the edge belongs to a different graph or already exists.

KeyError

If either endpoint node is missing.

Source code in src/knowledge_platform/core/graph.py
def add_edge(self, edge: Edge) -> None:
    """Insert *edge* into this graph.

    Args:
        edge: Must reference nodes within this graph.

    Raises:
        ValueError: If the edge belongs to a different graph or already exists.
        KeyError: If either endpoint node is missing.
    """
    if edge.graph_id != self.id:
        raise ValueError(f"Edge graph_id {edge.graph_id!r} != {self.id!r}")
    if edge.id in self._edges:
        raise ValueError(f"Edge {edge.id!r} already exists in graph {self.id!r}")
    if edge.source_id not in self._nodes:
        raise KeyError(f"Source node {edge.source_id!r} not found")
    if edge.target_id not in self._nodes:
        raise KeyError(f"Target node {edge.target_id!r} not found")
    self._edges[edge.id] = edge
    self._touch()
add_node
add_node(node: Node) -> None

Insert node into this graph.

Parameters:

Name Type Description Default
node Node

Must have :attr:~Node.graph_id matching this graph.

required

Raises:

Type Description
ValueError

If the node belongs to a different graph or already exists.

Source code in src/knowledge_platform/core/graph.py
def add_node(self, node: Node) -> None:
    """Insert *node* into this graph.

    Args:
        node: Must have :attr:`~Node.graph_id` matching this graph.

    Raises:
        ValueError: If the node belongs to a different graph or already exists.
    """
    if node.graph_id != self.id:
        raise ValueError(f"Node graph_id {node.graph_id!r} != {self.id!r}")
    if node.id in self._nodes:
        raise ValueError(f"Node {node.id!r} already exists in graph {self.id!r}")
    self._nodes[node.id] = node
    self._touch()
create classmethod
create(workspace_id: WorkspaceId, type_name: str, name: str = '') -> 'Graph'

Create a new empty graph.

Parameters:

Name Type Description Default
workspace_id WorkspaceId

Owning workspace.

required
type_name str

Semantic graph type name.

required
name str

Optional display name.

''

Returns:

Name Type Description
Empty 'Graph'

class:Graph.

Source code in src/knowledge_platform/core/graph.py
@classmethod
def create(cls, workspace_id: WorkspaceId, type_name: str, name: str = "") -> "Graph":
    """Create a new empty graph.

    Args:
        workspace_id: Owning workspace.
        type_name: Semantic graph type name.
        name: Optional display name.

    Returns:
        Empty :class:`Graph`.
    """
    now = datetime.now(timezone.utc)
    return cls(
        id=new_graph_id(),
        workspace_id=workspace_id,
        type_name=type_name,
        name=name,
        version=1,
        created_at=now,
        updated_at=now,
    )
edge_count
edge_count() -> int

Return the number of edges.

Source code in src/knowledge_platform/core/graph.py
def edge_count(self) -> int:
    """Return the number of edges."""
    return len(self._edges)
edges
edges(type_name: str | None = None) -> Iterator[Edge]

Iterate over edges, optionally filtered by type_name.

Parameters:

Name Type Description Default
type_name str | None

If given, yield only edges with this type.

None

Yields:

Name Type Description
Matching Edge

class:~knowledge_platform.core.edge.Edge instances.

Source code in src/knowledge_platform/core/graph.py
def edges(self, type_name: str | None = None) -> Iterator[Edge]:
    """Iterate over edges, optionally filtered by *type_name*.

    Args:
        type_name: If given, yield only edges with this type.

    Yields:
        Matching :class:`~knowledge_platform.core.edge.Edge` instances.
    """
    for edge in self._edges.values():
        if type_name is None or edge.type_name == type_name:
            yield edge
get_edge
get_edge(edge_id: EdgeId) -> Edge

Retrieve an edge by ID.

Parameters:

Name Type Description Default
edge_id EdgeId

Target edge identifier.

required

Returns:

Name Type Description
The Edge

class:~knowledge_platform.core.edge.Edge.

Raises:

Type Description
KeyError

If not found.

Source code in src/knowledge_platform/core/graph.py
def get_edge(self, edge_id: EdgeId) -> Edge:
    """Retrieve an edge by ID.

    Args:
        edge_id: Target edge identifier.

    Returns:
        The :class:`~knowledge_platform.core.edge.Edge`.

    Raises:
        KeyError: If not found.
    """
    return self._edges[edge_id]
get_node
get_node(node_id: NodeId) -> Node

Retrieve a node by ID.

Parameters:

Name Type Description Default
node_id NodeId

Target node identifier.

required

Returns:

Name Type Description
The Node

class:~knowledge_platform.core.node.Node.

Raises:

Type Description
KeyError

If not found.

Source code in src/knowledge_platform/core/graph.py
def get_node(self, node_id: NodeId) -> Node:
    """Retrieve a node by ID.

    Args:
        node_id: Target node identifier.

    Returns:
        The :class:`~knowledge_platform.core.node.Node`.

    Raises:
        KeyError: If not found.
    """
    return self._nodes[node_id]
incoming_edges
incoming_edges(node_id: NodeId, type_name: str | None = None) -> Iterator[Edge]

Yield edges where node_id is the target.

Parameters:

Name Type Description Default
node_id NodeId

Target node identifier.

required
type_name str | None

Optional type filter.

None

Yields:

Type Description
Edge

Matching incoming :class:~knowledge_platform.core.edge.Edge instances.

Source code in src/knowledge_platform/core/graph.py
def incoming_edges(self, node_id: NodeId, type_name: str | None = None) -> Iterator[Edge]:
    """Yield edges where *node_id* is the target.

    Args:
        node_id: Target node identifier.
        type_name: Optional type filter.

    Yields:
        Matching incoming :class:`~knowledge_platform.core.edge.Edge` instances.
    """
    for edge in self._edges.values():
        if edge.target_id == node_id:
            if type_name is None or edge.type_name == type_name:
                yield edge
node_count
node_count() -> int

Return the number of nodes.

Source code in src/knowledge_platform/core/graph.py
def node_count(self) -> int:
    """Return the number of nodes."""
    return len(self._nodes)
nodes
nodes(type_name: str | None = None) -> Iterator[Node]

Iterate over nodes, optionally filtered by type_name.

Parameters:

Name Type Description Default
type_name str | None

If given, yield only nodes with this type.

None

Yields:

Name Type Description
Matching Node

class:~knowledge_platform.core.node.Node instances.

Source code in src/knowledge_platform/core/graph.py
def nodes(self, type_name: str | None = None) -> Iterator[Node]:
    """Iterate over nodes, optionally filtered by *type_name*.

    Args:
        type_name: If given, yield only nodes with this type.

    Yields:
        Matching :class:`~knowledge_platform.core.node.Node` instances.
    """
    for node in self._nodes.values():
        if type_name is None or node.type_name == type_name:
            yield node
outgoing_edges
outgoing_edges(node_id: NodeId, type_name: str | None = None) -> Iterator[Edge]

Yield edges where node_id is the source.

Parameters:

Name Type Description Default
node_id NodeId

Source node identifier.

required
type_name str | None

Optional type filter.

None

Yields:

Type Description
Edge

Matching outgoing :class:~knowledge_platform.core.edge.Edge instances.

Source code in src/knowledge_platform/core/graph.py
def outgoing_edges(self, node_id: NodeId, type_name: str | None = None) -> Iterator[Edge]:
    """Yield edges where *node_id* is the source.

    Args:
        node_id: Source node identifier.
        type_name: Optional type filter.

    Yields:
        Matching outgoing :class:`~knowledge_platform.core.edge.Edge` instances.
    """
    for edge in self._edges.values():
        if edge.source_id == node_id:
            if type_name is None or edge.type_name == type_name:
                yield edge
remove_edge
remove_edge(edge_id: EdgeId) -> None

Delete an edge by ID.

Parameters:

Name Type Description Default
edge_id EdgeId

Target edge identifier.

required

Raises:

Type Description
KeyError

If not found.

Source code in src/knowledge_platform/core/graph.py
def remove_edge(self, edge_id: EdgeId) -> None:
    """Delete an edge by ID.

    Args:
        edge_id: Target edge identifier.

    Raises:
        KeyError: If not found.
    """
    if edge_id not in self._edges:
        raise KeyError(f"Edge {edge_id!r} not found in graph {self.id!r}")
    del self._edges[edge_id]
    self._touch()
remove_node
remove_node(node_id: NodeId) -> None

Delete a node and all its incident edges.

Parameters:

Name Type Description Default
node_id NodeId

ID of the node to remove.

required

Raises:

Type Description
KeyError

If the node does not exist.

Source code in src/knowledge_platform/core/graph.py
def remove_node(self, node_id: NodeId) -> None:
    """Delete a node and all its incident edges.

    Args:
        node_id: ID of the node to remove.

    Raises:
        KeyError: If the node does not exist.
    """
    if node_id not in self._nodes:
        raise KeyError(f"Node {node_id!r} not found in graph {self.id!r}")
    del self._nodes[node_id]
    # Cascade-delete incident edges.
    to_remove = [
        eid
        for eid, e in self._edges.items()
        if e.source_id == node_id or e.target_id == node_id
    ]
    for eid in to_remove:
        del self._edges[eid]
    self._touch()
update_node
update_node(node: Node) -> None

Replace an existing node with an updated version.

Parameters:

Name Type Description Default
node Node

Updated node (same ID, higher version).

required

Raises:

Type Description
KeyError

If the node does not exist in this graph.

Source code in src/knowledge_platform/core/graph.py
def update_node(self, node: Node) -> None:
    """Replace an existing node with an updated version.

    Args:
        node: Updated node (same ID, higher version).

    Raises:
        KeyError: If the node does not exist in this graph.
    """
    if node.id not in self._nodes:
        raise KeyError(f"Node {node.id!r} not found in graph {self.id!r}")
    self._nodes[node.id] = node
    self._touch()