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
228 lines
9.8 KiB
Python
228 lines
9.8 KiB
Python
"""Cognee graph visualization — orchestrator.
|
|
|
|
Single entry point ``cognee_network_visualization(graph_data, ...)``:
|
|
|
|
1. Preprocesses the raw graph into a renderer-facing snapshot
|
|
(``preprocessor.preprocess``).
|
|
2. Reads the HTML shell from ``template.html``.
|
|
3. Asks each view module (``views/*``) and layout module (``layouts/*``)
|
|
to emit its JS chunk.
|
|
4. Substitutes ``__TOKEN__`` placeholders — JS chunks first, then the
|
|
JSON data payloads — and writes the final HTML.
|
|
|
|
The split into views/ + layouts/ + preprocessor + template is the
|
|
Phase 1c refactor described in the plan
|
|
(``/Users/vasilije/.claude/plans/floating-snacking-waffle.md``). It
|
|
preserves the public API for backward compatibility — every existing
|
|
caller of ``cognee_network_visualization`` or
|
|
``aggregate_multi_user_graphs`` continues to work without change.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from typing import Optional
|
|
|
|
from cognee.shared.logging_utils import get_logger
|
|
from cognee.infrastructure.files.storage.LocalFileStorage import LocalFileStorage
|
|
from cognee.modules.visualization.preprocessor import preprocess
|
|
from cognee.modules.visualization.views import (
|
|
inspector,
|
|
memory_map,
|
|
schema_view,
|
|
semantic_map,
|
|
story_view,
|
|
ui_chrome,
|
|
)
|
|
from cognee.modules.visualization.layouts import pipeline_layout, semantic_layout
|
|
from cognee.modules.visualization.embedding_join import fetch_node_embeddings, select_nodes
|
|
from cognee.modules.visualization.semantic_clusters import compute_clusters
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
async def _semantic_payload(pre) -> tuple[Optional[dict], Optional[dict]]:
|
|
"""Best-effort semantic positions + clusters. Never blocks the classic render.
|
|
|
|
Returns ``(positions, clusters)`` or ``(None, None)`` when they can't be
|
|
computed — in which case the semantic tab shows a friendly empty state.
|
|
Bounded: the layout and clustering run on the same capped node sample as
|
|
the embedding fetch (``select_nodes``).
|
|
"""
|
|
try:
|
|
nodes = select_nodes(pre.nodes)
|
|
embeddings = await fetch_node_embeddings(nodes)
|
|
if not embeddings:
|
|
return None, None
|
|
# PCA is the deterministic zero-dependency default; set
|
|
# SEMANTIC_MAP_PROJECTION=umap to opt in when umap-learn is installed
|
|
# (silently falls back to PCA when it isn't).
|
|
method = os.environ.get("SEMANTIC_MAP_PROJECTION", "pca").strip().lower()
|
|
positions = semantic_layout.compute_positions(nodes, pre.links, embeddings, method=method)
|
|
clusters = compute_clusters(nodes, embeddings)
|
|
return positions, clusters
|
|
except Exception as exc:
|
|
logger.warning("Semantic map: payload computation failed (%s); tab shows empty state.", exc)
|
|
return None, None
|
|
|
|
|
|
_TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "template.html")
|
|
|
|
|
|
def _safe_json_embed(obj) -> str:
|
|
"""JSON-encode while neutralising ``</`` so the result is safe to
|
|
embed inside a ``<script>`` element."""
|
|
return json.dumps(obj).replace("</", "<\\/")
|
|
|
|
|
|
def _read_template() -> str:
|
|
with open(_TEMPLATE_PATH, "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
async def cognee_network_visualization(
|
|
graph_data,
|
|
destination_file_path: Optional[str] = None,
|
|
schema_data: Optional[dict] = None,
|
|
search_events: Optional[list] = None,
|
|
) -> str:
|
|
"""Render the graph to a self-contained HTML file and return the HTML.
|
|
|
|
Args:
|
|
graph_data: ``(nodes_data, edges_data)`` tuple as produced by
|
|
``GraphDBInterface.get_graph_data()``.
|
|
destination_file_path: Where to write the HTML. Defaults to
|
|
``~/graph_visualization.html``.
|
|
schema_data: Optional pre-built schema payload. When absent, the
|
|
preprocessor derives a type-graph from the nodes/links it sees.
|
|
search_events: Optional list of operation events shown on the Memory
|
|
tab's timeline. Two kinds::
|
|
|
|
{"kind": "search", "time": "2026-06-10T10:31:02",
|
|
"qa_id": "...", "question": "...", "answer": "...",
|
|
"node_ids": ["uuid", ...], "edge_ids": ["uuid", ...]}
|
|
|
|
{"kind": "improve", "time": "...", "qa_id": "...",
|
|
"question": "...", "rating": 5, "feedback_text": "...",
|
|
"applied": true,
|
|
"node_ids": ["uuid", ...], "edge_ids": ["uuid", ...]}
|
|
|
|
``search`` renders a retrieval spotlight; ``improve`` renders a
|
|
reinforcement overlay (the elements whose feedback_weight the
|
|
rated answer updated — green for positive ratings, amber for
|
|
negative). Entries without ``kind`` default to ``search``.
|
|
|
|
No cache is read here — ``visualize_graph(include_session_events
|
|
=True)`` collects these automatically from the session layer via
|
|
``cognee.modules.visualization.session_events``; pass them
|
|
explicitly only for custom pipelines.
|
|
|
|
Returns:
|
|
The full HTML as a string.
|
|
"""
|
|
pre = preprocess(graph_data, schema_data=schema_data)
|
|
|
|
# Best-effort semantic layout; guarded so it never blocks the classic render.
|
|
semantic_positions, semantic_clusters = await _semantic_payload(pre)
|
|
|
|
html = _read_template()
|
|
|
|
# 1) JS chunks: ordered so the first script block (ui_chrome + schema)
|
|
# runs before the main story-view IIFE in the second block.
|
|
html = html.replace("__UI_CHROME_JS__", ui_chrome.emit_js(pre))
|
|
html = html.replace("__SCHEMA_VIEW_JS__", schema_view.emit_js(pre))
|
|
html = html.replace("__STORY_VIEW_JS__", story_view.emit_js(pre))
|
|
html = html.replace("__PIPELINE_LAYOUT_JS__", pipeline_layout.emit_js(pre))
|
|
html = html.replace("__INSPECTOR_JS__", inspector.emit_js(pre))
|
|
html = html.replace("__MEMORY_VIEW_JS__", memory_map.emit_js(pre))
|
|
html = html.replace("__SEMANTIC_LAYOUT_JS__", semantic_layout.emit_js(pre))
|
|
html = html.replace("__SEMANTIC_VIEW_JS__", semantic_map.emit_js(pre))
|
|
|
|
# 2) Data tokens: substituted last so JSON-embedded ``__SCHEMA_GRAPH_DATA__``
|
|
# inside the schema JS chunk gets resolved correctly.
|
|
html = html.replace("__NODES_DATA__", _safe_json_embed(pre.nodes))
|
|
html = html.replace("__LINKS_DATA__", _safe_json_embed(pre.links))
|
|
html = html.replace("__TASK_COLORS__", _safe_json_embed(pre.color_maps["task"]))
|
|
html = html.replace("__PIPELINE_COLORS__", _safe_json_embed(pre.color_maps["pipeline"]))
|
|
html = html.replace("__NODESET_COLORS__", _safe_json_embed(pre.color_maps["node_set"]))
|
|
html = html.replace("__USER_COLORS__", _safe_json_embed(pre.color_maps["user"]))
|
|
html = html.replace(
|
|
"__SCHEMA_DATA__",
|
|
_safe_json_embed(schema_data) if schema_data else "null",
|
|
)
|
|
html = html.replace(
|
|
"__SCHEMA_GRAPH_DATA__",
|
|
_safe_json_embed(pre.schema_graph or {"nodes": [], "links": []}),
|
|
)
|
|
# Unconditional, JSON-fallback substitutions: a leaked __MEMORY_DATA__ /
|
|
# __SEARCH_EVENTS__ token would fail the no-placeholder assembly test.
|
|
html = html.replace("__MEMORY_DATA__", _safe_json_embed(pre.memory_map or {}))
|
|
html = html.replace("__SEARCH_EVENTS__", _safe_json_embed(search_events or []))
|
|
# Semantic tokens: null when there are no embeddings, so the tab renders a
|
|
# friendly empty state without leaving a placeholder behind.
|
|
html = html.replace(
|
|
"__SEMANTIC_POSITIONS__",
|
|
_safe_json_embed(semantic_positions) if semantic_positions else "null",
|
|
)
|
|
html = html.replace(
|
|
"__SEMANTIC_CLUSTERS__",
|
|
_safe_json_embed(semantic_clusters) if semantic_clusters else "null",
|
|
)
|
|
|
|
if not destination_file_path:
|
|
destination_file_path = os.path.join(os.path.expanduser("~"), "graph_visualization.html")
|
|
|
|
dir_path = os.path.dirname(destination_file_path)
|
|
file_name = os.path.basename(destination_file_path)
|
|
file_storage = LocalFileStorage(dir_path)
|
|
await file_storage.store(file_name, html, overwrite=True)
|
|
|
|
logger.info(f"Graph visualization saved as {destination_file_path}")
|
|
|
|
return html
|
|
|
|
|
|
async def aggregate_multi_user_graphs(user_dataset_pairs):
|
|
"""Aggregate graph data from multiple user+dataset pairs into a single graph.
|
|
|
|
Args:
|
|
user_dataset_pairs: list of ``(user, dataset)`` tuples where ``user``
|
|
is a User model instance and ``dataset`` is a Dataset model instance.
|
|
|
|
Returns:
|
|
A tuple ``(nodes_data, edges_data)`` in the same format as
|
|
``get_graph_data()``, with nodes tagged with ``source_user`` from
|
|
the owning user's email.
|
|
"""
|
|
from cognee.infrastructure.databases.graph import get_graph_engine
|
|
from cognee.context_global_variables import set_database_global_context_variables
|
|
|
|
all_nodes: dict = {}
|
|
all_edges: list = []
|
|
seen_edges: set = set()
|
|
|
|
for user, dataset in user_dataset_pairs:
|
|
async with set_database_global_context_variables(dataset.id, user.id):
|
|
graph_engine = await get_graph_engine()
|
|
nodes_data, edges_data = await graph_engine.get_graph_data()
|
|
|
|
user_label = getattr(user, "email", None) or str(user.id)
|
|
|
|
for node_id, node_info in nodes_data:
|
|
node_key = str(node_id)
|
|
if node_key not in all_nodes:
|
|
node_info = (
|
|
dict(node_info) if not isinstance(node_info, dict) else node_info.copy()
|
|
)
|
|
if not node_info.get("source_user"):
|
|
node_info["source_user"] = user_label
|
|
all_nodes[node_key] = (node_id, node_info)
|
|
|
|
for edge in edges_data:
|
|
source, target, relation = edge[0], edge[1], edge[2]
|
|
edge_key = (str(source), str(target), relation)
|
|
if edge_key not in seen_edges:
|
|
seen_edges.add(edge_key)
|
|
all_edges.append(edge)
|
|
|
|
return (list(all_nodes.values()), all_edges)
|