graphiti-core 0.30.0rc0__tar.gz → 0.30.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of graphiti-core might be problematic. Click here for more details.

Files changed (192) hide show
  1. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/PKG-INFO +1 -1
  2. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/bulk_utils.py +126 -60
  3. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/dedup_helpers.py +6 -1
  4. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/node_operations.py +3 -2
  5. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/pyproject.toml +1 -1
  6. graphiti_core-0.30.0rc1/tests/utils/maintenance/test_bulk_utils.py +232 -0
  7. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/utils/maintenance/test_node_operations.py +4 -0
  8. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/uv.lock +1 -1
  9. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.env.example +0 -0
  10. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/dependabot.yml +0 -0
  12. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/pull_request_template.md +0 -0
  13. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/secret_scanning.yml +0 -0
  14. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/ai-moderator.yml +0 -0
  15. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/cla.yml +0 -0
  16. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/claude-code-review.yml +0 -0
  17. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/claude.yml +0 -0
  18. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/codeql.yml +0 -0
  19. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/lint.yml +0 -0
  20. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/mcp-server-docker.yml +0 -0
  21. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/release-graphiti-core.yml +0 -0
  22. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/typecheck.yml +0 -0
  23. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/unit_tests.yml +0 -0
  24. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.gitignore +0 -0
  25. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/AGENTS.md +0 -0
  26. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/CLAUDE.md +0 -0
  27. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/CODE_OF_CONDUCT.md +0 -0
  28. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/CONTRIBUTING.md +0 -0
  29. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/Dockerfile +0 -0
  30. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/LICENSE +0 -0
  31. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/Makefile +0 -0
  32. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/README.md +0 -0
  33. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/SECURITY.md +0 -0
  34. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/Zep-CLA.md +0 -0
  35. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/conftest.py +0 -0
  36. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/depot.json +0 -0
  37. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/docker-compose.test.yml +0 -0
  38. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/docker-compose.yml +0 -0
  39. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/ellipsis.yaml +0 -0
  40. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/data/manybirds_products.json +0 -0
  41. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/ecommerce/runner.ipynb +0 -0
  42. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/ecommerce/runner.py +0 -0
  43. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/langgraph-agent/agent.ipynb +0 -0
  44. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/langgraph-agent/tinybirds-jess.png +0 -0
  45. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/podcast/podcast_runner.py +0 -0
  46. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/podcast/podcast_transcript.txt +0 -0
  47. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/podcast/transcript_parser.py +0 -0
  48. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/README.md +0 -0
  49. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/quickstart_falkordb.py +0 -0
  50. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/quickstart_neo4j.py +0 -0
  51. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/quickstart_neptune.py +0 -0
  52. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/requirements.txt +0 -0
  53. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/wizard_of_oz/parser.py +0 -0
  54. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/wizard_of_oz/runner.py +0 -0
  55. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/wizard_of_oz/woo.txt +0 -0
  56. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/__init__.py +0 -0
  57. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/__init__.py +0 -0
  58. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/bge_reranker_client.py +0 -0
  59. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/client.py +0 -0
  60. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/gemini_reranker_client.py +0 -0
  61. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/openai_reranker_client.py +0 -0
  62. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/__init__.py +0 -0
  63. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/driver.py +0 -0
  64. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/falkordb_driver.py +0 -0
  65. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/kuzu_driver.py +0 -0
  66. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/neo4j_driver.py +0 -0
  67. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/neptune_driver.py +0 -0
  68. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/edges.py +0 -0
  69. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/__init__.py +0 -0
  70. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/azure_openai.py +0 -0
  71. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/client.py +0 -0
  72. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/gemini.py +0 -0
  73. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/openai.py +0 -0
  74. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/voyage.py +0 -0
  75. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/errors.py +0 -0
  76. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/graph_queries.py +0 -0
  77. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/graphiti.py +0 -0
  78. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/graphiti_types.py +0 -0
  79. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/helpers.py +0 -0
  80. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/__init__.py +0 -0
  81. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/anthropic_client.py +0 -0
  82. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/azure_openai_client.py +0 -0
  83. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/client.py +0 -0
  84. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/config.py +0 -0
  85. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/errors.py +0 -0
  86. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/gemini_client.py +0 -0
  87. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/groq_client.py +0 -0
  88. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/openai_base_client.py +0 -0
  89. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/openai_client.py +0 -0
  90. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/openai_generic_client.py +0 -0
  91. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/utils.py +0 -0
  92. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/migrations/__init__.py +0 -0
  93. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/__init__.py +0 -0
  94. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/edges/__init__.py +0 -0
  95. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/edges/edge_db_queries.py +0 -0
  96. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/nodes/__init__.py +0 -0
  97. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/nodes/node_db_queries.py +0 -0
  98. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/nodes.py +0 -0
  99. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/__init__.py +0 -0
  100. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/dedupe_edges.py +0 -0
  101. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/dedupe_nodes.py +0 -0
  102. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/eval.py +0 -0
  103. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/extract_edge_dates.py +0 -0
  104. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/extract_edges.py +0 -0
  105. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/extract_nodes.py +0 -0
  106. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/invalidate_edges.py +0 -0
  107. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/lib.py +0 -0
  108. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/models.py +0 -0
  109. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/prompt_helpers.py +0 -0
  110. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/summarize_nodes.py +0 -0
  111. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/py.typed +0 -0
  112. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/__init__.py +0 -0
  113. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search.py +0 -0
  114. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_config.py +0 -0
  115. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_config_recipes.py +0 -0
  116. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_filters.py +0 -0
  117. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_helpers.py +0 -0
  118. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_utils.py +0 -0
  119. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/telemetry/__init__.py +0 -0
  120. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/telemetry/telemetry.py +0 -0
  121. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/__init__.py +0 -0
  122. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/datetime_utils.py +0 -0
  123. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/__init__.py +0 -0
  124. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/community_operations.py +0 -0
  125. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/edge_operations.py +0 -0
  126. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/graph_data_operations.py +0 -0
  127. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/temporal_operations.py +0 -0
  128. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/utils.py +0 -0
  129. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/ontology_utils/entity_types_utils.py +0 -0
  130. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/images/arxiv-screenshot.png +0 -0
  131. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/images/graphiti-graph-intro.gif +0 -0
  132. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/images/graphiti-intro-slides-stock-2.gif +0 -0
  133. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/images/simple_graph.svg +0 -0
  134. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/.env.example +0 -0
  135. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/.python-version +0 -0
  136. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/Dockerfile +0 -0
  137. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/README.md +0 -0
  138. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/cursor_rules.md +0 -0
  139. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/docker-compose.yml +0 -0
  140. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/graphiti_mcp_server.py +0 -0
  141. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/mcp_config_sse_example.json +0 -0
  142. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/mcp_config_stdio_example.json +0 -0
  143. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/pyproject.toml +0 -0
  144. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/uv.lock +0 -0
  145. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/poetry.lock +0 -0
  146. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/py.typed +0 -0
  147. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/pytest.ini +0 -0
  148. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/.env.example +0 -0
  149. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/Makefile +0 -0
  150. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/README.md +0 -0
  151. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/__init__.py +0 -0
  152. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/config.py +0 -0
  153. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/dto/__init__.py +0 -0
  154. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/dto/common.py +0 -0
  155. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/dto/ingest.py +0 -0
  156. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/dto/retrieve.py +0 -0
  157. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/main.py +0 -0
  158. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/routers/__init__.py +0 -0
  159. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/routers/ingest.py +0 -0
  160. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/routers/retrieve.py +0 -0
  161. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/zep_graphiti.py +0 -0
  162. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/pyproject.toml +0 -0
  163. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/uv.lock +0 -0
  164. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/signatures/version1/cla.json +0 -0
  165. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/cross_encoder/test_bge_reranker_client.py +0 -0
  166. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/cross_encoder/test_gemini_reranker_client.py +0 -0
  167. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/driver/__init__.py +0 -0
  168. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/driver/test_falkordb_driver.py +0 -0
  169. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/embedder/embedder_fixtures.py +0 -0
  170. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/embedder/test_gemini.py +0 -0
  171. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/embedder/test_openai.py +0 -0
  172. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/embedder/test_voyage.py +0 -0
  173. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/data/longmemeval_data/README.md +0 -0
  174. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/data/longmemeval_data/longmemeval_oracle.json +0 -0
  175. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/eval_cli.py +0 -0
  176. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/eval_e2e_graph_building.py +0 -0
  177. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/pytest.ini +0 -0
  178. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/utils.py +0 -0
  179. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/helpers_test.py +0 -0
  180. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_anthropic_client.py +0 -0
  181. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_anthropic_client_int.py +0 -0
  182. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_client.py +0 -0
  183. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_errors.py +0 -0
  184. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_gemini_client.py +0 -0
  185. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_edge_int.py +0 -0
  186. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_entity_exclusion_int.py +0 -0
  187. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_graphiti_int.py +0 -0
  188. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_graphiti_mock.py +0 -0
  189. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_node_int.py +0 -0
  190. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/utils/maintenance/test_edge_operations.py +0 -0
  191. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/utils/maintenance/test_temporal_operations_int.py +0 -0
  192. {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/utils/search/search_utils_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphiti-core
3
- Version: 0.30.0rc0
3
+ Version: 0.30.0rc1
4
4
  Summary: A temporal graph building library
5
5
  Project-URL: Homepage, https://help.getzep.com/graphiti/graphiti/overview
6
6
  Project-URL: Repository, https://github.com/getzep/graphiti
@@ -43,8 +43,14 @@ from graphiti_core.models.nodes.node_db_queries import (
43
43
  get_entity_node_save_bulk_query,
44
44
  get_episode_node_save_bulk_query,
45
45
  )
46
- from graphiti_core.nodes import EntityNode, EpisodeType, EpisodicNode, create_entity_node_embeddings
46
+ from graphiti_core.nodes import EntityNode, EpisodeType, EpisodicNode
47
47
  from graphiti_core.utils.datetime_utils import convert_datetimes_to_strings
48
+ from graphiti_core.utils.maintenance.dedup_helpers import (
49
+ DedupResolutionState,
50
+ _build_candidate_indexes,
51
+ _normalize_string_exact,
52
+ _resolve_with_similarity,
53
+ )
48
54
  from graphiti_core.utils.maintenance.edge_operations import (
49
55
  extract_edges,
50
56
  resolve_extracted_edge,
@@ -63,6 +69,38 @@ logger = logging.getLogger(__name__)
63
69
  CHUNK_SIZE = 10
64
70
 
65
71
 
72
+ def _build_directed_uuid_map(pairs: list[tuple[str, str]]) -> dict[str, str]:
73
+ """Collapse alias -> canonical chains while preserving direction.
74
+
75
+ The incoming pairs represent directed mappings discovered during node dedupe. We use a simple
76
+ union-find with iterative path compression to ensure every source UUID resolves to its ultimate
77
+ canonical target, even if aliases appear lexicographically smaller than the canonical UUID.
78
+ """
79
+
80
+ parent: dict[str, str] = {}
81
+
82
+ def find(uuid: str) -> str:
83
+ """Directed union-find lookup using iterative path compression."""
84
+ parent.setdefault(uuid, uuid)
85
+ root = uuid
86
+ while parent[root] != root:
87
+ root = parent[root]
88
+
89
+ while parent[uuid] != root:
90
+ next_uuid = parent[uuid]
91
+ parent[uuid] = root
92
+ uuid = next_uuid
93
+
94
+ return root
95
+
96
+ for source_uuid, target_uuid in pairs:
97
+ parent.setdefault(source_uuid, source_uuid)
98
+ parent.setdefault(target_uuid, target_uuid)
99
+ parent[find(source_uuid)] = find(target_uuid)
100
+
101
+ return {uuid: find(uuid) for uuid in parent}
102
+
103
+
66
104
  class RawEpisode(BaseModel):
67
105
  name: str
68
106
  uuid: str | None = Field(default=None)
@@ -266,83 +304,111 @@ async def dedupe_nodes_bulk(
266
304
  episode_tuples: list[tuple[EpisodicNode, list[EpisodicNode]]],
267
305
  entity_types: dict[str, type[BaseModel]] | None = None,
268
306
  ) -> tuple[dict[str, list[EntityNode]], dict[str, str]]:
269
- embedder = clients.embedder
270
- min_score = 0.8
271
-
272
- # generate embeddings
273
- await semaphore_gather(
274
- *[create_entity_node_embeddings(embedder, nodes) for nodes in extracted_nodes]
275
- )
276
-
277
- # Find similar results
278
- dedupe_tuples: list[tuple[list[EntityNode], list[EntityNode]]] = []
279
- for i, nodes_i in enumerate(extracted_nodes):
280
- existing_nodes: list[EntityNode] = []
281
- for j, nodes_j in enumerate(extracted_nodes):
282
- if i == j:
283
- continue
284
- existing_nodes += nodes_j
285
-
286
- candidates_i: list[EntityNode] = []
287
- for node in nodes_i:
288
- for existing_node in existing_nodes:
289
- # Approximate BM25 by checking for word overlaps (this is faster than creating many in-memory indices)
290
- # This approach will cast a wider net than BM25, which is ideal for this use case
291
- node_words = set(node.name.lower().split())
292
- existing_node_words = set(existing_node.name.lower().split())
293
- has_overlap = not node_words.isdisjoint(existing_node_words)
294
- if has_overlap:
295
- candidates_i.append(existing_node)
296
- continue
307
+ """Resolve entity duplicates across an in-memory batch using a two-pass strategy.
297
308
 
298
- # Check for semantic similarity even if there is no overlap
299
- similarity = np.dot(
300
- normalize_l2(node.name_embedding or []),
301
- normalize_l2(existing_node.name_embedding or []),
302
- )
303
- if similarity >= min_score:
304
- candidates_i.append(existing_node)
305
-
306
- dedupe_tuples.append((nodes_i, candidates_i))
309
+ 1. Run :func:`resolve_extracted_nodes` for every episode in parallel so each batch item is
310
+ reconciled against the live graph just like the non-batch flow.
311
+ 2. Re-run the deterministic similarity heuristics across the union of resolved nodes to catch
312
+ duplicates that only co-occur inside this batch, emitting a canonical UUID map that callers
313
+ can apply to edges and persistence.
314
+ """
307
315
 
308
- # Determine Node Resolutions
309
- bulk_node_resolutions: list[
310
- tuple[list[EntityNode], dict[str, str], list[tuple[EntityNode, EntityNode]]]
311
- ] = await semaphore_gather(
316
+ first_pass_results = await semaphore_gather(
312
317
  *[
313
318
  resolve_extracted_nodes(
314
319
  clients,
315
- dedupe_tuple[0],
320
+ nodes,
316
321
  episode_tuples[i][0],
317
322
  episode_tuples[i][1],
318
323
  entity_types,
319
- existing_nodes_override=dedupe_tuples[i][1],
320
324
  )
321
- for i, dedupe_tuple in enumerate(dedupe_tuples)
325
+ for i, nodes in enumerate(extracted_nodes)
322
326
  ]
323
327
  )
324
328
 
325
- # Collect all duplicate pairs sorted by uuid
329
+ episode_resolutions: list[tuple[str, list[EntityNode]]] = []
330
+ per_episode_uuid_maps: list[dict[str, str]] = []
326
331
  duplicate_pairs: list[tuple[str, str]] = []
327
- for _, _, duplicates in bulk_node_resolutions:
328
- for duplicate in duplicates:
329
- n, m = duplicate
330
- duplicate_pairs.append((n.uuid, m.uuid))
331
332
 
332
- # Now we compress the duplicate_map, so that 3 -> 2 and 2 -> becomes 3 -> 1 (sorted by uuid)
333
- compressed_map: dict[str, str] = compress_uuid_map(duplicate_pairs)
333
+ for (resolved_nodes, uuid_map, duplicates), (episode, _) in zip(
334
+ first_pass_results, episode_tuples, strict=True
335
+ ):
336
+ episode_resolutions.append((episode.uuid, resolved_nodes))
337
+ per_episode_uuid_maps.append(uuid_map)
338
+ duplicate_pairs.extend((source.uuid, target.uuid) for source, target in duplicates)
339
+
340
+ canonical_nodes: dict[str, EntityNode] = {}
341
+ for _, resolved_nodes in episode_resolutions:
342
+ for node in resolved_nodes:
343
+ # NOTE: this loop is O(n^2) in the number of nodes inside the batch because we rebuild
344
+ # the MinHash index for the accumulated canonical pool each time. The LRU-backed
345
+ # shingle cache keeps the constant factors low for typical batch sizes (≤ CHUNK_SIZE),
346
+ # but if batches grow significantly we should switch to an incremental index or chunked
347
+ # processing.
348
+ if not canonical_nodes:
349
+ canonical_nodes[node.uuid] = node
350
+ continue
334
351
 
335
- node_uuid_map: dict[str, EntityNode] = {
336
- node.uuid: node for nodes in extracted_nodes for node in nodes
337
- }
352
+ existing_candidates = list(canonical_nodes.values())
353
+ normalized = _normalize_string_exact(node.name)
354
+ exact_match = next(
355
+ (
356
+ candidate
357
+ for candidate in existing_candidates
358
+ if _normalize_string_exact(candidate.name) == normalized
359
+ ),
360
+ None,
361
+ )
362
+ if exact_match is not None:
363
+ if exact_match.uuid != node.uuid:
364
+ duplicate_pairs.append((node.uuid, exact_match.uuid))
365
+ continue
366
+
367
+ indexes = _build_candidate_indexes(existing_candidates)
368
+ state = DedupResolutionState(
369
+ resolved_nodes=[None],
370
+ uuid_map={},
371
+ unresolved_indices=[],
372
+ )
373
+ _resolve_with_similarity([node], indexes, state)
374
+
375
+ resolved = state.resolved_nodes[0]
376
+ if resolved is None:
377
+ canonical_nodes[node.uuid] = node
378
+ continue
379
+
380
+ canonical_uuid = resolved.uuid
381
+ canonical_nodes.setdefault(canonical_uuid, resolved)
382
+ if canonical_uuid != node.uuid:
383
+ duplicate_pairs.append((node.uuid, canonical_uuid))
384
+
385
+ union_pairs: list[tuple[str, str]] = []
386
+ for uuid_map in per_episode_uuid_maps:
387
+ union_pairs.extend(uuid_map.items())
388
+ union_pairs.extend(duplicate_pairs)
389
+
390
+ compressed_map: dict[str, str] = _build_directed_uuid_map(union_pairs)
338
391
 
339
392
  nodes_by_episode: dict[str, list[EntityNode]] = {}
340
- for i, nodes in enumerate(extracted_nodes):
341
- episode = episode_tuples[i][0]
393
+ for episode_uuid, resolved_nodes in episode_resolutions:
394
+ deduped_nodes: list[EntityNode] = []
395
+ seen: set[str] = set()
396
+ for node in resolved_nodes:
397
+ canonical_uuid = compressed_map.get(node.uuid, node.uuid)
398
+ if canonical_uuid in seen:
399
+ continue
400
+ seen.add(canonical_uuid)
401
+ canonical_node = canonical_nodes.get(canonical_uuid)
402
+ if canonical_node is None:
403
+ logger.error(
404
+ 'Canonical node %s missing during batch dedupe; falling back to %s',
405
+ canonical_uuid,
406
+ node.uuid,
407
+ )
408
+ canonical_node = node
409
+ deduped_nodes.append(canonical_node)
342
410
 
343
- nodes_by_episode[episode.uuid] = [
344
- node_uuid_map[compressed_map.get(node.uuid, node.uuid)] for node in nodes
345
- ]
411
+ nodes_by_episode[episode_uuid] = deduped_nodes
346
412
 
347
413
  return nodes_by_episode, compressed_map
348
414
 
@@ -20,7 +20,7 @@ import math
20
20
  import re
21
21
  from collections import defaultdict
22
22
  from collections.abc import Iterable
23
- from dataclasses import dataclass
23
+ from dataclasses import dataclass, field
24
24
  from functools import lru_cache
25
25
  from hashlib import blake2b
26
26
  from typing import TYPE_CHECKING
@@ -164,6 +164,7 @@ class DedupResolutionState:
164
164
  resolved_nodes: list[EntityNode | None]
165
165
  uuid_map: dict[str, str]
166
166
  unresolved_indices: list[int]
167
+ duplicate_pairs: list[tuple[EntityNode, EntityNode]] = field(default_factory=list)
167
168
 
168
169
 
169
170
  def _build_candidate_indexes(existing_nodes: list[EntityNode]) -> DedupCandidateIndexes:
@@ -213,6 +214,8 @@ def _resolve_with_similarity(
213
214
  match = existing_matches[0]
214
215
  state.resolved_nodes[idx] = match
215
216
  state.uuid_map[node.uuid] = match.uuid
217
+ if match.uuid != node.uuid:
218
+ state.duplicate_pairs.append((node, match))
216
219
  continue
217
220
  if len(existing_matches) > 1:
218
221
  state.unresolved_indices.append(idx)
@@ -236,6 +239,8 @@ def _resolve_with_similarity(
236
239
  if best_candidate is not None and best_score >= _FUZZY_JACCARD_THRESHOLD:
237
240
  state.resolved_nodes[idx] = best_candidate
238
241
  state.uuid_map[node.uuid] = best_candidate.uuid
242
+ if best_candidate.uuid != node.uuid:
243
+ state.duplicate_pairs.append((node, best_candidate))
239
244
  continue
240
245
 
241
246
  state.unresolved_indices.append(idx)
@@ -306,6 +306,8 @@ async def _resolve_with_llm(
306
306
 
307
307
  state.resolved_nodes[original_index] = resolved_node
308
308
  state.uuid_map[extracted_node.uuid] = resolved_node.uuid
309
+ if resolved_node.uuid != extracted_node.uuid:
310
+ state.duplicate_pairs.append((extracted_node, resolved_node))
309
311
 
310
312
 
311
313
  async def resolve_extracted_nodes(
@@ -332,7 +334,6 @@ async def resolve_extracted_nodes(
332
334
  uuid_map={},
333
335
  unresolved_indices=[],
334
336
  )
335
- node_duplicates: list[tuple[EntityNode, EntityNode]] = []
336
337
 
337
338
  _resolve_with_similarity(extracted_nodes, indexes, state)
338
339
 
@@ -359,7 +360,7 @@ async def resolve_extracted_nodes(
359
360
 
360
361
  new_node_duplicates: list[
361
362
  tuple[EntityNode, EntityNode]
362
- ] = await filter_existing_duplicate_of_edges(driver, node_duplicates)
363
+ ] = await filter_existing_duplicate_of_edges(driver, state.duplicate_pairs)
363
364
 
364
365
  return (
365
366
  [node for node in state.resolved_nodes if node is not None],
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "graphiti-core"
3
3
  description = "A temporal graph building library"
4
- version = "0.30.0pre0"
4
+ version = "0.30.0pre1"
5
5
  authors = [
6
6
  { name = "Paul Paliychuk", email = "paul@getzep.com" },
7
7
  { name = "Preston Rasmussen", email = "preston@getzep.com" },
@@ -0,0 +1,232 @@
1
+ from collections import deque
2
+ from unittest.mock import AsyncMock, MagicMock
3
+
4
+ import pytest
5
+
6
+ from graphiti_core.edges import EntityEdge
7
+ from graphiti_core.graphiti_types import GraphitiClients
8
+ from graphiti_core.nodes import EntityNode, EpisodeType, EpisodicNode
9
+ from graphiti_core.utils import bulk_utils
10
+ from graphiti_core.utils.datetime_utils import utc_now
11
+
12
+
13
+ def _make_episode(uuid_suffix: str, group_id: str = 'group') -> EpisodicNode:
14
+ return EpisodicNode(
15
+ name=f'episode-{uuid_suffix}',
16
+ group_id=group_id,
17
+ labels=[],
18
+ source=EpisodeType.message,
19
+ content='content',
20
+ source_description='test',
21
+ created_at=utc_now(),
22
+ valid_at=utc_now(),
23
+ )
24
+
25
+
26
+ def _make_clients() -> GraphitiClients:
27
+ driver = MagicMock()
28
+ embedder = MagicMock()
29
+ cross_encoder = MagicMock()
30
+ llm_client = MagicMock()
31
+
32
+ return GraphitiClients.model_construct( # bypass validation to allow test doubles
33
+ driver=driver,
34
+ embedder=embedder,
35
+ cross_encoder=cross_encoder,
36
+ llm_client=llm_client,
37
+ ensure_ascii=False,
38
+ )
39
+
40
+
41
+ @pytest.mark.asyncio
42
+ async def test_dedupe_nodes_bulk_reuses_canonical_nodes(monkeypatch):
43
+ clients = _make_clients()
44
+
45
+ episode_one = _make_episode('1')
46
+ episode_two = _make_episode('2')
47
+
48
+ extracted_one = EntityNode(name='Alice Smith', group_id='group', labels=['Entity'])
49
+ extracted_two = EntityNode(name='Alice Smith', group_id='group', labels=['Entity'])
50
+
51
+ canonical = extracted_one
52
+
53
+ call_queue = deque()
54
+
55
+ async def fake_resolve(
56
+ clients_arg,
57
+ nodes_arg,
58
+ episode_arg,
59
+ previous_episodes_arg,
60
+ entity_types_arg,
61
+ existing_nodes_override=None,
62
+ ):
63
+ call_queue.append(existing_nodes_override)
64
+
65
+ if nodes_arg == [extracted_one]:
66
+ return [canonical], {canonical.uuid: canonical.uuid}, []
67
+
68
+ assert nodes_arg == [extracted_two]
69
+ assert existing_nodes_override is None
70
+
71
+ return [canonical], {extracted_two.uuid: canonical.uuid}, [(extracted_two, canonical)]
72
+
73
+ monkeypatch.setattr(bulk_utils, 'resolve_extracted_nodes', fake_resolve)
74
+
75
+ nodes_by_episode, compressed_map = await bulk_utils.dedupe_nodes_bulk(
76
+ clients,
77
+ [[extracted_one], [extracted_two]],
78
+ [(episode_one, []), (episode_two, [])],
79
+ )
80
+
81
+ assert len(call_queue) == 2
82
+ assert call_queue[0] is None
83
+ assert call_queue[1] is None
84
+
85
+ assert nodes_by_episode[episode_one.uuid] == [canonical]
86
+ assert nodes_by_episode[episode_two.uuid] == [canonical]
87
+ assert compressed_map.get(extracted_two.uuid) == canonical.uuid
88
+
89
+
90
+ @pytest.mark.asyncio
91
+ async def test_dedupe_nodes_bulk_handles_empty_batch(monkeypatch):
92
+ clients = _make_clients()
93
+
94
+ resolve_mock = AsyncMock()
95
+ monkeypatch.setattr(bulk_utils, 'resolve_extracted_nodes', resolve_mock)
96
+
97
+ nodes_by_episode, compressed_map = await bulk_utils.dedupe_nodes_bulk(
98
+ clients,
99
+ [],
100
+ [],
101
+ )
102
+
103
+ assert nodes_by_episode == {}
104
+ assert compressed_map == {}
105
+ resolve_mock.assert_not_awaited()
106
+
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_dedupe_nodes_bulk_single_episode(monkeypatch):
110
+ clients = _make_clients()
111
+
112
+ episode = _make_episode('solo')
113
+ extracted = EntityNode(name='Solo', group_id='group', labels=['Entity'])
114
+
115
+ resolve_mock = AsyncMock(return_value=([extracted], {extracted.uuid: extracted.uuid}, []))
116
+ monkeypatch.setattr(bulk_utils, 'resolve_extracted_nodes', resolve_mock)
117
+
118
+ nodes_by_episode, compressed_map = await bulk_utils.dedupe_nodes_bulk(
119
+ clients,
120
+ [[extracted]],
121
+ [(episode, [])],
122
+ )
123
+
124
+ assert nodes_by_episode == {episode.uuid: [extracted]}
125
+ assert compressed_map == {extracted.uuid: extracted.uuid}
126
+ resolve_mock.assert_awaited_once()
127
+
128
+
129
+ @pytest.mark.asyncio
130
+ async def test_dedupe_nodes_bulk_uuid_map_respects_direction(monkeypatch):
131
+ clients = _make_clients()
132
+
133
+ episode_one = _make_episode('one')
134
+ episode_two = _make_episode('two')
135
+
136
+ extracted_one = EntityNode(uuid='b-uuid', name='Edge Case', group_id='group', labels=['Entity'])
137
+ extracted_two = EntityNode(uuid='a-uuid', name='Edge Case', group_id='group', labels=['Entity'])
138
+
139
+ canonical = extracted_one
140
+ alias = extracted_two
141
+
142
+ async def fake_resolve(
143
+ clients_arg,
144
+ nodes_arg,
145
+ episode_arg,
146
+ previous_episodes_arg,
147
+ entity_types_arg,
148
+ existing_nodes_override=None,
149
+ ):
150
+ if nodes_arg == [extracted_one]:
151
+ return [canonical], {canonical.uuid: canonical.uuid}, []
152
+ assert nodes_arg == [extracted_two]
153
+ return [canonical], {alias.uuid: canonical.uuid}, [(alias, canonical)]
154
+
155
+ monkeypatch.setattr(bulk_utils, 'resolve_extracted_nodes', fake_resolve)
156
+
157
+ nodes_by_episode, compressed_map = await bulk_utils.dedupe_nodes_bulk(
158
+ clients,
159
+ [[extracted_one], [extracted_two]],
160
+ [(episode_one, []), (episode_two, [])],
161
+ )
162
+
163
+ assert nodes_by_episode[episode_one.uuid] == [canonical]
164
+ assert nodes_by_episode[episode_two.uuid] == [canonical]
165
+ assert compressed_map.get(alias.uuid) == canonical.uuid
166
+
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_dedupe_nodes_bulk_missing_canonical_falls_back(monkeypatch, caplog):
170
+ clients = _make_clients()
171
+
172
+ episode = _make_episode('missing')
173
+ extracted = EntityNode(name='Fallback', group_id='group', labels=['Entity'])
174
+
175
+ resolve_mock = AsyncMock(return_value=([extracted], {extracted.uuid: 'missing-canonical'}, []))
176
+ monkeypatch.setattr(bulk_utils, 'resolve_extracted_nodes', resolve_mock)
177
+
178
+ with caplog.at_level('WARNING'):
179
+ nodes_by_episode, compressed_map = await bulk_utils.dedupe_nodes_bulk(
180
+ clients,
181
+ [[extracted]],
182
+ [(episode, [])],
183
+ )
184
+
185
+ assert nodes_by_episode[episode.uuid] == [extracted]
186
+ assert compressed_map.get(extracted.uuid) == 'missing-canonical'
187
+ assert any('Canonical node missing' in rec.message for rec in caplog.records)
188
+
189
+
190
+ def test_build_directed_uuid_map_empty():
191
+ assert bulk_utils._build_directed_uuid_map([]) == {}
192
+
193
+
194
+ def test_build_directed_uuid_map_chain():
195
+ mapping = bulk_utils._build_directed_uuid_map(
196
+ [
197
+ ('a', 'b'),
198
+ ('b', 'c'),
199
+ ]
200
+ )
201
+
202
+ assert mapping['a'] == 'c'
203
+ assert mapping['b'] == 'c'
204
+ assert mapping['c'] == 'c'
205
+
206
+
207
+ def test_build_directed_uuid_map_preserves_direction():
208
+ mapping = bulk_utils._build_directed_uuid_map(
209
+ [
210
+ ('alias', 'canonical'),
211
+ ]
212
+ )
213
+
214
+ assert mapping['alias'] == 'canonical'
215
+ assert mapping['canonical'] == 'canonical'
216
+
217
+
218
+ def test_resolve_edge_pointers_updates_sources():
219
+ created_at = utc_now()
220
+ edge = EntityEdge(
221
+ name='knows',
222
+ fact='fact',
223
+ group_id='group',
224
+ source_node_uuid='alias',
225
+ target_node_uuid='target',
226
+ created_at=created_at,
227
+ )
228
+
229
+ bulk_utils.resolve_edge_pointers([edge], {'alias': 'canonical'})
230
+
231
+ assert edge.source_node_uuid == 'canonical'
232
+ assert edge.target_node_uuid == 'target'
@@ -257,6 +257,7 @@ def test_resolve_with_similarity_exact_match_updates_state():
257
257
  assert state.resolved_nodes[0].uuid == candidate.uuid
258
258
  assert state.uuid_map[extracted.uuid] == candidate.uuid
259
259
  assert state.unresolved_indices == []
260
+ assert state.duplicate_pairs == [(extracted, candidate)]
260
261
 
261
262
 
262
263
  def test_resolve_with_similarity_low_entropy_defers_resolution():
@@ -274,6 +275,7 @@ def test_resolve_with_similarity_low_entropy_defers_resolution():
274
275
 
275
276
  assert state.resolved_nodes[0] is None
276
277
  assert state.unresolved_indices == [0]
278
+ assert state.duplicate_pairs == []
277
279
 
278
280
 
279
281
  def test_resolve_with_similarity_multiple_exact_matches_defers_to_llm():
@@ -288,6 +290,7 @@ def test_resolve_with_similarity_multiple_exact_matches_defers_to_llm():
288
290
 
289
291
  assert state.resolved_nodes[0] is None
290
292
  assert state.unresolved_indices == [0]
293
+ assert state.duplicate_pairs == []
291
294
 
292
295
 
293
296
  @pytest.mark.asyncio
@@ -339,3 +342,4 @@ async def test_resolve_with_llm_updates_unresolved(monkeypatch):
339
342
  assert state.uuid_map[extracted.uuid] == candidate.uuid
340
343
  assert captured_context['existing_nodes'][0]['idx'] == 0
341
344
  assert isinstance(captured_context['existing_nodes'], list)
345
+ assert state.duplicate_pairs == [(extracted, candidate)]
@@ -783,7 +783,7 @@ wheels = [
783
783
 
784
784
  [[package]]
785
785
  name = "graphiti-core"
786
- version = "0.30.0rc0"
786
+ version = "0.30.0rc1"
787
787
  source = { editable = "." }
788
788
  dependencies = [
789
789
  { name = "diskcache" },