Skip to content

feat: driver operations architecture redesign#1232

Merged
prasmussen15 merged 3 commits intomainfrom
feat/driver-operations-redesign
Feb 16, 2026
Merged

feat: driver operations architecture redesign#1232
prasmussen15 merged 3 commits intomainfrom
feat/driver-operations-redesign

Conversation

@prasmussen15
Copy link
Collaborator

Summary

  • Introduces a clean operations-based architecture for graph driver operations with abstract interfaces (ABCs) and concrete implementations for Neo4j and FalkorDB
  • Adds QueryExecutor and Transaction ABCs for database-agnostic query execution, plus 11 operations ABCs covering entity/episode/community nodes, entity/episodic/community/has_episode/next_episode edges, search, and graph maintenance
  • Implements all 22 concrete operation classes (11 Neo4j + 11 FalkorDB) with proper transaction semantics, provider-specific query patterns (RedisSearch fulltext, vecf32() embeddings for FalkorDB; Lucene fulltext, native vector index for Neo4j), and NodeNamespace/EdgeNamespace convenience wrappers

Architecture

graphiti_core/driver/
├── query_executor.py          # QueryExecutor + Transaction ABCs
├── operations/                # 11 abstract operation interfaces
│   ├── entity_node_ops.py
│   ├── episode_node_ops.py
│   ├── community_node_ops.py
│   ├── saga_node_ops.py
│   ├── entity_edge_ops.py
│   ├── episodic_edge_ops.py
│   ├── community_edge_ops.py
│   ├── has_episode_edge_ops.py
│   ├── next_episode_edge_ops.py
│   ├── search_ops.py
│   └── graph_ops.py
├── neo4j/operations/          # 11 Neo4j concrete implementations
├── falkordb/operations/       # 11 FalkorDB concrete implementations

Each driver now exposes operations via typed properties (e.g., driver.entity_node_ops, driver.search_ops), all accepting the driver itself as a QueryExecutor and an optional Transaction for write operations.

Test plan

  • ruff check passes on all new and modified files
  • pyright reports 0 new errors across the entire graphiti_core/ directory
  • All 282 existing unit tests continue to pass (12 pre-existing failures unrelated to this change)
  • FalkorDB circular import resolved (STOPWORDS moved to package __init__.py)
  • Integration tests with live Neo4j/FalkorDB instances

🤖 Generated with Claude Code

… concrete implementations

Introduces a clean operations-based architecture for graph driver operations,
replacing inline query logic with abstract interfaces (ABCs) and concrete
implementations for both Neo4j and FalkorDB backends.

Key changes:
- Add QueryExecutor and Transaction ABCs for database-agnostic query execution
- Add 11 operations ABCs covering all node, edge, search, and graph maintenance operations
- Implement all 11 operations for Neo4j with real transaction commit/rollback
- Implement all 11 operations for FalkorDB with RedisSearch fulltext and vecf32 embeddings
- Add NodeNamespace and EdgeNamespace convenience wrappers on Graphiti class
- Wire operations into Neo4jDriver and FalkorDriver with property accessors
- Fix circular import by moving STOPWORDS to graphiti_core.driver.falkordb package
- Include design spec documenting architecture decisions and migration plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@@ -61,6 +102,64 @@ def __init__(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scheduling build_indices_and_constraints as a fire-and-forget task in __init__ is problematic:

  1. Silent failures: If the task fails, exceptions are silently swallowed unless you've configured an exception handler for the event loop
  2. Race condition: Code that immediately uses the driver may execute before indices are ready
  3. Testing difficulty: Makes it hard to control when indices are built in tests

Consider either:

  • Removing the auto-scheduling and documenting that users should call build_indices_and_constraints() explicitly
  • Making initialization async with a factory method like Neo4jDriver.create()

Comment on lines +48 to +74
def _entity_node_from_record(record: Any) -> EntityNode:
attributes = record['attributes']
attributes.pop('uuid', None)
attributes.pop('name', None)
attributes.pop('group_id', None)
attributes.pop('name_embedding', None)
attributes.pop('summary', None)
attributes.pop('created_at', None)
attributes.pop('labels', None)

labels = record.get('labels', [])
group_id = record.get('group_id')
dynamic_label = 'Entity_' + group_id.replace('-', '')
if dynamic_label in labels:
labels.remove(dynamic_label)

return EntityNode(
uuid=record['uuid'],
name=record['name'],
name_embedding=record.get('name_embedding'),
group_id=group_id,
labels=labels,
created_at=parse_db_date(record['created_at']), # type: ignore[arg-type]
summary=record['summary'],
attributes=attributes,
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This _entity_node_from_record function is duplicated verbatim across 4 files:

  • neo4j/operations/search_ops.py
  • neo4j/operations/entity_node_ops.py
  • neo4j/operations/graph_ops.py
  • And their FalkorDB equivalents

Similarly, _entity_edge_from_record and _community_node_from_record are duplicated across multiple files.

Consider extracting these into a shared graphiti_core/driver/record_converters.py module to eliminate the duplication.

Comment on lines +168 to +210
@property
def entity_node_ops(self) -> EntityNodeOperations | None:
return None

@property
def episode_node_ops(self) -> EpisodeNodeOperations | None:
return None

@property
def community_node_ops(self) -> CommunityNodeOperations | None:
return None

@property
def saga_node_ops(self) -> SagaNodeOperations | None:
return None

@property
def entity_edge_ops(self) -> EntityEdgeOperations | None:
return None

@property
def episodic_edge_ops(self) -> EpisodicEdgeOperations | None:
return None

@property
def community_edge_ops(self) -> CommunityEdgeOperations | None:
return None

@property
def has_episode_edge_ops(self) -> HasEpisodeEdgeOperations | None:
return None

@property
def next_episode_edge_ops(self) -> NextEpisodeEdgeOperations | None:
return None

@property
def search_ops(self) -> SearchOperations | None:
return None

@property
def graph_ops(self) -> GraphMaintenanceOperations | None:
return None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these operations properties return | None but the concrete drivers (Neo4jDriver, FalkorDriver) always return non-None implementations. This forces callers to do null checks even though these are never actually None for real drivers.

Consider either:

  1. Raising NotImplementedError instead of returning None (like other abstract methods in this class)
  2. Making these abstract properties that concrete drivers must implement

The current approach leads to unnecessary null checks throughout the codebase and makes the type system less helpful.


logger = logging.getLogger(__name__)

MAX_QUERY_LENGTH = 128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant MAX_QUERY_LENGTH = 128 is defined here but build_fulltext_query on line 782 uses a default of max_query_length: int = 8000. This inconsistency between the class-level constant and the method default could cause confusion. Either use the constant consistently or document why different defaults are appropriate.

Comment on lines +321 to +334
def __init__(self, driver: GraphDriver, embedder: EmbedderClient):
entity_node_ops = driver.entity_node_ops
episode_node_ops = driver.episode_node_ops
community_node_ops = driver.community_node_ops
saga_node_ops = driver.saga_node_ops

if entity_node_ops is not None:
self.entity = EntityNodeNamespace(driver, entity_node_ops, embedder)
if episode_node_ops is not None:
self.episode = EpisodeNodeNamespace(driver, episode_node_ops)
if community_node_ops is not None:
self.community = CommunityNodeNamespace(driver, community_node_ops, embedder)
if saga_node_ops is not None:
self.saga = SagaNodeNamespace(driver, saga_node_ops)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The namespace attributes (entity, episode, community, saga) are only set if the corresponding ops is not None, which means accessing graphiti.nodes.entity will raise AttributeError if the driver doesn't implement entity operations.

Consider either:

  1. Always setting all attributes but raising NotImplementedError when methods are called
  2. Documenting this behavior and providing a has_entity_ops() method or similar

This would improve the developer experience when working with drivers that may not implement all operations.

Comment on lines +97 to +103
@abstractmethod
async def load_embeddings_bulk(
self,
executor: QueryExecutor,
nodes: list[EntityNode],
batch_size: int = 100,
) -> None: ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The batch_size parameter is declared in the abstract method but is not enforced in either the Neo4j or FalkorDB implementations for load_embeddings_bulk. The FalkorDB implementation explicitly ignores it with # noqa: ARG002. Consider either:

  1. Implementing actual batching in the concrete implementations
  2. Removing the parameter from the interface if batching isn't needed

@claude
Copy link
Contributor

claude bot commented Feb 16, 2026

Code Review Summary

This PR introduces a significant architectural redesign with 22 new operation classes. While the overall design is sound and follows good separation of concerns, there are several areas that need attention:

Major Concerns

1. No Unit Tests for New Operations
This PR adds 9,298 lines of new code including 22 operation classes across Neo4j and FalkorDB implementations, but no unit tests for these operations. The PR description notes that existing tests pass, but dedicated tests for the new operations layer would improve confidence in the implementation and prevent regressions.

2. Significant Code Duplication
Helper functions like _entity_node_from_record, _entity_edge_from_record, and _community_node_from_record are duplicated verbatim across multiple files (search_ops.py, entity_node_ops.py, graph_ops.py for both Neo4j and FalkorDB). Consider extracting these to a shared module.

3. Fire-and-Forget Task in Constructor
Both Neo4jDriver and FalkorDriver schedule build_indices_and_constraints() as a fire-and-forget task in __init__. This can lead to silent failures and race conditions where code tries to use the driver before indices are ready.

Additional Issues

  • Inconsistent MAX_QUERY_LENGTH constants: FalkorDB search_ops defines MAX_QUERY_LENGTH = 128 but uses max_query_length: int = 8000 as default in build_fulltext_query
  • Unused batch_size parameters: Several abstract methods declare batch_size but implementations ignore it with # noqa: ARG002
  • Operations properties return None: The base GraphDriver class returns None for all ops properties, forcing null checks everywhere even though concrete drivers always provide implementations
  • Duplicate build_fulltext_query: FalkorDriver has both build_fulltext_query method and the same logic exists in FalkorSearchOperations._build_falkor_fulltext_query

Spec Document

The spec file spec/driver-operations-redesign.md is helpful but should probably be moved to a docs/ directory or removed before merge since it's a design document rather than permanent documentation.

- Fix ruff UP037: remove quoted type annotations in driver.py
  (redundant with `from __future__ import annotations`)
- Extract duplicate record parsers into shared record_parsers.py module,
  eliminating identical _entity_node_from_record, _entity_edge_from_record,
  _episodic_node_from_record, and _community_node_from_record across
  10 files in both Neo4j and FalkorDB operations
- Fix MAX_QUERY_LENGTH inconsistency in FalkorDB search_ops
  build_fulltext_query (was 8000, now uses module constant 128)
- Make namespace attributes unconditional with NotImplementedError
  for drivers that don't implement required operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
KuzuDriver doesn't implement the new operations interfaces, so the
NotImplementedError on init broke Kuzu tests. Now attributes are only
set when the driver provides them, and __getattr__ gives a clear error
on access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@prasmussen15 prasmussen15 merged commit 99923c0 into main Feb 16, 2026
10 checks passed
@prasmussen15 prasmussen15 deleted the feat/driver-operations-redesign branch February 16, 2026 18:05
@getzep getzep locked and limited conversation to collaborators Feb 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant