graphiti-core 0.21.0rc9__tar.gz → 0.21.0rc11__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 (193) hide show
  1. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/claude-code-review.yml +3 -0
  2. graphiti_core-0.21.0rc11/.github/workflows/daily_issue_maintenance.yml +123 -0
  3. graphiti_core-0.21.0rc11/.github/workflows/issue-triage.yml +140 -0
  4. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/PKG-INFO +1 -1
  5. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/edges.py +6 -3
  6. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/nodes.py +6 -3
  7. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/dedupe_nodes.py +2 -1
  8. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/extract_nodes.py +1 -1
  9. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/maintenance/node_operations.py +65 -9
  10. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/pyproject.toml +1 -1
  11. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/utils/maintenance/test_node_operations.py +182 -0
  12. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/uv.lock +2 -2
  13. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.env.example +0 -0
  14. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/dependabot.yml +0 -0
  16. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/pull_request_template.md +0 -0
  17. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/secret_scanning.yml +0 -0
  18. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/ai-moderator.yml +0 -0
  19. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/cla.yml +0 -0
  20. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/claude.yml +0 -0
  21. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/codeql.yml +0 -0
  22. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/lint.yml +0 -0
  23. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/mcp-server-docker.yml +0 -0
  24. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/release-graphiti-core.yml +0 -0
  25. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/typecheck.yml +0 -0
  26. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.github/workflows/unit_tests.yml +0 -0
  27. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/.gitignore +0 -0
  28. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/AGENTS.md +0 -0
  29. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/CLAUDE.md +0 -0
  30. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/CODE_OF_CONDUCT.md +0 -0
  31. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/CONTRIBUTING.md +0 -0
  32. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/Dockerfile +0 -0
  33. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/LICENSE +0 -0
  34. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/Makefile +0 -0
  35. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/README.md +0 -0
  36. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/SECURITY.md +0 -0
  37. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/Zep-CLA.md +0 -0
  38. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/conftest.py +0 -0
  39. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/depot.json +0 -0
  40. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/docker-compose.test.yml +0 -0
  41. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/docker-compose.yml +0 -0
  42. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/ellipsis.yaml +0 -0
  43. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/data/manybirds_products.json +0 -0
  44. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/ecommerce/runner.ipynb +0 -0
  45. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/ecommerce/runner.py +0 -0
  46. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/langgraph-agent/agent.ipynb +0 -0
  47. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/langgraph-agent/tinybirds-jess.png +0 -0
  48. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/podcast/podcast_runner.py +0 -0
  49. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/podcast/podcast_transcript.txt +0 -0
  50. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/podcast/transcript_parser.py +0 -0
  51. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/quickstart/README.md +0 -0
  52. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/quickstart/quickstart_falkordb.py +0 -0
  53. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/quickstart/quickstart_neo4j.py +0 -0
  54. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/quickstart/quickstart_neptune.py +0 -0
  55. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/quickstart/requirements.txt +0 -0
  56. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/wizard_of_oz/parser.py +0 -0
  57. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/wizard_of_oz/runner.py +0 -0
  58. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/examples/wizard_of_oz/woo.txt +0 -0
  59. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/__init__.py +0 -0
  60. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/cross_encoder/__init__.py +0 -0
  61. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/cross_encoder/bge_reranker_client.py +0 -0
  62. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/cross_encoder/client.py +0 -0
  63. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/cross_encoder/gemini_reranker_client.py +0 -0
  64. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/cross_encoder/openai_reranker_client.py +0 -0
  65. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/driver/__init__.py +0 -0
  66. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/driver/driver.py +0 -0
  67. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/driver/falkordb_driver.py +0 -0
  68. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/driver/kuzu_driver.py +0 -0
  69. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/driver/neo4j_driver.py +0 -0
  70. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/driver/neptune_driver.py +0 -0
  71. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/embedder/__init__.py +0 -0
  72. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/embedder/azure_openai.py +0 -0
  73. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/embedder/client.py +0 -0
  74. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/embedder/gemini.py +0 -0
  75. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/embedder/openai.py +0 -0
  76. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/embedder/voyage.py +0 -0
  77. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/errors.py +0 -0
  78. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/graph_queries.py +0 -0
  79. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/graphiti.py +0 -0
  80. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/graphiti_types.py +0 -0
  81. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/helpers.py +0 -0
  82. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/__init__.py +0 -0
  83. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/anthropic_client.py +0 -0
  84. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/azure_openai_client.py +0 -0
  85. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/client.py +0 -0
  86. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/config.py +0 -0
  87. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/errors.py +0 -0
  88. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/gemini_client.py +0 -0
  89. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/groq_client.py +0 -0
  90. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/openai_base_client.py +0 -0
  91. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/openai_client.py +0 -0
  92. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/openai_generic_client.py +0 -0
  93. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/llm_client/utils.py +0 -0
  94. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/migrations/__init__.py +0 -0
  95. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/models/__init__.py +0 -0
  96. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/models/edges/__init__.py +0 -0
  97. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/models/edges/edge_db_queries.py +0 -0
  98. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/models/nodes/__init__.py +0 -0
  99. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/models/nodes/node_db_queries.py +0 -0
  100. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/__init__.py +0 -0
  101. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/dedupe_edges.py +0 -0
  102. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/eval.py +0 -0
  103. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/extract_edge_dates.py +0 -0
  104. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/extract_edges.py +0 -0
  105. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/invalidate_edges.py +0 -0
  106. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/lib.py +0 -0
  107. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/models.py +0 -0
  108. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/prompt_helpers.py +0 -0
  109. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/prompts/summarize_nodes.py +0 -0
  110. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/py.typed +0 -0
  111. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/search/__init__.py +0 -0
  112. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/search/search.py +0 -0
  113. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/search/search_config.py +0 -0
  114. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/search/search_config_recipes.py +0 -0
  115. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/search/search_filters.py +0 -0
  116. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/search/search_helpers.py +0 -0
  117. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/search/search_utils.py +0 -0
  118. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/telemetry/__init__.py +0 -0
  119. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/telemetry/telemetry.py +0 -0
  120. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/__init__.py +0 -0
  121. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/bulk_utils.py +0 -0
  122. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/datetime_utils.py +0 -0
  123. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/maintenance/__init__.py +0 -0
  124. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/maintenance/community_operations.py +0 -0
  125. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/maintenance/dedup_helpers.py +0 -0
  126. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/maintenance/edge_operations.py +0 -0
  127. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/maintenance/graph_data_operations.py +0 -0
  128. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/maintenance/temporal_operations.py +0 -0
  129. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/maintenance/utils.py +0 -0
  130. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/graphiti_core/utils/ontology_utils/entity_types_utils.py +0 -0
  131. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/images/arxiv-screenshot.png +0 -0
  132. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/images/graphiti-graph-intro.gif +0 -0
  133. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/images/graphiti-intro-slides-stock-2.gif +0 -0
  134. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/images/simple_graph.svg +0 -0
  135. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/.env.example +0 -0
  136. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/.python-version +0 -0
  137. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/Dockerfile +0 -0
  138. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/README.md +0 -0
  139. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/cursor_rules.md +0 -0
  140. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/docker-compose.yml +0 -0
  141. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/graphiti_mcp_server.py +0 -0
  142. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/mcp_config_sse_example.json +0 -0
  143. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/mcp_config_stdio_example.json +0 -0
  144. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/pyproject.toml +0 -0
  145. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/mcp_server/uv.lock +0 -0
  146. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/py.typed +0 -0
  147. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/pytest.ini +0 -0
  148. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/.env.example +0 -0
  149. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/Makefile +0 -0
  150. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/README.md +0 -0
  151. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/__init__.py +0 -0
  152. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/config.py +0 -0
  153. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/dto/__init__.py +0 -0
  154. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/dto/common.py +0 -0
  155. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/dto/ingest.py +0 -0
  156. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/dto/retrieve.py +0 -0
  157. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/main.py +0 -0
  158. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/routers/__init__.py +0 -0
  159. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/routers/ingest.py +0 -0
  160. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/routers/retrieve.py +0 -0
  161. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/graph_service/zep_graphiti.py +0 -0
  162. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/pyproject.toml +0 -0
  163. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/server/uv.lock +0 -0
  164. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/signatures/version1/cla.json +0 -0
  165. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/cross_encoder/test_bge_reranker_client.py +0 -0
  166. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/cross_encoder/test_gemini_reranker_client.py +0 -0
  167. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/driver/__init__.py +0 -0
  168. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/driver/test_falkordb_driver.py +0 -0
  169. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/embedder/embedder_fixtures.py +0 -0
  170. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/embedder/test_gemini.py +0 -0
  171. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/embedder/test_openai.py +0 -0
  172. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/embedder/test_voyage.py +0 -0
  173. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/evals/data/longmemeval_data/README.md +0 -0
  174. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/evals/data/longmemeval_data/longmemeval_oracle.json +0 -0
  175. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/evals/eval_cli.py +0 -0
  176. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/evals/eval_e2e_graph_building.py +0 -0
  177. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/evals/pytest.ini +0 -0
  178. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/evals/utils.py +0 -0
  179. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/helpers_test.py +0 -0
  180. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/llm_client/test_anthropic_client.py +0 -0
  181. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/llm_client/test_anthropic_client_int.py +0 -0
  182. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/llm_client/test_client.py +0 -0
  183. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/llm_client/test_errors.py +0 -0
  184. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/llm_client/test_gemini_client.py +0 -0
  185. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/test_edge_int.py +0 -0
  186. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/test_entity_exclusion_int.py +0 -0
  187. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/test_graphiti_int.py +0 -0
  188. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/test_graphiti_mock.py +0 -0
  189. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/test_node_int.py +0 -0
  190. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/utils/maintenance/test_bulk_utils.py +0 -0
  191. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/utils/maintenance/test_edge_operations.py +0 -0
  192. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/utils/maintenance/test_temporal_operations_int.py +0 -0
  193. {graphiti_core-0.21.0rc9 → graphiti_core-0.21.0rc11}/tests/utils/search/search_utils_test.py +0 -0
@@ -29,6 +29,8 @@ jobs:
29
29
 
30
30
  Please review this pull request.
31
31
 
32
+ IMPORTANT: Your role is to critically review code. You must not provide POSITIVE feedback on code, this only adds noise to the review process.
33
+
32
34
  Note: The PR branch is already checked out in the current working directory.
33
35
 
34
36
  Focus on:
@@ -48,3 +50,4 @@ jobs:
48
50
 
49
51
  claude_args: |
50
52
  --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*)"
53
+ --model claude-sonnet-4-5-20250929
@@ -0,0 +1,123 @@
1
+ name: Daily Issue Maintenance
2
+ on:
3
+ schedule:
4
+ - cron: "0 0 * * *" # Every day at midnight
5
+ workflow_dispatch: # Manual trigger option
6
+
7
+ jobs:
8
+ find-legacy-duplicates:
9
+ runs-on: ubuntu-latest
10
+ if: github.event_name == 'workflow_dispatch'
11
+ permissions:
12
+ contents: read
13
+ issues: write
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 1
19
+
20
+ - uses: anthropics/claude-code-action@v1
21
+ with:
22
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
23
+ prompt: |
24
+ REPO: ${{ github.repository }}
25
+
26
+ Find potential duplicate issues in the repository:
27
+
28
+ 1. Use `gh issue list --state open --limit 1000 --json number,title,body,createdAt` to get all open issues
29
+ 2. For each issue, search for potential duplicates using `gh search issues` with keywords from the title and body
30
+ 3. Compare issues to identify true duplicates using these criteria:
31
+ - Same bug or error being reported
32
+ - Same feature request (even if worded differently)
33
+ - Same question being asked
34
+ - Issues describing the same root problem
35
+
36
+ For each duplicate found:
37
+ - Add a comment linking to the original issue
38
+ - Apply the "duplicate" label using `gh issue edit`
39
+ - Be polite and explain why it's a duplicate
40
+
41
+ Focus on finding true duplicates, not just similar issues.
42
+
43
+ claude_args: |
44
+ --allowedTools "Bash(gh issue:*),Bash(gh search:*)"
45
+ --model claude-sonnet-4-5-20250929
46
+
47
+ check-stale-issues:
48
+ runs-on: ubuntu-latest
49
+ if: github.event_name == 'schedule'
50
+ permissions:
51
+ contents: read
52
+ issues: write
53
+ id-token: write
54
+ steps:
55
+ - uses: actions/checkout@v4
56
+ with:
57
+ fetch-depth: 1
58
+
59
+ - uses: anthropics/claude-code-action@v1
60
+ with:
61
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
62
+ prompt: |
63
+ REPO: ${{ github.repository }}
64
+
65
+ Review stale issues and request confirmation:
66
+
67
+ 1. Use `gh issue list --state open --limit 1000 --json number,title,updatedAt,comments` to get all open issues
68
+ 2. Identify issues that are:
69
+ - Older than 60 days (based on updatedAt)
70
+ - Have no comments with "stale-check" label
71
+ - Are not labeled as "enhancement" or "documentation"
72
+ 3. For each stale issue:
73
+ - Add a polite comment asking the issue originator if this is still relevant
74
+ - Apply a "stale-check" label to track that we've asked
75
+ - Use format: "@{author} Is this still an issue? Please confirm within 14 days or this issue will be closed."
76
+
77
+ Use:
78
+ - `gh issue view` to check issue details and labels
79
+ - `gh issue comment` to add comments
80
+ - `gh issue edit` to add the "stale-check" label
81
+
82
+ claude_args: |
83
+ --allowedTools "Bash(gh issue:*)"
84
+ --model claude-sonnet-4-5-20250929
85
+
86
+ close-unconfirmed-issues:
87
+ runs-on: ubuntu-latest
88
+ if: github.event_name == 'schedule'
89
+ needs: check-stale-issues
90
+ permissions:
91
+ contents: read
92
+ issues: write
93
+ id-token: write
94
+ steps:
95
+ - uses: actions/checkout@v4
96
+ with:
97
+ fetch-depth: 1
98
+
99
+ - uses: anthropics/claude-code-action@v1
100
+ with:
101
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
102
+ prompt: |
103
+ REPO: ${{ github.repository }}
104
+
105
+ Close unconfirmed stale issues:
106
+
107
+ 1. Use `gh issue list --state open --label "stale-check" --limit 1000 --json number,title,comments,updatedAt` to get issues with stale-check label
108
+ 2. For each issue, check if:
109
+ - The "stale-check" comment was added 14+ days ago
110
+ - There has been no response from the issue author or activity since the comment
111
+ 3. For issues meeting the criteria:
112
+ - Add a polite closing comment
113
+ - Close the issue using `gh issue close`
114
+ - Use format: "Closing due to inactivity. Feel free to reopen if this is still relevant."
115
+
116
+ Use:
117
+ - `gh issue view` to check issue comments and activity
118
+ - `gh issue comment` to add closing comment
119
+ - `gh issue close` to close the issue
120
+
121
+ claude_args: |
122
+ --allowedTools "Bash(gh issue:*)"
123
+ --model claude-sonnet-4-5-20250929
@@ -0,0 +1,140 @@
1
+ name: Issue Triage and Deduplication
2
+ on:
3
+ issues:
4
+ types: [opened]
5
+
6
+ jobs:
7
+ triage:
8
+ runs-on: ubuntu-latest
9
+ timeout-minutes: 10
10
+ permissions:
11
+ contents: read
12
+ issues: write
13
+ id-token: write
14
+
15
+ steps:
16
+ - name: Checkout repository
17
+ uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 1
20
+
21
+ - name: Run Claude Code for Issue Triage
22
+ uses: anthropics/claude-code-action@v1
23
+ with:
24
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
25
+ allowed_non_write_users: "*"
26
+ github_token: ${{ secrets.GITHUB_TOKEN }}
27
+ prompt: |
28
+ You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
29
+
30
+ IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels. DO NOT check for duplicates - that's handled by a separate job.
31
+
32
+ Issue Information:
33
+ - REPO: ${{ github.repository }}
34
+ - ISSUE_NUMBER: ${{ github.event.issue.number }}
35
+
36
+ TASK OVERVIEW:
37
+
38
+ 1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
39
+
40
+ 2. Next, use gh commands to get context about the issue:
41
+ - Use `gh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details
42
+ - Use `gh search issues` to find similar issues that might provide context for proper categorization
43
+ - You have access to these Bash commands:
44
+ - Bash(gh label list:*) - to get available labels
45
+ - Bash(gh issue view:*) - to view issue details
46
+ - Bash(gh issue edit:*) - to apply labels to the issue
47
+ - Bash(gh search:*) - to search for similar issues
48
+
49
+ 3. Analyze the issue content, considering:
50
+ - The issue title and description
51
+ - The type of issue (bug report, feature request, question, etc.)
52
+ - Technical areas mentioned
53
+ - Database mentions (neo4j, falkordb, neptune, etc.)
54
+ - LLM providers mentioned (openai, anthropic, gemini, groq, etc.)
55
+ - Components affected (embeddings, search, prompts, server, mcp, etc.)
56
+
57
+ 4. Select appropriate labels from the available labels list:
58
+ - Choose labels that accurately reflect the issue's nature
59
+ - Be specific but comprehensive
60
+ - Add database-specific labels if mentioned: neo4j, falkordb, neptune
61
+ - Add component labels if applicable
62
+ - DO NOT add priority labels (P1, P2, P3)
63
+ - DO NOT add duplicate label - that's handled by the deduplication job
64
+
65
+ 5. Apply the selected labels:
66
+ - Use `gh issue edit ${{ github.event.issue.number }} --add-label "label1,label2,label3"` to apply your selected labels
67
+ - DO NOT post any comments explaining your decision
68
+ - DO NOT communicate directly with users
69
+ - If no labels are clearly applicable, do not apply any labels
70
+
71
+ IMPORTANT GUIDELINES:
72
+ - Be thorough in your analysis
73
+ - Only select labels from the provided list
74
+ - DO NOT post any comments to the issue
75
+ - Your ONLY action should be to apply labels using gh issue edit
76
+ - It's okay to not add any labels if none are clearly applicable
77
+ - DO NOT check for duplicates
78
+
79
+ claude_args: |
80
+ --allowedTools "Bash(gh label list:*),Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh search:*)"
81
+ --model claude-sonnet-4-5-20250929
82
+
83
+ deduplicate:
84
+ runs-on: ubuntu-latest
85
+ timeout-minutes: 10
86
+ needs: triage
87
+ permissions:
88
+ contents: read
89
+ issues: write
90
+ id-token: write
91
+
92
+ steps:
93
+ - name: Checkout repository
94
+ uses: actions/checkout@v4
95
+ with:
96
+ fetch-depth: 1
97
+
98
+ - name: Check for duplicate issues
99
+ uses: anthropics/claude-code-action@v1
100
+ with:
101
+ prompt: |
102
+ Analyze this new issue and check if it's a duplicate of existing issues in the repository.
103
+
104
+ Issue: #${{ github.event.issue.number }}
105
+ Repository: ${{ github.repository }}
106
+
107
+ Your task:
108
+ 1. Use mcp__github__get_issue to get details of the current issue (#${{ github.event.issue.number }})
109
+ 2. Search for similar existing OPEN issues using mcp__github__search_issues with relevant keywords from the issue title and body
110
+ 3. Compare the new issue with existing ones to identify potential duplicates
111
+
112
+ Criteria for duplicates:
113
+ - Same bug or error being reported
114
+ - Same feature request (even if worded differently)
115
+ - Same question being asked
116
+ - Issues describing the same root problem
117
+
118
+ If you find duplicates:
119
+ - Add a comment on the new issue linking to the original issue(s)
120
+ - Apply the "duplicate" label to the new issue
121
+ - Be polite and explain why it's a duplicate
122
+ - Suggest the user follow the original issue for updates
123
+
124
+ If it's NOT a duplicate:
125
+ - Don't add any comments
126
+ - Don't modify labels
127
+
128
+ Use these tools:
129
+ - mcp__github__get_issue: Get issue details
130
+ - mcp__github__search_issues: Search for similar issues (use state:open)
131
+ - mcp__github__list_issues: List recent issues if needed
132
+ - mcp__github__create_issue_comment: Add a comment if duplicate found
133
+ - mcp__github__update_issue: Add "duplicate" label
134
+
135
+ Be thorough but efficient. Focus on finding true duplicates, not just similar issues.
136
+
137
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
138
+ claude_args: |
139
+ --allowedTools "mcp__github__get_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_issue_comment,mcp__github__update_issue,mcp__github__get_issue_comments"
140
+ --model claude-sonnet-4-5-20250929
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphiti-core
3
- Version: 0.21.0rc9
3
+ Version: 0.21.0rc11
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
@@ -644,8 +644,11 @@ def get_community_edge_from_record(record: Any):
644
644
 
645
645
 
646
646
  async def create_entity_edge_embeddings(embedder: EmbedderClient, edges: list[EntityEdge]):
647
- if len(edges) == 0:
647
+ # filter out falsey values from edges
648
+ filtered_edges = [edge for edge in edges if edge.fact]
649
+
650
+ if len(filtered_edges) == 0:
648
651
  return
649
- fact_embeddings = await embedder.create_batch([edge.fact for edge in edges])
650
- for edge, fact_embedding in zip(edges, fact_embeddings, strict=True):
652
+ fact_embeddings = await embedder.create_batch([edge.fact for edge in filtered_edges])
653
+ for edge, fact_embedding in zip(filtered_edges, fact_embeddings, strict=True):
651
654
  edge.fact_embedding = fact_embedding
@@ -868,9 +868,12 @@ def get_community_node_from_record(record: Any) -> CommunityNode:
868
868
 
869
869
 
870
870
  async def create_entity_node_embeddings(embedder: EmbedderClient, nodes: list[EntityNode]):
871
- if not nodes: # Handle empty list case
871
+ # filter out falsey values from nodes
872
+ filtered_nodes = [node for node in nodes if node.name]
873
+
874
+ if not filtered_nodes:
872
875
  return
873
876
 
874
- name_embeddings = await embedder.create_batch([node.name for node in nodes])
875
- for node, name_embedding in zip(nodes, name_embeddings, strict=True):
877
+ name_embeddings = await embedder.create_batch([node.name for node in filtered_nodes])
878
+ for node, name_embedding in zip(filtered_nodes, name_embeddings, strict=True):
876
879
  node.name_embedding = name_embedding
@@ -166,7 +166,8 @@ def nodes(context: dict[str, Any]) -> list[Message]:
166
166
  - They have similar names or purposes but refer to separate instances or concepts.
167
167
 
168
168
  Task:
169
- Respond with a JSON object that contains an "entity_resolutions" array with one entry for each entity in ENTITIES, ordered by the entity id.
169
+ ENTITIES contains {len(context['extracted_nodes'])} entities with IDs 0 through {len(context['extracted_nodes']) - 1}.
170
+ Your response MUST include EXACTLY {len(context['extracted_nodes'])} resolutions with IDs 0 through {len(context['extracted_nodes']) - 1}. Do not skip or add IDs.
170
171
 
171
172
  For every entity, return an object with the following keys:
172
173
  {{
@@ -151,7 +151,7 @@ For each entity extracted, also determine its entity type based on the provided
151
151
  Indicate the classified entity type by providing its entity_type_id.
152
152
 
153
153
  Guidelines:
154
- 1. Always try to extract an entities that the JSON represents. This will often be something like a "name" or "user field
154
+ 1. Extract all entities that the JSON represents. This will often be something like a "name" or "user" field
155
155
  2. Extract all entities mentioned in all other properties throughout the JSON structure
156
156
  3. Do NOT extract any properties that contain dates
157
157
  """
@@ -15,6 +15,7 @@ limitations under the License.
15
15
  """
16
16
 
17
17
  import logging
18
+ from collections.abc import Awaitable, Callable
18
19
  from time import time
19
20
  from typing import Any
20
21
 
@@ -55,6 +56,8 @@ from graphiti_core.utils.maintenance.edge_operations import (
55
56
 
56
57
  logger = logging.getLogger(__name__)
57
58
 
59
+ NodeSummaryFilter = Callable[[EntityNode], Awaitable[bool]]
60
+
58
61
 
59
62
  async def extract_nodes_reflexion(
60
63
  llm_client: LLMClient,
@@ -266,6 +269,27 @@ async def _resolve_with_llm(
266
269
  for i, node in enumerate(llm_extracted_nodes)
267
270
  ]
268
271
 
272
+ sent_ids = [ctx['id'] for ctx in extracted_nodes_context]
273
+ logger.debug(
274
+ 'Sending %d entities to LLM for deduplication with IDs 0-%d (actual IDs sent: %s)',
275
+ len(llm_extracted_nodes),
276
+ len(llm_extracted_nodes) - 1,
277
+ sent_ids if len(sent_ids) < 20 else f'{sent_ids[:10]}...{sent_ids[-10:]}',
278
+ )
279
+ if llm_extracted_nodes:
280
+ sample_size = min(3, len(extracted_nodes_context))
281
+ logger.debug(
282
+ 'First %d entities: %s',
283
+ sample_size,
284
+ [(ctx['id'], ctx['name']) for ctx in extracted_nodes_context[:sample_size]],
285
+ )
286
+ if len(extracted_nodes_context) > 3:
287
+ logger.debug(
288
+ 'Last %d entities: %s',
289
+ sample_size,
290
+ [(ctx['id'], ctx['name']) for ctx in extracted_nodes_context[-sample_size:]],
291
+ )
292
+
269
293
  existing_nodes_context = [
270
294
  {
271
295
  **{
@@ -298,15 +322,38 @@ async def _resolve_with_llm(
298
322
  valid_relative_range = range(len(state.unresolved_indices))
299
323
  processed_relative_ids: set[int] = set()
300
324
 
325
+ received_ids = {r.id for r in node_resolutions}
326
+ expected_ids = set(valid_relative_range)
327
+ missing_ids = expected_ids - received_ids
328
+ extra_ids = received_ids - expected_ids
329
+
330
+ logger.debug(
331
+ 'Received %d resolutions for %d entities',
332
+ len(node_resolutions),
333
+ len(state.unresolved_indices),
334
+ )
335
+
336
+ if missing_ids:
337
+ logger.warning('LLM did not return resolutions for IDs: %s', sorted(missing_ids))
338
+
339
+ if extra_ids:
340
+ logger.warning(
341
+ 'LLM returned invalid IDs outside valid range 0-%d: %s (all returned IDs: %s)',
342
+ len(state.unresolved_indices) - 1,
343
+ sorted(extra_ids),
344
+ sorted(received_ids),
345
+ )
346
+
301
347
  for resolution in node_resolutions:
302
348
  relative_id: int = resolution.id
303
349
  duplicate_idx: int = resolution.duplicate_idx
304
350
 
305
351
  if relative_id not in valid_relative_range:
306
352
  logger.warning(
307
- 'Skipping invalid LLM dedupe id %s (unresolved indices: %s)',
353
+ 'Skipping invalid LLM dedupe id %d (valid range: 0-%d, received %d resolutions)',
308
354
  relative_id,
309
- state.unresolved_indices,
355
+ len(state.unresolved_indices) - 1,
356
+ len(node_resolutions),
310
357
  )
311
358
  continue
312
359
 
@@ -402,6 +449,7 @@ async def extract_attributes_from_nodes(
402
449
  episode: EpisodicNode | None = None,
403
450
  previous_episodes: list[EpisodicNode] | None = None,
404
451
  entity_types: dict[str, type[BaseModel]] | None = None,
452
+ should_summarize_node: NodeSummaryFilter | None = None,
405
453
  ) -> list[EntityNode]:
406
454
  llm_client = clients.llm_client
407
455
  embedder = clients.embedder
@@ -418,6 +466,7 @@ async def extract_attributes_from_nodes(
418
466
  else None
419
467
  ),
420
468
  clients.ensure_ascii,
469
+ should_summarize_node,
421
470
  )
422
471
  for node in nodes
423
472
  ]
@@ -435,6 +484,7 @@ async def extract_attributes_from_node(
435
484
  previous_episodes: list[EpisodicNode] | None = None,
436
485
  entity_type: type[BaseModel] | None = None,
437
486
  ensure_ascii: bool = False,
487
+ should_summarize_node: NodeSummaryFilter | None = None,
438
488
  ) -> EntityNode:
439
489
  node_context: dict[str, Any] = {
440
490
  'name': node.name,
@@ -477,16 +527,22 @@ async def extract_attributes_from_node(
477
527
  else {}
478
528
  )
479
529
 
480
- summary_response = await llm_client.generate_response(
481
- prompt_library.extract_nodes.extract_summary(summary_context),
482
- response_model=EntitySummary,
483
- model_size=ModelSize.small,
484
- )
530
+ # Determine if summary should be generated
531
+ generate_summary = True
532
+ if should_summarize_node is not None:
533
+ generate_summary = await should_summarize_node(node)
534
+
535
+ # Conditionally generate summary
536
+ if generate_summary:
537
+ summary_response = await llm_client.generate_response(
538
+ prompt_library.extract_nodes.extract_summary(summary_context),
539
+ response_model=EntitySummary,
540
+ model_size=ModelSize.small,
541
+ )
542
+ node.summary = summary_response.get('summary', '')
485
543
 
486
544
  if has_entity_attributes and entity_type is not None:
487
545
  entity_type(**llm_response)
488
-
489
- node.summary = summary_response.get('summary', '')
490
546
  node_attributes = {key: value for key, value in llm_response.items()}
491
547
 
492
548
  node.attributes.update(node_attributes)
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "graphiti-core"
3
3
  description = "A temporal graph building library"
4
- version = "0.21.0pre9"
4
+ version = "0.21.0pre11"
5
5
  authors = [
6
6
  { name = "Paul Paliychuk", email = "paul@getzep.com" },
7
7
  { name = "Preston Rasmussen", email = "preston@getzep.com" },
@@ -27,6 +27,8 @@ from graphiti_core.utils.maintenance.dedup_helpers import (
27
27
  from graphiti_core.utils.maintenance.node_operations import (
28
28
  _collect_candidate_nodes,
29
29
  _resolve_with_llm,
30
+ extract_attributes_from_node,
31
+ extract_attributes_from_nodes,
30
32
  resolve_extracted_nodes,
31
33
  )
32
34
 
@@ -477,3 +479,183 @@ async def test_resolve_with_llm_invalid_duplicate_idx_defaults_to_extracted(monk
477
479
  assert state.resolved_nodes[0] == extracted
478
480
  assert state.uuid_map[extracted.uuid] == extracted.uuid
479
481
  assert state.duplicate_pairs == []
482
+
483
+
484
+ @pytest.mark.asyncio
485
+ async def test_extract_attributes_without_callback_generates_summary():
486
+ """Test that summary is generated when no callback is provided (default behavior)."""
487
+ llm_client = MagicMock()
488
+ llm_client.generate_response = AsyncMock(
489
+ return_value={'summary': 'Generated summary', 'attributes': {}}
490
+ )
491
+
492
+ node = EntityNode(name='Test Node', group_id='group', labels=['Entity'], summary='Old summary')
493
+ episode = _make_episode()
494
+
495
+ result = await extract_attributes_from_node(
496
+ llm_client,
497
+ node,
498
+ episode=episode,
499
+ previous_episodes=[],
500
+ entity_type=None,
501
+ ensure_ascii=False,
502
+ should_summarize_node=None, # No callback provided
503
+ )
504
+
505
+ # Summary should be generated
506
+ assert result.summary == 'Generated summary'
507
+ # LLM should have been called for summary
508
+ assert llm_client.generate_response.call_count == 1
509
+
510
+
511
+ @pytest.mark.asyncio
512
+ async def test_extract_attributes_with_callback_skip_summary():
513
+ """Test that summary is NOT regenerated when callback returns False."""
514
+ llm_client = MagicMock()
515
+ llm_client.generate_response = AsyncMock(
516
+ return_value={'summary': 'This should not be used', 'attributes': {}}
517
+ )
518
+
519
+ node = EntityNode(name='Test Node', group_id='group', labels=['Entity'], summary='Old summary')
520
+ episode = _make_episode()
521
+
522
+ # Callback that always returns False (skip summary generation)
523
+ async def skip_summary_filter(node: EntityNode) -> bool:
524
+ return False
525
+
526
+ result = await extract_attributes_from_node(
527
+ llm_client,
528
+ node,
529
+ episode=episode,
530
+ previous_episodes=[],
531
+ entity_type=None,
532
+ ensure_ascii=False,
533
+ should_summarize_node=skip_summary_filter,
534
+ )
535
+
536
+ # Summary should remain unchanged
537
+ assert result.summary == 'Old summary'
538
+ # LLM should NOT have been called for summary
539
+ assert llm_client.generate_response.call_count == 0
540
+
541
+
542
+ @pytest.mark.asyncio
543
+ async def test_extract_attributes_with_callback_generate_summary():
544
+ """Test that summary is regenerated when callback returns True."""
545
+ llm_client = MagicMock()
546
+ llm_client.generate_response = AsyncMock(
547
+ return_value={'summary': 'New generated summary', 'attributes': {}}
548
+ )
549
+
550
+ node = EntityNode(name='Test Node', group_id='group', labels=['Entity'], summary='Old summary')
551
+ episode = _make_episode()
552
+
553
+ # Callback that always returns True (generate summary)
554
+ async def generate_summary_filter(node: EntityNode) -> bool:
555
+ return True
556
+
557
+ result = await extract_attributes_from_node(
558
+ llm_client,
559
+ node,
560
+ episode=episode,
561
+ previous_episodes=[],
562
+ entity_type=None,
563
+ ensure_ascii=False,
564
+ should_summarize_node=generate_summary_filter,
565
+ )
566
+
567
+ # Summary should be updated
568
+ assert result.summary == 'New generated summary'
569
+ # LLM should have been called for summary
570
+ assert llm_client.generate_response.call_count == 1
571
+
572
+
573
+ @pytest.mark.asyncio
574
+ async def test_extract_attributes_with_selective_callback():
575
+ """Test callback that selectively skips summaries based on node properties."""
576
+ llm_client = MagicMock()
577
+ llm_client.generate_response = AsyncMock(
578
+ return_value={'summary': 'Generated summary', 'attributes': {}}
579
+ )
580
+
581
+ user_node = EntityNode(name='User', group_id='group', labels=['Entity', 'User'], summary='Old')
582
+ topic_node = EntityNode(
583
+ name='Topic', group_id='group', labels=['Entity', 'Topic'], summary='Old'
584
+ )
585
+
586
+ episode = _make_episode()
587
+
588
+ # Callback that skips User nodes but generates for others
589
+ async def selective_filter(node: EntityNode) -> bool:
590
+ return 'User' not in node.labels
591
+
592
+ result_user = await extract_attributes_from_node(
593
+ llm_client,
594
+ user_node,
595
+ episode=episode,
596
+ previous_episodes=[],
597
+ entity_type=None,
598
+ ensure_ascii=False,
599
+ should_summarize_node=selective_filter,
600
+ )
601
+
602
+ result_topic = await extract_attributes_from_node(
603
+ llm_client,
604
+ topic_node,
605
+ episode=episode,
606
+ previous_episodes=[],
607
+ entity_type=None,
608
+ ensure_ascii=False,
609
+ should_summarize_node=selective_filter,
610
+ )
611
+
612
+ # User summary should remain unchanged
613
+ assert result_user.summary == 'Old'
614
+ # Topic summary should be generated
615
+ assert result_topic.summary == 'Generated summary'
616
+ # LLM should have been called only once (for topic)
617
+ assert llm_client.generate_response.call_count == 1
618
+
619
+
620
+ @pytest.mark.asyncio
621
+ async def test_extract_attributes_from_nodes_with_callback():
622
+ """Test that callback is properly passed through extract_attributes_from_nodes."""
623
+ clients, _ = _make_clients()
624
+ clients.llm_client.generate_response = AsyncMock(
625
+ return_value={'summary': 'New summary', 'attributes': {}}
626
+ )
627
+ clients.embedder.create = AsyncMock(return_value=[0.1, 0.2, 0.3])
628
+ clients.embedder.create_batch = AsyncMock(return_value=[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])
629
+
630
+ node1 = EntityNode(name='Node1', group_id='group', labels=['Entity', 'User'], summary='Old1')
631
+ node2 = EntityNode(name='Node2', group_id='group', labels=['Entity', 'Topic'], summary='Old2')
632
+
633
+ episode = _make_episode()
634
+
635
+ call_tracker = []
636
+
637
+ # Callback that tracks which nodes it's called with
638
+ async def tracking_filter(node: EntityNode) -> bool:
639
+ call_tracker.append(node.name)
640
+ return 'User' not in node.labels
641
+
642
+ results = await extract_attributes_from_nodes(
643
+ clients,
644
+ [node1, node2],
645
+ episode=episode,
646
+ previous_episodes=[],
647
+ entity_types=None,
648
+ should_summarize_node=tracking_filter,
649
+ )
650
+
651
+ # Callback should have been called for both nodes
652
+ assert len(call_tracker) == 2
653
+ assert 'Node1' in call_tracker
654
+ assert 'Node2' in call_tracker
655
+
656
+ # Node1 (User) should keep old summary, Node2 (Topic) should get new summary
657
+ node1_result = next(n for n in results if n.name == 'Node1')
658
+ node2_result = next(n for n in results if n.name == 'Node2')
659
+
660
+ assert node1_result.summary == 'Old1'
661
+ assert node2_result.summary == 'New summary'