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

188 lines
8.0 KiB
Python

"""Schema and entity inventory over the knowledge graph.
Summarizes the graph by semantic type: per-type instance counts, representative
sample names, and the per-pair relationship distribution. Composes the verified
building blocks ``get_graph_engine`` and ``get_graph_data`` and reads node/edge
fields using their exact shapes.
A node's semantic type is its ``type`` property, except for extracted entities
whose ``type`` is the literal ``"Entity"``. For those, the semantic type is
resolved by following the ``is_a`` edge (Entity -> EntityType) to the target
EntityType node's ``name``.
"""
from typing import Any
from uuid import UUID
from cognee.context_global_variables import set_database_global_context_variables
from cognee.infrastructure.databases.graph.get_graph_engine import get_graph_engine
# Relationship name and direction of the Entity -> EntityType edge (verified).
ENTITY_TYPE_RELATION = "is_a"
# Internal graph taxonomy types that must not appear as separate type groups in
# the inventory. EntityType nodes act as semantic-type labels — their role is
# resolved onto the Entity instances via the is_a edge, so surfacing them as a
# separate group would double-count. Filtering them here also drops is_a edges
# from the relationship distribution (their target is always an EntityType node).
_INTERNAL_TYPES: frozenset[str] = frozenset({"EntityType"})
def _resolve_node_types(
nodes: list[tuple[str, dict[str, Any]]],
edges: list[tuple[str, str, str, dict[str, Any]]],
) -> dict[str, str | None]:
"""Map each node id to its semantic type name.
Non-Entity nodes keep their ``type`` property. Entity nodes (``type ==
"Entity"``) resolve to the EntityType ``name`` reached via the ``is_a`` edge.
"""
node_props = {node_id: props for node_id, props in nodes}
# Collect EntityType target name for each Entity source via the is_a edge
entity_type_name: dict[str, str | None] = {}
for source_id, target_id, relation, _ in edges:
if relation == ENTITY_TYPE_RELATION and target_id in node_props:
entity_type_name[source_id] = node_props[target_id].get("name")
node_type: dict[str, str | None] = {}
for node_id, props in nodes:
raw_type = props.get("type")
if raw_type == "Entity" and node_id in entity_type_name:
node_type[node_id] = entity_type_name[node_id]
else:
node_type[node_id] = raw_type
return node_type
def _compute_degrees(
node_ids: list[str] | set[str],
edges: list[tuple[str, str, str, dict[str, Any]]],
) -> dict[str, int]:
"""Count edges touching each node (undirected degree) from the edge list."""
degree: dict[str, int] = {node_id: 0 for node_id in node_ids}
for source_id, target_id, _, _ in edges:
if source_id in degree:
degree[source_id] += 1
if target_id in degree:
degree[target_id] += 1
return degree
VALID_SORTS: frozenset[str] = frozenset({"count", "none"})
async def get_schema_inventory(
dataset: str | UUID | None = None,
samples_per_type: int = 5,
sort: str = "count",
) -> list[dict[str, Any]]:
"""Summarize the knowledge graph by semantic type.
Parameters:
dataset: optional dataset id/name to scope the graph databases to.
When set, scoping mirrors the visualize router via
``set_database_global_context_variables``.
samples_per_type: maximum number of sample instance names per type.
sort: one of ``VALID_SORTS``. ``"count"`` (default) orders types by
descending count, then type name; ``"none"`` preserves discovery
order. Any other value raises ``ValueError``.
Returns:
A list of dicts with keys ``type``, ``count``, ``samples``,
``sample_size``, and ``relationships``. Each ``relationships`` entry is
``{"to_type", "relation", "count"}`` aggregated over edges.
"""
if samples_per_type < 0:
raise ValueError("samples_per_type must be non-negative")
if sort not in VALID_SORTS:
raise ValueError(f"sort must be one of {sorted(VALID_SORTS)}, got {sort!r}")
if dataset is not None:
# Scope graph databases to the dataset, mirroring the visualize router.
# String dataset names cannot resolve to an owner_id; skip scoping for them
# rather than calling the context manager with a None owner (which would raise).
owner_id = await _resolve_dataset_owner(dataset)
if owner_id is not None:
async with set_database_global_context_variables(dataset, owner_id):
return await _build_inventory(samples_per_type, sort)
return await _build_inventory(samples_per_type, sort)
async def _resolve_dataset_owner(dataset: str | UUID) -> UUID | None:
"""Return the owner id for a dataset, or None when it cannot be resolved."""
from cognee.infrastructure.databases.relational import get_relational_engine
from cognee.modules.data.models import Dataset
if not isinstance(dataset, UUID):
return None
db_engine = get_relational_engine()
async with db_engine.get_async_session() as session:
record = await session.get(Dataset, dataset)
return record.owner_id if record else None
async def _build_inventory(samples_per_type: int, sort: str) -> list[dict[str, Any]]:
"""Fetch graph data and assemble the per-type inventory."""
graph_engine = await get_graph_engine()
nodes, edges = await graph_engine.get_graph_data()
node_type = _resolve_node_types(nodes, edges)
node_name = {node_id: props.get("name") for node_id, props in nodes}
degree = _compute_degrees(node_type.keys(), edges)
# Group node ids by semantic type, skipping internal taxonomy types
ids_by_type: dict[str, list[str]] = {}
for node_id, type_name in node_type.items():
if type_name in _INTERNAL_TYPES:
continue
ids_by_type.setdefault(type_name, []).append(node_id)
# Aggregate relationship counts keyed by (source_type, target_type, relation).
# Skip edges where either endpoint is an internal type.
relation_counts: dict[tuple[str, str, str], int] = {}
for source_id, target_id, relation, _ in edges:
source_type = node_type.get(source_id)
target_type = node_type.get(target_id)
if source_type is None or target_type is None:
continue
if source_type in _INTERNAL_TYPES or target_type in _INTERNAL_TYPES:
continue
key = (source_type, relation, target_type)
relation_counts[key] = relation_counts.get(key, 0) + 1
# Build outgoing relationships per type AND incoming (so types like DocumentChunk
# whose primary connections are incoming are not shown as isolated nodes).
relationships_by_type: dict[str, list[dict[str, Any]]] = {}
for (source_type, relation, target_type), count in relation_counts.items():
outgoing = {"to_type": target_type, "relation": relation, "count": count}
relationships_by_type.setdefault(source_type, []).append(outgoing)
incoming = {"to_type": source_type, "relation": f"\u2190 {relation}", "count": count}
relationships_by_type.setdefault(target_type, []).append(incoming)
# Build one inventory record per semantic type
inventory = []
for type_name, node_ids in ids_by_type.items():
# Sort instances by descending degree, then name as a stable tiebreaker
ranked = sorted(node_ids, key=lambda nid: (-degree[nid], node_name.get(nid) or ""))
samples = [node_name[nid] for nid in ranked[:samples_per_type]]
relationships = sorted(
relationships_by_type.get(type_name, []),
key=lambda rel: (-rel["count"], rel["to_type"] or "", rel["relation"]),
)
inventory.append(
{
"type": type_name,
"count": len(node_ids),
"samples": samples,
"sample_size": len(samples),
"relationships": relationships,
}
)
if sort == "count":
inventory.sort(key=lambda rec: (-rec["count"], rec["type"] or ""))
return inventory