Skip to content

Commit 52cb3e4

Browse files
yhl999prasmussen15claudejackaldenryandanielchalef
authored
sync: merge upstream/main (2026-02-22) with explicit core patch stack (#70)
* fix(summary): exclude duplicate edges from node summary generation (getzep#1223) * fix(summary): exclude duplicate edges from node summary generation When resolving extracted edges, edges that match existing edges in the graph were still being passed to node summary generation, causing facts to be duplicated in summaries. Changes: - Update resolve_extracted_edges to return new_edges (non-duplicates) - Update _extract_and_resolve_edges to pass through new_edges - Pass only new_edges to extract_attributes_from_nodes in add_episode - An edge is considered "new" if its resolved UUID matches extracted UUID Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: bump version to 0.27.1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * feat: simplify extraction pipeline and add batch entity summarization (getzep#1224) * feat(llm): add token usage tracking for LLM calls Add TokenUsageTracker class to track input/output tokens by prompt type during LLM calls. This helps analyze token costs across different operations like extract_nodes, extract_edges, resolve_nodes, etc. Changes: - Add graphiti_core/llm_client/token_tracker.py with TokenUsageTracker - Update LLMClient base class to include token_tracker instance - Update OpenAI base client to capture and record token usage - Add token_tracker property on Graphiti class for easy access - Update podcast_runner.py to print token usage summary after ingestion Usage: client = Graphiti(...) # ... run ingestion ... client.token_tracker.print_summary(sort_by='prompt_name') Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: temporarily disable summary early return optimization Disable the optimization that skips LLM calls when node summary + edge facts is under 2000 characters. This forces all summaries to be generated via LLM for token usage analysis. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Revert "chore: temporarily disable summary early return optimization" This reverts the summary optimization changes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: simplify extraction pipeline and add batch entity summarization - Remove chunking code for entity-dense episodes (node_operations.py) - Delete _extract_nodes_chunked, _extract_from_chunk, _merge_extracted_entities - Always use single LLM call for entity extraction - Remove chunking code for edge extraction (edge_operations.py) - Remove MAX_NODES constant and generate_covering_chunks usage - Process all nodes in single LLM call instead of covering subsets - Add batch entity summarization (node_operations.py, extract_nodes.py) - New SummarizedEntity and SummarizedEntities Pydantic models - New extract_summaries_batch prompt for batch processing - New _extract_entity_summaries_batch function - Nodes with short summaries get edge facts appended directly (no LLM) - Only nodes needing LLM summarization are batched together - Simplify edge attribute extraction (extract_edges.py, edge_operations.py) - Remove episode_content from context (attributes from fact only) - Keep reference_time for temporal resolution - Add existing_attributes to preserve/update existing values - Improve edge deduplication prompt (dedupe_edges.py, edge_operations.py) - Use continuous indexing across duplicate and invalidation candidates - Deduplicate invalidation candidates against duplicate candidates - Allow EXISTING FACTS to be both duplicates AND contradicted - Consolidate to single contradicted_facts field - Remove obsolete chunking tests (test_entity_extraction.py) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: bump version to 0.27.2pre1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add token tracking for Anthropic/Gemini clients and missing tests - Implement token tracking in AnthropicClient._generate_response() and generate_response() using result.usage.input_tokens/output_tokens - Implement token tracking in GeminiClient._generate_response() and generate_response() using response.usage_metadata - Add comprehensive unit tests for TokenUsageTracker class - Add tests for _extract_entity_summaries_batch function covering: - No nodes needing summarization - Short summaries with edge facts - Long summaries requiring LLM - Node filter (should_summarize_node) - Batch multiple nodes - Unknown entity handling - Missing episode and summary Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update test_node_operations.py for batch summarization API - Remove import of extract_attributes_from_node (function was removed) - Add import of _extract_entity_summaries_batch - Update tests to use new batch summarization API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add MAX_NODES limit for batch entity summarization - Add MAX_NODES = 30 constant - Partition nodes needing summarization into flights of MAX_NODES - Extract _process_summary_flight helper for processing each flight - Each flight makes a separate LLM call to avoid context overflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Change default OpenAI models to gpt-5-mini Update both DEFAULT_MODEL and DEFAULT_SMALL_MODEL to use gpt-5-mini. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update podcast_runner.py to use default OpenAI models Remove explicit model configuration to use the default gpt-5-mini models from OpenAIClient. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Revert default model changes to gpt-4.1-mini/nano Restore the original default models instead of gpt-5-mini. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Address PR review comments - Fix unreachable code in _handle_structured_response (check response.refusal) - Process node summary flights in parallel using semaphore_gather - Use case-insensitive name matching for LLM summary responses - Handle duplicate node names by applying summary to all matching nodes - Fix edge case when both edge lists are empty in contradiction processing - Fix potential AttributeError when episode is None in edge attributes - Add tests for flight partitioning and case-insensitive name matching Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * chore(deps): update dependencies to fix dependabot alerts (getzep#1225) Update lock files to address security alerts: - cryptography, cffi, and other security-related packages - Major version bumps for langchain-core and related packages - Minor updates to other dependencies Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com> * @contextablemark has signed the CLA in getzep#1227 * @avonian has signed the CLA in getzep#1230 * feat: driver operations architecture redesign (getzep#1232) * feat: add driver operations architecture with abstract interfaces and 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> * fix: address PR review feedback - 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> * fix: make namespace init graceful for drivers missing operations 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> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Bump graphiti-core[falkordb] from 0.26.3 to 0.27.1 in /mcp_server (getzep#1231) Bumps [graphiti-core[falkordb]](https://github.com/getzep/graphiti) from 0.26.3 to 0.27.1. - [Release notes](https://github.com/getzep/graphiti/releases) - [Commits](getzep/graphiti@v0.26.3...v0.27.1) --- updated-dependencies: - dependency-name: graphiti-core[falkordb] dependency-version: 0.27.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: implement Neptune and Kuzu driver operations (getzep#1235) * feat: implement Neptune and Kuzu driver operations Extract scattered Neptune and Kuzu logic from nodes.py, edges.py, search_utils.py, and maintenance utilities into structured operations classes, following the same architecture established for Neo4j and FalkorDB in getzep#1232. Each driver now has 11 operations classes: entity_node_ops, episode_node_ops, community_node_ops, saga_node_ops, entity_edge_ops, episodic_edge_ops, community_edge_ops, has_episode_edge_ops, next_episode_edge_ops, search_ops, and graph_ops. Neptune-specific: AOSS fulltext search, comma-separated embeddings, manual cosine similarity, removeKeyFromMap() for saves. Kuzu-specific: RelatesToNode_ intermediate pattern, JSON attributes, QUERY_FTS_INDEX/array_cosine_similarity, BFS depth doubling, Saga/HAS_EPISODE/NEXT_EPISODE schema additions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review comments for Neptune/Kuzu operations - Extract `_label_propagation` and `Neighbor` to shared `graph_utils.py` module, removing duplication across all 4 driver graph_ops.py files - Extract `_parse_kuzu_entity_node` and `_parse_kuzu_entity_edge` to shared `kuzu/operations/record_parsers.py`, removing duplication across entity_node_ops, entity_edge_ops, graph_ops, and search_ops - Fix UNWIND bug in Kuzu `node_distance_reranker` and `episode_mentions_reranker` (Kuzu doesn't support UNWIND) - Fix `_build_kuzu_fulltext_query` max_query_length calculation bug (`len(group_ids or '')` was meaningless) - Replace inline import + cast pattern with constructor dependency injection for Neptune AOSS access in community_node_ops, search_ops, and graph_ops - Use existing `calculate_cosine_similarity` from `search_utils.py` instead of duplicating it in Neptune search_ops Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump version to 0.28.0 and document graph driver architecture (getzep#1236) * chore: bump version to 0.28.0 and document graph driver architecture Bump graphiti-core to 0.28.0 and update the MCP server dependency to match. Add a new "Graph Driver Architecture" section to the README explaining how the pluggable driver layer works and how to add a new graph database backend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: address PR review comments on driver architecture section - Add legacy directories (graph_operations/, search_interface/) and Kuzu record_parsers.py to the diagram, with a "simplified; see source" note - Clarify that the ABC defines operations properties as optional (| None) and concrete drivers override to return non-optional types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove PII from log messages (getzep#1237) * fix: remove PII from log messages Remove entity names, edge facts, and LLM input/output content from log messages to prevent personally identifiable information from leaking into logs. Replace with UUIDs, counts, and structural metadata only. Changes: - edge_operations.py: Remove entity names from WARNING logs, replace full edge objects and name tuples with UUIDs in DEBUG logs - node_operations.py: Remove entity names from WARNING and DEBUG logs, log only UUIDs and counts instead of (name, uuid) tuples - llm_client/client.py: Replace full message content dump in _get_failed_generation_log with message count and role metadata Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: preserve schema metadata and truncated output in logs Address review feedback — the initial PII fix overcorrected by removing non-PII debugging context: - Restore relation types in edge WARNING logs (schema metadata, not PII) - Restore truncated duplicate_name in dedup WARNING (needed for diagnosis) - Restore truncated entity name (first 30 chars) in summary WARNING - Restore truncated raw LLM output (first 500 chars) in failed generation ERROR logs — malformed output is structural, not user content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix: extract custom edge attributes on first episode ingestion (getzep#1242) The fast path in resolve_extracted_edge() returned early when no related/existing edges existed, skipping the LLM attribute extraction call. This meant edges created during the first episode never had their custom ontology attributes populated. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace diskcache with sqlite-based cache to resolve CVE (getzep#1238) * fix: replace diskcache with sqlite-based cache to resolve CVE diskcache <= 5.6.3 has an unsafe pickle deserialization vulnerability with no patched version available. Replace it with a minimal SQLite + JSON cache implementation that only stores JSON-serializable data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add thread safety, error handling, and cleanup to LLMCache - Use check_same_thread=False for safe cross-thread SQLite access - Handle JSON serialization/deserialization errors gracefully - Add __del__ for connection cleanup on garbage collection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: upgrade urllib3 to 2.6.3 in examples lock file Fixes decompression-bomb redirect bypass vulnerability (requires >= 2.6.3). The main and mcp_server lock files already had 2.6.3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add unit tests for LLMCache Covers get/set, overwrites, nested values, non-serializable handling, corrupted entry recovery, directory creation, persistence, and cleanup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump version to 0.28.1 (getzep#1243) Patch release so that mcp_server and server lockfiles can drop the diskcache transitive dependency once published, resolving dependabot alerts #69 and #70. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * chore: regenerate lockfiles to drop diskcache (getzep#1244) * chore: regenerate lockfiles to drop diskcache dependency Resolves dependabot alerts #69 and #70 (unsafe pickle deserialization in diskcache). Now that graphiti-core 0.28.1 is published without diskcache, all downstream lockfiles can be updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update server/pyproject.toml Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * @Yifan-233-max has signed the CLA in getzep#1245 * @sprotasovitsky has signed the CLA in getzep#1254 * @hanxiao has signed the CLA in getzep#1257 * docs(sync): add upstream baseline and graphiti_core patch classification * fix(edges): skip malformed RELATES_TO rows in get_between_nodes (#66) * fix(edges): ignore malformed RELATES_TO edges in get_between_nodes Filter out edges missing uuid/group_id/episodes to avoid EntityEdge validation failures when legacy malformed relationships exist between node pairs. * fix(edges): ignore malformed RELATES_TO rows in get_by_node_uuid/get_by_uuids/get_by_group_ids * fix(edges): add null guards to Kuzu get_between_nodes query Add WHERE e.uuid IS NOT NULL AND e.group_id IS NOT NULL AND e.episodes IS NOT NULL to the Kuzu branch of get_between_nodes, matching the Neo4j branch's guards. Addresses review finding on PR #66. (cherry picked from commit ff34e16) * feat: trust-aware retrieval — post-RRF additive boost (#63) * feat: trust-aware retrieval — post-RRF additive boost for promoted facts - Add trust_weight field to SearchConfig (default 0.0 = disabled, backwards compat) - Add rrf_with_trust_boost() and load_trust_scores() to search_utils.py - Add EDGE/NODE_HYBRID_SEARCH_RRF_TRUST recipes - Wire trust boost into edge and node search pipelines in search.py - MCP server: GRAPHITI_TRUST_WEIGHT env var (default 0.15) * fix: review findings — default trust_weight=0.0, skip episode_mentions, flatten double RRF, safe env parsing - H1: MCP TRUST_WEIGHT default 0.15 → 0.0 (opt-in, not opt-out) - H2: Trust boost only for RRF reranker, not episode_mentions (was no-op with overhead) - M1: Remove redundant outer rrf() call in trust branch (use set comprehension for UUIDs) - L1: Try/except on GRAPHITI_TRUST_WEIGHT env var parsing * fix: ruff lint — consistent trust_weight default, strict zip, remove unused import - rrf_with_trust_boost() default trust_weight 0.15 → 0.0 (consistent with SearchConfig) - zip(uuids, rrf_scores, strict=True) per B905 - Remove unused OntologyRegistry import (F401) - Fix import sorting (I001) (cherry picked from commit f93924f) * feat(dedupe): migration-only deterministic edge dedupe mode (#67) * feat(dedupe): add deterministic migration mode to bypass semantic edge dedupe When GRAPHITI_DEDUPE_MODE=deterministic: - keep exact-match fast path - skip LLM duplicate/contradiction resolution - preserve optional attribute extraction Intended for controlled migration backfills where semantic dedupe instability must not block canonical ingestion. * refactor(dedupe): replace env-var GRAPHITI_DEDUPE_MODE with explicit dedupe_mode parameter - Remove os.getenv('GRAPHITI_DEDUPE_MODE') from resolve_extracted_edge - Add dedupe_mode: Literal['semantic','deterministic']='semantic' to: - resolve_extracted_edge(...) - resolve_extracted_edges(...) - _extract_and_resolve_edges(...) [internal helper] - add_episode(...) [public API] - Thread parameter through all call sites - Default remains 'semantic' — no behavior change for existing callsites - add_episode_bulk and add_triplet implicitly keep 'semantic' via default Addresses review finding on PR #67: env-var global bypass too risky. * fix(edge_ops): preserve semantic-mode call signature for resolve_extracted_edge Avoid passing dedupe_mode kwarg in semantic mode so existing monkeypatched tests/callsites without dedupe_mode parameter remain compatible. * docs+api: document dedupe_mode and ontology safety notes for migration hardening (cherry picked from commit 5f4e7c0) * ci(sync): add graphiti_core allowlist guardrail * sync: align maintenance tests with upstream and allow upstream-sync reports * ci: use ubuntu-latest for upstream legacy workflows in fork * ci: disable upstream legacy workflow jobs in fork repo --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Preston Rasmussen <109292228+prasmussen15@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Jack Ryan <61809814+jackaldenryan@users.noreply.github.com> Co-authored-by: Daniel Chalef <131175+danielchalef@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
1 parent 72a5580 commit 52cb3e4

110 files changed

Lines changed: 16937 additions & 695 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ concurrency:
1414
cancel-in-progress: true
1515

1616
jobs:
17+
graphiti_core_guard:
18+
name: Graphiti core allowlist guard
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
- name: Enforce graphiti_core patch allowlist
25+
run: bash scripts/ci/check_graphiti_core_allowlist.sh
26+
1727
lint:
1828
name: Ruff lint
1929
runs-on: ubuntu-latest

‎.github/workflows/lint.yml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Lint with Ruff
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
jobs:
10+
ruff:
11+
if: github.repository == 'getzep/graphiti'
12+
environment: development
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.10"
20+
- name: Install dependencies
21+
run: |
22+
python -m pip install --upgrade pip
23+
pip install "ruff>0.1.7"
24+
- name: Run Ruff linting
25+
run: ruff check --output-format=github

‎.github/workflows/typecheck.yml‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Pyright Type Check
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
push:
8+
branches: ["main"]
9+
pull_request:
10+
branches: ["main"]
11+
12+
jobs:
13+
pyright:
14+
if: github.repository == 'getzep/graphiti'
15+
runs-on: ubuntu-latest
16+
environment: development
17+
steps:
18+
- uses: actions/checkout@v4
19+
- name: Set up Python
20+
id: setup-python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.10"
24+
- name: Install uv
25+
uses: astral-sh/setup-uv@v3
26+
with:
27+
version: "latest"
28+
- name: Install dependencies
29+
run: uv sync --all-extras
30+
- name: Run Pyright for graphiti-core
31+
shell: bash
32+
run: |
33+
uv run pyright ./graphiti_core
34+
- name: Install graph-service dependencies
35+
shell: bash
36+
run: |
37+
cd server
38+
uv sync --all-extras
39+
- name: Run Pyright for graph-service
40+
shell: bash
41+
run: |
42+
cd server
43+
uv run pyright .

‎.github/workflows/unit_tests.yml‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
unit-tests:
14+
if: github.repository == 'getzep/graphiti'
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Set up Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.10"
22+
- name: Install uv
23+
uses: astral-sh/setup-uv@v3
24+
with:
25+
version: "latest"
26+
- name: Install dependencies
27+
run: uv sync --all-extras
28+
- name: Run unit tests (no external dependencies)
29+
env:
30+
PYTHONPATH: ${{ github.workspace }}
31+
DISABLE_NEPTUNE: 1
32+
DISABLE_NEO4J: 1
33+
DISABLE_FALKORDB: 1
34+
DISABLE_KUZU: 1
35+
run: |
36+
uv run pytest tests/ -m "not integration" \
37+
--ignore=tests/test_graphiti_int.py \
38+
--ignore=tests/test_graphiti_mock.py \
39+
--ignore=tests/test_node_int.py \
40+
--ignore=tests/test_edge_int.py \
41+
--ignore=tests/test_entity_exclusion_int.py \
42+
--ignore=tests/driver/ \
43+
--ignore=tests/llm_client/test_anthropic_client_int.py \
44+
--ignore=tests/utils/maintenance/test_temporal_operations_int.py \
45+
--ignore=tests/cross_encoder/test_bge_reranker_client_int.py \
46+
--ignore=tests/evals/
47+
48+
database-integration-tests:
49+
if: github.repository == 'getzep/graphiti'
50+
runs-on: ubuntu-latest
51+
services:
52+
falkordb:
53+
image: falkordb/falkordb:latest
54+
ports:
55+
- 6379:6379
56+
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
57+
neo4j:
58+
image: neo4j:5.26-community
59+
ports:
60+
- 7687:7687
61+
- 7474:7474
62+
env:
63+
NEO4J_AUTH: neo4j/testpass
64+
NEO4J_PLUGINS: '["apoc"]'
65+
options: --health-cmd "cypher-shell -u neo4j -p testpass 'RETURN 1'" --health-interval 10s --health-timeout 5s --health-retries 10
66+
steps:
67+
- uses: actions/checkout@v4
68+
- name: Set up Python
69+
uses: actions/setup-python@v5
70+
with:
71+
python-version: "3.10"
72+
- name: Install uv
73+
uses: astral-sh/setup-uv@v3
74+
with:
75+
version: "latest"
76+
- name: Install redis-cli for FalkorDB health check
77+
run: sudo apt-get update && sudo apt-get install -y redis-tools
78+
- name: Install dependencies
79+
run: uv sync --all-extras
80+
- name: Wait for FalkorDB
81+
run: |
82+
timeout 60 bash -c 'until redis-cli -h localhost -p 6379 ping; do sleep 1; done'
83+
- name: Wait for Neo4j
84+
run: |
85+
timeout 60 bash -c 'until wget -O /dev/null http://localhost:7474 >/dev/null 2>&1; do sleep 1; done'
86+
- name: Run database integration tests
87+
env:
88+
PYTHONPATH: ${{ github.workspace }}
89+
NEO4J_URI: bolt://localhost:7687
90+
NEO4J_USER: neo4j
91+
NEO4J_PASSWORD: testpass
92+
FALKORDB_HOST: localhost
93+
FALKORDB_PORT: 6379
94+
DISABLE_NEPTUNE: 1
95+
run: |
96+
uv run pytest \
97+
tests/test_graphiti_mock.py \
98+
tests/test_node_int.py \
99+
tests/test_edge_int.py \
100+
tests/cross_encoder/test_bge_reranker_client_int.py \
101+
tests/driver/test_falkordb_driver.py \
102+
-m "not integration"

‎config/graphiti_core_allowlist.txt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
graphiti_core/search/search.py
2+
graphiti_core/search/search_config.py
3+
graphiti_core/search/search_config_recipes.py
4+
graphiti_core/search/search_utils.py
5+
graphiti_core/edges.py
6+
graphiti_core/graphiti.py
7+
graphiti_core/utils/maintenance/edge_operations.py

‎config/public_export_allowlist.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ allowlist:
1919
- "examples/**"
2020
- "images/**"
2121
- "reports/publicization/**"
22+
- "reports/upstream-sync/**"
2223

2324
# Build/release/config metadata
2425
- "config/**"

‎docs/runbooks/upstream-sync-openclaw.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ Why strict:
103103
- PR lane preserves review, rollback, and conflict visibility,
104104
- Sync button bypasses branch-level review discipline.
105105

106+
## Graphiti Core Patch-Stack Guardrail
107+
108+
Policy file: `config/graphiti_core_allowlist.txt`
109+
110+
CI check: `scripts/ci/check_graphiti_core_allowlist.sh` (wired into `.github/workflows/ci.yml`)
111+
112+
Rule:
113+
- Any PR touching `graphiti_core/**` must be limited to the allowlisted files.
114+
- Non-allowlisted `graphiti_core/**` changes fail CI by default.
115+
116+
Operational intent:
117+
- keep `graphiti_core` local drift explicit and small,
118+
- preserve fast upstream syncs,
119+
- move behavior to runtime layer when feasible.
120+
106121
## Rollback / Recovery
107122

108123
### A) Bad sync PR before merge

‎graphiti_core/driver/driver.py‎

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,36 @@
1414
limitations under the License.
1515
"""
1616

17+
from __future__ import annotations
18+
1719
import copy
1820
import logging
1921
import os
2022
from abc import ABC, abstractmethod
21-
from collections.abc import Coroutine
23+
from collections.abc import AsyncIterator, Coroutine
24+
from contextlib import asynccontextmanager
2225
from enum import Enum
23-
from typing import Any
26+
from typing import TYPE_CHECKING, Any
2427

2528
from dotenv import load_dotenv
2629

2730
from graphiti_core.driver.graph_operations.graph_operations import GraphOperationsInterface
31+
from graphiti_core.driver.query_executor import QueryExecutor, Transaction
2832
from graphiti_core.driver.search_interface.search_interface import SearchInterface
2933

34+
if TYPE_CHECKING:
35+
from graphiti_core.driver.operations.community_edge_ops import CommunityEdgeOperations
36+
from graphiti_core.driver.operations.community_node_ops import CommunityNodeOperations
37+
from graphiti_core.driver.operations.entity_edge_ops import EntityEdgeOperations
38+
from graphiti_core.driver.operations.entity_node_ops import EntityNodeOperations
39+
from graphiti_core.driver.operations.episode_node_ops import EpisodeNodeOperations
40+
from graphiti_core.driver.operations.episodic_edge_ops import EpisodicEdgeOperations
41+
from graphiti_core.driver.operations.graph_ops import GraphMaintenanceOperations
42+
from graphiti_core.driver.operations.has_episode_edge_ops import HasEpisodeEdgeOperations
43+
from graphiti_core.driver.operations.next_episode_edge_ops import NextEpisodeEdgeOperations
44+
from graphiti_core.driver.operations.saga_node_ops import SagaNodeOperations
45+
from graphiti_core.driver.operations.search_ops import SearchOperations
46+
3047
logger = logging.getLogger(__name__)
3148

3249
DEFAULT_SIZE = 10
@@ -70,13 +87,14 @@ async def execute_write(self, func, *args, **kwargs):
7087
raise NotImplementedError()
7188

7289

73-
class GraphDriver(ABC):
90+
class GraphDriver(QueryExecutor, ABC):
7491
provider: GraphProvider
7592
fulltext_syntax: str = (
7693
'' # Neo4j (default) syntax does not require a prefix for fulltext queries
7794
)
7895
_database: str
7996
default_group_id: str = ''
97+
# Legacy interfaces (kept for backwards compatibility during Phase 1)
8098
search_interface: SearchInterface | None = None
8199
graph_operations_interface: GraphOperationsInterface | None = None
82100

@@ -96,7 +114,7 @@ def close(self):
96114
def delete_all_indexes(self) -> Coroutine:
97115
raise NotImplementedError()
98116

99-
def with_database(self, database: str) -> 'GraphDriver':
117+
def with_database(self, database: str) -> GraphDriver:
100118
"""
101119
Returns a shallow copy of this driver with a different default database.
102120
Reuses the same connection (e.g. FalkorDB, Neo4j).
@@ -110,7 +128,7 @@ def with_database(self, database: str) -> 'GraphDriver':
110128
async def build_indices_and_constraints(self, delete_existing: bool = False):
111129
raise NotImplementedError()
112130

113-
def clone(self, database: str) -> 'GraphDriver':
131+
def clone(self, database: str) -> GraphDriver:
114132
"""Clone the driver with a different database or graph name."""
115133
return self
116134

@@ -122,3 +140,81 @@ def build_fulltext_query(
122140
Only implemented by providers that need custom fulltext query building.
123141
"""
124142
raise NotImplementedError(f'build_fulltext_query not implemented for {self.provider}')
143+
144+
# --- New operations interfaces ---
145+
146+
@asynccontextmanager
147+
async def transaction(self) -> AsyncIterator[Transaction]:
148+
"""Return a transaction context manager.
149+
150+
Usage::
151+
152+
async with driver.transaction() as tx:
153+
await ops.save(driver, node, tx=tx)
154+
155+
Drivers with real transaction support (e.g., Neo4j) commit on clean exit
156+
and roll back on exception. Drivers without native transactions return a
157+
thin wrapper where queries execute immediately.
158+
159+
The base implementation provides a no-op wrapper using the session. Drivers
160+
should override this to provide real transaction semantics where supported.
161+
"""
162+
session = self.session()
163+
try:
164+
yield _SessionTransaction(session)
165+
finally:
166+
await session.close()
167+
168+
@property
169+
def entity_node_ops(self) -> EntityNodeOperations | None:
170+
return None
171+
172+
@property
173+
def episode_node_ops(self) -> EpisodeNodeOperations | None:
174+
return None
175+
176+
@property
177+
def community_node_ops(self) -> CommunityNodeOperations | None:
178+
return None
179+
180+
@property
181+
def saga_node_ops(self) -> SagaNodeOperations | None:
182+
return None
183+
184+
@property
185+
def entity_edge_ops(self) -> EntityEdgeOperations | None:
186+
return None
187+
188+
@property
189+
def episodic_edge_ops(self) -> EpisodicEdgeOperations | None:
190+
return None
191+
192+
@property
193+
def community_edge_ops(self) -> CommunityEdgeOperations | None:
194+
return None
195+
196+
@property
197+
def has_episode_edge_ops(self) -> HasEpisodeEdgeOperations | None:
198+
return None
199+
200+
@property
201+
def next_episode_edge_ops(self) -> NextEpisodeEdgeOperations | None:
202+
return None
203+
204+
@property
205+
def search_ops(self) -> SearchOperations | None:
206+
return None
207+
208+
@property
209+
def graph_ops(self) -> GraphMaintenanceOperations | None:
210+
return None
211+
212+
213+
class _SessionTransaction(Transaction):
214+
"""Fallback transaction that wraps a session — queries execute immediately."""
215+
216+
def __init__(self, session: GraphDriverSession):
217+
self._session = session
218+
219+
async def run(self, query: str, **kwargs: Any) -> Any:
220+
return await self._session.run(query, **kwargs)

0 commit comments

Comments
 (0)