Files
wehub-resource-sync c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

354 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
from typing import TYPE_CHECKING, Dict, List, Optional
from cognee.modules.pipelines.tasks.task import task_summary
from cognee.infrastructure.engine import DataPoint
from cognee.infrastructure.databases.unified import get_unified_engine
from cognee.infrastructure.databases.unified.capabilities import EngineCapability
from cognee.infrastructure.databases.provenance import (
EdgeIdentity,
data_item_id,
make_source_ref_key,
)
from cognee.infrastructure.databases.provenance.markers import (
mark_graph_provenance_if_empty,
)
from cognee.infrastructure.databases.relational import get_async_session
from cognee.modules.graph.methods import upsert_edges, upsert_nodes
from cognee.modules.graph.utils import (
deduplicate_nodes_and_edges,
ensure_default_edge_properties,
get_graph_from_model,
)
from .index_data_points import index_data_points
from .index_graph_edges import index_graph_edges
from cognee.modules.engine.models import Triplet
from cognee.shared.logging_utils import get_logger
from cognee.tasks.storage.exceptions import (
InvalidDataPointsInAddDataPointsError,
)
from ...modules.engine.utils import generate_node_id
if TYPE_CHECKING:
from cognee.modules.pipelines.models import PipelineContext
logger = get_logger("add_data_points")
@task_summary("Stored {n} data point(s)")
async def add_data_points(
data_points: List[DataPoint],
custom_edges: Optional[List] = None,
embed_triplets: bool = False,
ctx: Optional["PipelineContext"] = None,
) -> List[DataPoint]:
"""
Add a batch of data points to the graph database by extracting nodes and edges,
deduplicating them, and indexing them for retrieval.
Args:
data_points: Data points to process and insert into the graph.
custom_edges: Custom edges between datapoints.
embed_triplets: If True, creates and indexes triplet embeddings.
ctx: Pipeline runtime context (user, dataset, data_item).
"""
user = ctx.user if ctx else None
data_item = ctx.data_item if ctx else None
dataset = ctx.dataset if ctx else None
pipeline_run_id = ctx.pipeline_run_id if ctx else None
if not isinstance(data_points, list):
raise InvalidDataPointsInAddDataPointsError("data_points must be a list.")
if not all(isinstance(dp, DataPoint) for dp in data_points):
raise InvalidDataPointsInAddDataPointsError("data_points: each item must be a DataPoint.")
nodes = []
edges = []
added_nodes = {}
added_edges = {}
visited_properties = {}
results = await asyncio.gather(
*[
get_graph_from_model(
data_point,
added_nodes=added_nodes,
added_edges=added_edges,
visited_properties=visited_properties,
)
for data_point in data_points
]
)
for result_nodes, result_edges in results:
nodes.extend(result_nodes)
edges.extend(result_edges)
nodes, edges = deduplicate_nodes_and_edges(nodes, edges)
edges = ensure_default_edge_properties(edges, nodes=nodes)
custom_edges = (
ensure_default_edge_properties(custom_edges, nodes=nodes)
if isinstance(custom_edges, list) and custom_edges
else None
)
unified = await get_unified_engine()
graph_engine = unified.graph
vector_engine = unified.vector
use_hybrid = unified.has_capability(EngineCapability.HYBRID_WRITE)
# Provenance needs a concrete (dataset, data) pair. data_item_id resolves
# the id whether data_item is a relational Data (.id) or an ingestion
# DataItem (.data_id); it is None for items that carry neither (a raw
# file/text item, or the CogneeGraph memify passes in), in which case there
# is nothing to attribute and both the ledger and graph-fold paths are skipped.
data_id = data_item_id(data_item)
stores_provenance = False
if user and dataset and data_id is not None:
# Graph-provenance graphs (empty graphs marked via graph metadata) carry
# their provenance in the graph itself, so they skip the relational
# rollback ledger entirely. On backends that implement provenance
# (e.g. Ladybug + LanceDB) a fresh empty graph IS marked here and takes
# the graph-provenance path; backends without provenance support raise on
# set_graph_metadata, so this stays False and the ledger path runs.
#
# On the non-hybrid path the provenance source refs are folded into the
# graph write below (atomic — no window where an artifact exists without
# its provenance). Hybrid backends still attach in a second pass and keep
# that window; if the attach raises, the run is marked failed.
#
# Under COGNEE_DISTRIBUTED the graph write is diverted to a queue; the
# provenance stamp (source_ref_key / pipeline_run_id) rides along in the
# queue payload and is folded per data item by the graph_saving_worker,
# so graph-native provenance works in distributed mode too.
stores_provenance = await mark_graph_provenance_if_empty(graph_engine)
if not stores_provenance:
# Single session for all upserts: one transaction, one commit. The
# rollback ledger is written BEFORE the graph/vector writes so a
# failed write can always be swept by the rollback handler.
async with get_async_session() as session:
await upsert_nodes(
nodes,
tenant_id=user.tenant_id,
user_id=user.id,
dataset_id=dataset.id,
data_id=data_id,
session=session,
pipeline_run_id=pipeline_run_id,
)
await upsert_edges(
edges,
tenant_id=user.tenant_id,
user_id=user.id,
dataset_id=dataset.id,
data_id=data_id,
session=session,
pipeline_run_id=pipeline_run_id,
)
if custom_edges:
await upsert_edges(
custom_edges,
tenant_id=user.tenant_id,
user_id=user.id,
dataset_id=dataset.id,
data_id=data_id,
session=session,
pipeline_run_id=pipeline_run_id,
)
await session.commit()
# Graph provenance is folded INTO the graph write so a node/edge is
# created and stamped in one atomic statement (no write-then-attach window,
# no concurrent lost update — COG-5522 #4/#8). Only the non-hybrid path can
# fold today; hybrid backends still stamp via a separate attach pass below.
# source_ref_key stays None for non-graph-provenance writes (no provenance).
fold_source_ref_key = None
fold_run_arg = None
if stores_provenance and not use_hybrid:
fold_source_ref_key = make_source_ref_key(dataset.id, data_id)
fold_run_arg = str(pipeline_run_id) if pipeline_run_id else None
if use_hybrid:
await graph_engine.add_nodes_with_vectors(nodes)
else:
await asyncio.gather(
graph_engine.add_nodes(
nodes, source_ref_key=fold_source_ref_key, pipeline_run_id=fold_run_arg
),
index_data_points(
[node.model_copy(deep=True) for node in nodes],
vector_engine=vector_engine,
),
)
if use_hybrid:
await graph_engine.add_edges_with_vectors(edges)
else:
await asyncio.gather(
graph_engine.add_edges(
edges, source_ref_key=fold_source_ref_key, pipeline_run_id=fold_run_arg
),
index_graph_edges(edges, vector_engine=vector_engine),
)
if custom_edges:
# This must be handled separately from datapoint edges, created a task in linear to dig deeper but (COG-3488)
# Note: custom_edges is already normalized (with nodes) above, before the
# rollback-ledger upsert, so no second ensure_default_edge_properties here.
if use_hybrid:
await graph_engine.add_edges_with_vectors(custom_edges)
else:
await asyncio.gather(
graph_engine.add_edges(
custom_edges,
source_ref_key=fold_source_ref_key,
pipeline_run_id=fold_run_arg,
),
index_graph_edges(custom_edges, vector_engine=vector_engine),
)
edges.extend(custom_edges)
if stores_provenance and use_hybrid:
# Hybrid backends write nodes/edges and their vectors in one call that
# cannot yet fold provenance, so stamp the source refs in a separate
# attach pass. This keeps a write-then-attach window for hybrid graphs
# only; the non-hybrid path above is already atomic.
source_ref_key = make_source_ref_key(dataset.id, data_id)
run_arg = str(pipeline_run_id) if pipeline_run_id else None
node_ids = [str(node.id) for node in nodes]
edge_ids = [EdgeIdentity(str(edge[0]), str(edge[1]), edge[2]) for edge in edges]
if node_ids:
await graph_engine.attach_node_source_refs(node_ids, [source_ref_key], run_arg)
if edge_ids:
await graph_engine.attach_edge_source_refs(edge_ids, [source_ref_key], run_arg)
if embed_triplets:
triplets = _create_triplets_from_graph(nodes, edges)
if triplets:
await index_data_points(triplets, vector_engine=vector_engine)
logger.info(f"Created and indexed {len(triplets)} triplets from graph structure")
return data_points
def _extract_embeddable_text_from_datapoint(data_point: DataPoint) -> str:
"""
Extract embeddable text from a DataPoint using its index_fields metadata.
Uses the same approach as index_data_points.
Parameters:
-----------
- data_point (DataPoint): The data point to extract text from.
Returns:
--------
- str: Concatenated string of all embeddable property values, or empty string if none found.
"""
if not data_point or not hasattr(data_point, "metadata"):
return ""
index_fields = data_point.metadata.get("index_fields", [])
if not index_fields:
return ""
embeddable_values = []
for field_name in index_fields:
field_value = getattr(data_point, field_name, None)
if field_value is not None:
field_value = str(field_value).strip()
if field_value:
embeddable_values.append(field_value)
return " ".join(embeddable_values) if embeddable_values else ""
def _create_triplets_from_graph(nodes: List[DataPoint], edges: List[tuple]) -> List[Triplet]:
"""
Create Triplet objects from graph nodes and edges.
This function processes graph edges and their corresponding nodes to create
triplet datapoints with embeddable text, similar to the triplet embeddings pipeline.
Parameters:
-----------
- nodes (List[DataPoint]): List of graph nodes extracted from data points
- edges (List[tuple]): List of edge tuples in format
(source_node_id, target_node_id, relationship_name, properties_dict)
Note: All edges including those from DocumentChunk.contains are already extracted
by get_graph_from_model and included in this list.
Returns:
--------
- List[Triplet]: List of Triplet objects ready for indexing
"""
node_map: Dict[str, DataPoint] = {}
for node in nodes:
if hasattr(node, "id"):
node_id = str(node.id)
if node_id not in node_map:
node_map[node_id] = node
triplets = []
skipped_count = 0
seen_ids = set()
for edge_tuple in edges:
if len(edge_tuple) < 4:
continue
source_node_id, target_node_id, relationship_name, edge_properties = (
edge_tuple[0],
edge_tuple[1],
edge_tuple[2],
edge_tuple[3],
)
source_node = node_map.get(str(source_node_id))
target_node = node_map.get(str(target_node_id))
if not source_node or not target_node or relationship_name is None:
skipped_count += 1
continue
source_node_text = _extract_embeddable_text_from_datapoint(source_node)
target_node_text = _extract_embeddable_text_from_datapoint(target_node)
relationship_text = ""
if isinstance(edge_properties, dict):
edge_text = edge_properties.get("edge_text")
if edge_text and isinstance(edge_text, str) and edge_text.strip():
relationship_text = edge_text.strip()
if not relationship_text and relationship_name:
relationship_text = relationship_name
if not source_node_text and not relationship_text and not relationship_name:
skipped_count += 1
continue
embeddable_text = f"{source_node_text} - {relationship_text}-{target_node_text}".strip()
triplet_id = generate_node_id(str(source_node_id) + relationship_name + str(target_node_id))
if triplet_id in seen_ids:
continue
seen_ids.add(triplet_id)
triplets.append(
Triplet(
id=triplet_id,
from_node_id=str(source_node_id),
to_node_id=str(target_node_id),
text=embeddable_text,
)
)
return triplets