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.
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/PKG-INFO +1 -1
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/bulk_utils.py +126 -60
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/dedup_helpers.py +6 -1
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/node_operations.py +3 -2
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/pyproject.toml +1 -1
- graphiti_core-0.30.0rc1/tests/utils/maintenance/test_bulk_utils.py +232 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/utils/maintenance/test_node_operations.py +4 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/uv.lock +1 -1
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.env.example +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/dependabot.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/pull_request_template.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/secret_scanning.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/ai-moderator.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/cla.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/claude-code-review.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/claude.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/codeql.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/lint.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/mcp-server-docker.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/release-graphiti-core.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/typecheck.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/unit_tests.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.gitignore +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/AGENTS.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/CLAUDE.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/CODE_OF_CONDUCT.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/CONTRIBUTING.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/Dockerfile +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/LICENSE +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/Makefile +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/SECURITY.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/Zep-CLA.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/conftest.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/depot.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/docker-compose.test.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/docker-compose.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/ellipsis.yaml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/data/manybirds_products.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/ecommerce/runner.ipynb +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/ecommerce/runner.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/langgraph-agent/agent.ipynb +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/langgraph-agent/tinybirds-jess.png +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/podcast/podcast_runner.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/podcast/podcast_transcript.txt +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/podcast/transcript_parser.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/quickstart_falkordb.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/quickstart_neo4j.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/quickstart_neptune.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/quickstart/requirements.txt +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/wizard_of_oz/parser.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/wizard_of_oz/runner.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/wizard_of_oz/woo.txt +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/bge_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/gemini_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/cross_encoder/openai_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/falkordb_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/kuzu_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/neo4j_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/driver/neptune_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/edges.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/azure_openai.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/gemini.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/openai.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/embedder/voyage.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/errors.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/graph_queries.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/graphiti.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/graphiti_types.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/helpers.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/anthropic_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/azure_openai_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/config.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/errors.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/gemini_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/groq_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/openai_base_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/openai_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/openai_generic_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/llm_client/utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/migrations/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/edges/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/edges/edge_db_queries.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/nodes/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/models/nodes/node_db_queries.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/nodes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/dedupe_edges.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/dedupe_nodes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/eval.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/extract_edge_dates.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/extract_edges.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/extract_nodes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/invalidate_edges.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/lib.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/models.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/prompt_helpers.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/prompts/summarize_nodes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/py.typed +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_config.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_config_recipes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_filters.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_helpers.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/search/search_utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/telemetry/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/telemetry/telemetry.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/datetime_utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/community_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/edge_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/graph_data_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/temporal_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/ontology_utils/entity_types_utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/images/arxiv-screenshot.png +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/images/graphiti-graph-intro.gif +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/images/graphiti-intro-slides-stock-2.gif +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/images/simple_graph.svg +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/.env.example +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/.python-version +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/Dockerfile +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/cursor_rules.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/docker-compose.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/graphiti_mcp_server.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/mcp_config_sse_example.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/mcp_config_stdio_example.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/pyproject.toml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/mcp_server/uv.lock +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/poetry.lock +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/py.typed +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/pytest.ini +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/.env.example +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/Makefile +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/config.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/dto/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/dto/common.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/dto/ingest.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/dto/retrieve.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/main.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/routers/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/routers/ingest.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/routers/retrieve.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/graph_service/zep_graphiti.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/pyproject.toml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/server/uv.lock +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/signatures/version1/cla.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/cross_encoder/test_bge_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/cross_encoder/test_gemini_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/driver/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/driver/test_falkordb_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/embedder/embedder_fixtures.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/embedder/test_gemini.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/embedder/test_openai.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/embedder/test_voyage.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/data/longmemeval_data/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/data/longmemeval_data/longmemeval_oracle.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/eval_cli.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/eval_e2e_graph_building.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/pytest.ini +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/evals/utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/helpers_test.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_anthropic_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_anthropic_client_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_errors.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/llm_client/test_gemini_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_edge_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_entity_exclusion_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_graphiti_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_graphiti_mock.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/test_node_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/utils/maintenance/test_edge_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/utils/maintenance/test_temporal_operations_int.py +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
325
|
+
for i, nodes in enumerate(extracted_nodes)
|
|
322
326
|
]
|
|
323
327
|
)
|
|
324
328
|
|
|
325
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
|
341
|
-
|
|
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[
|
|
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
|
|
{graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/graphiti_core/utils/maintenance/dedup_helpers.py
RENAMED
|
@@ -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,
|
|
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.
|
|
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'
|
{graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/tests/utils/maintenance/test_node_operations.py
RENAMED
|
@@ -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)]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/claude-code-review.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/.github/workflows/release-graphiti-core.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc1}/examples/langgraph-agent/tinybirds-jess.png
RENAMED
|
File without changes
|
|
File without changes
|