graphiti-core 0.30.0rc0__tar.gz → 0.30.0rc2__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.0rc2}/PKG-INFO +1 -1
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/bulk_utils.py +126 -60
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/maintenance/dedup_helpers.py +6 -1
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/maintenance/node_operations.py +36 -8
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/pyproject.toml +1 -1
- graphiti_core-0.30.0rc2/tests/utils/maintenance/test_bulk_utils.py +232 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/utils/maintenance/test_node_operations.py +138 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/uv.lock +1 -1
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.env.example +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/dependabot.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/pull_request_template.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/secret_scanning.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/ai-moderator.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/cla.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/claude-code-review.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/claude.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/codeql.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/lint.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/mcp-server-docker.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/release-graphiti-core.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/typecheck.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.github/workflows/unit_tests.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/.gitignore +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/AGENTS.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/CLAUDE.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/CODE_OF_CONDUCT.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/CONTRIBUTING.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/Dockerfile +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/LICENSE +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/Makefile +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/SECURITY.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/Zep-CLA.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/conftest.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/depot.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/docker-compose.test.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/docker-compose.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/ellipsis.yaml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/data/manybirds_products.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/ecommerce/runner.ipynb +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/ecommerce/runner.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/langgraph-agent/agent.ipynb +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/langgraph-agent/tinybirds-jess.png +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/podcast/podcast_runner.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/podcast/podcast_transcript.txt +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/podcast/transcript_parser.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/quickstart/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/quickstart/quickstart_falkordb.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/quickstart/quickstart_neo4j.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/quickstart/quickstart_neptune.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/quickstart/requirements.txt +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/wizard_of_oz/parser.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/wizard_of_oz/runner.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/examples/wizard_of_oz/woo.txt +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/cross_encoder/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/cross_encoder/bge_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/cross_encoder/client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/cross_encoder/gemini_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/cross_encoder/openai_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/driver/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/driver/driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/driver/falkordb_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/driver/kuzu_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/driver/neo4j_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/driver/neptune_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/edges.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/embedder/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/embedder/azure_openai.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/embedder/client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/embedder/gemini.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/embedder/openai.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/embedder/voyage.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/errors.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/graph_queries.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/graphiti.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/graphiti_types.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/helpers.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/anthropic_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/azure_openai_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/config.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/errors.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/gemini_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/groq_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/openai_base_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/openai_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/openai_generic_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/llm_client/utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/migrations/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/models/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/models/edges/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/models/edges/edge_db_queries.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/models/nodes/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/models/nodes/node_db_queries.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/nodes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/dedupe_edges.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/dedupe_nodes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/eval.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/extract_edge_dates.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/extract_edges.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/extract_nodes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/invalidate_edges.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/lib.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/models.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/prompt_helpers.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/prompts/summarize_nodes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/py.typed +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/search/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/search/search.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/search/search_config.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/search/search_config_recipes.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/search/search_filters.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/search/search_helpers.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/search/search_utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/telemetry/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/telemetry/telemetry.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/datetime_utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/maintenance/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/maintenance/community_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/maintenance/edge_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/maintenance/graph_data_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/maintenance/temporal_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/maintenance/utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/graphiti_core/utils/ontology_utils/entity_types_utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/images/arxiv-screenshot.png +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/images/graphiti-graph-intro.gif +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/images/graphiti-intro-slides-stock-2.gif +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/images/simple_graph.svg +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/.env.example +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/.python-version +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/Dockerfile +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/cursor_rules.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/docker-compose.yml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/graphiti_mcp_server.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/mcp_config_sse_example.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/mcp_config_stdio_example.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/pyproject.toml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/mcp_server/uv.lock +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/poetry.lock +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/py.typed +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/pytest.ini +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/.env.example +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/Makefile +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/config.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/dto/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/dto/common.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/dto/ingest.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/dto/retrieve.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/main.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/routers/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/routers/ingest.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/routers/retrieve.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/graph_service/zep_graphiti.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/pyproject.toml +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/server/uv.lock +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/signatures/version1/cla.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/cross_encoder/test_bge_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/cross_encoder/test_gemini_reranker_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/driver/__init__.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/driver/test_falkordb_driver.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/embedder/embedder_fixtures.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/embedder/test_gemini.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/embedder/test_openai.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/embedder/test_voyage.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/evals/data/longmemeval_data/README.md +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/evals/data/longmemeval_data/longmemeval_oracle.json +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/evals/eval_cli.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/evals/eval_e2e_graph_building.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/evals/pytest.ini +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/evals/utils.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/helpers_test.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/llm_client/test_anthropic_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/llm_client/test_anthropic_client_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/llm_client/test_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/llm_client/test_errors.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/llm_client/test_gemini_client.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/test_edge_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/test_entity_exclusion_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/test_graphiti_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/test_graphiti_mock.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/test_node_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/utils/maintenance/test_edge_operations.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/tests/utils/maintenance/test_temporal_operations_int.py +0 -0
- {graphiti_core-0.30.0rc0 → graphiti_core-0.30.0rc2}/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.0rc2
|
|
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.0rc2}/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)
|
|
@@ -241,7 +241,11 @@ async def _resolve_with_llm(
|
|
|
241
241
|
previous_episodes: list[EpisodicNode] | None,
|
|
242
242
|
entity_types: dict[str, type[BaseModel]] | None,
|
|
243
243
|
) -> None:
|
|
244
|
-
"""Escalate unresolved nodes to the dedupe prompt so the LLM can select or reject duplicates.
|
|
244
|
+
"""Escalate unresolved nodes to the dedupe prompt so the LLM can select or reject duplicates.
|
|
245
|
+
|
|
246
|
+
The guardrails below defensively ignore malformed or duplicate LLM responses so the
|
|
247
|
+
ingestion workflow remains deterministic even when the model misbehaves.
|
|
248
|
+
"""
|
|
245
249
|
if not state.unresolved_indices:
|
|
246
250
|
return
|
|
247
251
|
|
|
@@ -291,21 +295,46 @@ async def _resolve_with_llm(
|
|
|
291
295
|
|
|
292
296
|
node_resolutions: list[NodeDuplicate] = NodeResolutions(**llm_response).entity_resolutions
|
|
293
297
|
|
|
298
|
+
valid_relative_range = range(len(state.unresolved_indices))
|
|
299
|
+
processed_relative_ids: set[int] = set()
|
|
300
|
+
|
|
294
301
|
for resolution in node_resolutions:
|
|
295
302
|
relative_id: int = resolution.id
|
|
296
303
|
duplicate_idx: int = resolution.duplicate_idx
|
|
297
304
|
|
|
305
|
+
if relative_id not in valid_relative_range:
|
|
306
|
+
logger.warning(
|
|
307
|
+
'Skipping invalid LLM dedupe id %s (unresolved indices: %s)',
|
|
308
|
+
relative_id,
|
|
309
|
+
state.unresolved_indices,
|
|
310
|
+
)
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
if relative_id in processed_relative_ids:
|
|
314
|
+
logger.warning('Duplicate LLM dedupe id %s received; ignoring.', relative_id)
|
|
315
|
+
continue
|
|
316
|
+
processed_relative_ids.add(relative_id)
|
|
317
|
+
|
|
298
318
|
original_index = state.unresolved_indices[relative_id]
|
|
299
319
|
extracted_node = extracted_nodes[original_index]
|
|
300
320
|
|
|
301
|
-
resolved_node
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
321
|
+
resolved_node: EntityNode
|
|
322
|
+
if duplicate_idx == -1:
|
|
323
|
+
resolved_node = extracted_node
|
|
324
|
+
elif 0 <= duplicate_idx < len(indexes.existing_nodes):
|
|
325
|
+
resolved_node = indexes.existing_nodes[duplicate_idx]
|
|
326
|
+
else:
|
|
327
|
+
logger.warning(
|
|
328
|
+
'Invalid duplicate_idx %s for extracted node %s; treating as no duplicate.',
|
|
329
|
+
duplicate_idx,
|
|
330
|
+
extracted_node.uuid,
|
|
331
|
+
)
|
|
332
|
+
resolved_node = extracted_node
|
|
306
333
|
|
|
307
334
|
state.resolved_nodes[original_index] = resolved_node
|
|
308
335
|
state.uuid_map[extracted_node.uuid] = resolved_node.uuid
|
|
336
|
+
if resolved_node.uuid != extracted_node.uuid:
|
|
337
|
+
state.duplicate_pairs.append((extracted_node, resolved_node))
|
|
309
338
|
|
|
310
339
|
|
|
311
340
|
async def resolve_extracted_nodes(
|
|
@@ -332,7 +361,6 @@ async def resolve_extracted_nodes(
|
|
|
332
361
|
uuid_map={},
|
|
333
362
|
unresolved_indices=[],
|
|
334
363
|
)
|
|
335
|
-
node_duplicates: list[tuple[EntityNode, EntityNode]] = []
|
|
336
364
|
|
|
337
365
|
_resolve_with_similarity(extracted_nodes, indexes, state)
|
|
338
366
|
|
|
@@ -359,7 +387,7 @@ async def resolve_extracted_nodes(
|
|
|
359
387
|
|
|
360
388
|
new_node_duplicates: list[
|
|
361
389
|
tuple[EntityNode, EntityNode]
|
|
362
|
-
] = await filter_existing_duplicate_of_edges(driver,
|
|
390
|
+
] = await filter_existing_duplicate_of_edges(driver, state.duplicate_pairs)
|
|
363
391
|
|
|
364
392
|
return (
|
|
365
393
|
[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.0pre2"
|
|
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'
|