cognee 0.3.7.dev2__py3-none-any.whl → 0.3.9__py3-none-any.whl

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.
@@ -16,17 +16,17 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
16
16
 
17
17
  nodes_list = []
18
18
  color_map = {
19
- "Entity": "#f47710",
20
- "EntityType": "#6510f4",
21
- "DocumentChunk": "#801212",
22
- "TextSummary": "#1077f4",
23
- "TableRow": "#f47710",
24
- "TableType": "#6510f4",
25
- "ColumnValue": "#13613a",
26
- "SchemaTable": "#f47710",
27
- "DatabaseSchema": "#6510f4",
28
- "SchemaRelationship": "#13613a",
29
- "default": "#D3D3D3",
19
+ "Entity": "#5C10F4",
20
+ "EntityType": "#A550FF",
21
+ "DocumentChunk": "#0DFF00",
22
+ "TextSummary": "#5C10F4",
23
+ "TableRow": "#A550FF",
24
+ "TableType": "#5C10F4",
25
+ "ColumnValue": "#757470",
26
+ "SchemaTable": "#A550FF",
27
+ "DatabaseSchema": "#5C10F4",
28
+ "SchemaRelationship": "#323332",
29
+ "default": "#D8D8D8",
30
30
  }
31
31
 
32
32
  for node_id, node_info in nodes_data:
@@ -98,16 +98,19 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
98
98
  <head>
99
99
  <meta charset="utf-8">
100
100
  <script src="https://d3js.org/d3.v5.min.js"></script>
101
+ <script src="https://d3js.org/d3-contour.v1.min.js"></script>
101
102
  <style>
102
103
  body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: linear-gradient(90deg, #101010, #1a1a2e); color: white; font-family: 'Inter', sans-serif; }
103
104
 
104
105
  svg { width: 100vw; height: 100vh; display: block; }
105
- .links line { stroke: rgba(255, 255, 255, 0.4); stroke-width: 2px; }
106
- .links line.weighted { stroke: rgba(255, 215, 0, 0.7); }
107
- .links line.multi-weighted { stroke: rgba(0, 255, 127, 0.8); }
108
- .nodes circle { stroke: white; stroke-width: 0.5px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.3)); }
109
- .node-label { font-size: 5px; font-weight: bold; fill: white; text-anchor: middle; dominant-baseline: middle; font-family: 'Inter', sans-serif; pointer-events: none; }
110
- .edge-label { font-size: 3px; fill: rgba(255, 255, 255, 0.7); text-anchor: middle; dominant-baseline: middle; font-family: 'Inter', sans-serif; pointer-events: none; }
106
+ .links line { stroke: rgba(160, 160, 160, 0.25); stroke-width: 1.5px; stroke-linecap: round; }
107
+ .links line.weighted { stroke: rgba(255, 215, 0, 0.4); }
108
+ .links line.multi-weighted { stroke: rgba(0, 255, 127, 0.45); }
109
+ .nodes circle { stroke: white; stroke-width: 0.5px; }
110
+ .node-label { font-size: 5px; font-weight: bold; fill: #F4F4F4; text-anchor: middle; dominant-baseline: middle; font-family: 'Inter', sans-serif; pointer-events: none; }
111
+ .edge-label { font-size: 3px; fill: #F4F4F4; text-anchor: middle; dominant-baseline: middle; font-family: 'Inter', sans-serif; pointer-events: none; paint-order: stroke; stroke: rgba(50,51,50,0.75); stroke-width: 1px; }
112
+
113
+ .density path { mix-blend-mode: screen; }
111
114
 
112
115
  .tooltip {
113
116
  position: absolute;
@@ -125,11 +128,32 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
125
128
  max-width: 300px;
126
129
  word-wrap: break-word;
127
130
  }
131
+ #info-panel {
132
+ position: fixed;
133
+ left: 12px;
134
+ top: 12px;
135
+ width: 340px;
136
+ max-height: calc(100vh - 24px);
137
+ overflow: auto;
138
+ background: rgba(50, 51, 50, 0.7);
139
+ backdrop-filter: blur(6px);
140
+ border: 1px solid rgba(216, 216, 216, 0.35);
141
+ border-radius: 8px;
142
+ color: #F4F4F4;
143
+ padding: 12px 14px;
144
+ z-index: 1100;
145
+ }
146
+ #info-panel h3 { margin: 0 0 8px 0; font-size: 14px; color: #F4F4F4; }
147
+ #info-panel .kv { font-size: 12px; line-height: 1.4; }
148
+ #info-panel .kv .k { color: #D8D8D8; }
149
+ #info-panel .kv .v { color: #F4F4F4; }
150
+ #info-panel .placeholder { opacity: 0.7; font-size: 12px; }
128
151
  </style>
129
152
  </head>
130
153
  <body>
131
154
  <svg></svg>
132
155
  <div class="tooltip" id="tooltip"></div>
156
+ <div id="info-panel"><div class="placeholder">Hover a node or edge to inspect details</div></div>
133
157
  <script>
134
158
  var nodes = {nodes};
135
159
  var links = {links};
@@ -140,19 +164,141 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
140
164
 
141
165
  var container = svg.append("g");
142
166
  var tooltip = d3.select("#tooltip");
167
+ var infoPanel = d3.select('#info-panel');
168
+
169
+ function renderInfo(title, entries){
170
+ function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
171
+ var html = '<h3>' + esc(title) + '</h3>';
172
+ html += '<div class="kv">';
173
+ entries.forEach(function(e){
174
+ html += '<div><span class="k">' + esc(e.k) + ':</span> <span class="v">' + esc(e.v) + '</span></div>';
175
+ });
176
+ html += '</div>';
177
+ infoPanel.html(html);
178
+ }
179
+ function pickDescription(obj){
180
+ if (!obj) return null;
181
+ var keys = ['description','summary','text','content'];
182
+ for (var i=0; i<keys.length; i++){
183
+ var v = obj[keys[i]];
184
+ if (typeof v === 'string' && v.trim()) return v.trim();
185
+ }
186
+ return null;
187
+ }
188
+ function truncate(s, n){ if (!s) return s; return s.length > n ? (s.slice(0, n) + '…') : s; }
189
+ function renderNodeInfo(n){
190
+ var entries = [];
191
+ if (n.name) entries.push({k:'Name', v: n.name});
192
+ if (n.type) entries.push({k:'Type', v: n.type});
193
+ if (n.id) entries.push({k:'ID', v: n.id});
194
+ var desc = pickDescription(n) || pickDescription(n.properties);
195
+ if (desc) entries.push({k:'Description', v: truncate(desc.replace(/\s+/g,' ').trim(), 280)});
196
+ if (n.properties) {
197
+ Object.keys(n.properties).slice(0, 12).forEach(function(key){
198
+ var v = n.properties[key];
199
+ if (v !== undefined && v !== null && typeof v !== 'object') entries.push({k: key, v: String(v)});
200
+ });
201
+ }
202
+ renderInfo(n.name || 'Node', entries);
203
+ }
204
+ function renderEdgeInfo(e){
205
+ var entries = [];
206
+ if (e.relation) entries.push({k:'Relation', v: e.relation});
207
+ if (e.weight !== undefined && e.weight !== null) entries.push({k:'Weight', v: e.weight});
208
+ if (e.all_weights && Object.keys(e.all_weights).length){
209
+ Object.keys(e.all_weights).slice(0, 8).forEach(function(k){ entries.push({k: 'w.'+k, v: e.all_weights[k]}); });
210
+ }
211
+ if (e.relationship_type) entries.push({k:'Type', v: e.relationship_type});
212
+ var edesc = pickDescription(e.edge_info);
213
+ if (edesc) entries.push({k:'Description', v: truncate(edesc.replace(/\s+/g,' ').trim(), 280)});
214
+ renderInfo('Edge', entries);
215
+ }
216
+
217
+ // Basic runtime diagnostics
218
+ console.log('[Cognee Visualization] nodes:', nodes ? nodes.length : 0, 'links:', links ? links.length : 0);
219
+ window.addEventListener('error', function(e){
220
+ try {
221
+ tooltip.html('<strong>Error:</strong> ' + e.message)
222
+ .style('left', '12px')
223
+ .style('top', '12px')
224
+ .style('opacity', 1);
225
+ } catch(_) {}
226
+ });
227
+
228
+ // Normalize node IDs and link endpoints for robustness
229
+ function resolveId(d){ return (d && (d.id || d.node_id || d.uuid || d.external_id || d.name)) || undefined; }
230
+ if (Array.isArray(nodes)) {
231
+ nodes.forEach(function(n){ var id = resolveId(n); if (id !== undefined) n.id = id; });
232
+ }
233
+ if (Array.isArray(links)) {
234
+ links.forEach(function(l){
235
+ if (typeof l.source === 'object') l.source = resolveId(l.source);
236
+ if (typeof l.target === 'object') l.target = resolveId(l.target);
237
+ });
238
+ }
239
+
240
+ if (!nodes || nodes.length === 0) {
241
+ container.append('text')
242
+ .attr('x', width / 2)
243
+ .attr('y', height / 2)
244
+ .attr('fill', '#fff')
245
+ .attr('font-size', 14)
246
+ .attr('text-anchor', 'middle')
247
+ .text('No graph data available');
248
+ }
249
+
250
+ // Visual defs - reusable glow
251
+ var defs = svg.append("defs");
252
+ var glow = defs.append("filter").attr("id", "glow")
253
+ .attr("x", "-30%")
254
+ .attr("y", "-30%")
255
+ .attr("width", "160%")
256
+ .attr("height", "160%");
257
+ glow.append("feGaussianBlur").attr("stdDeviation", 8).attr("result", "coloredBlur");
258
+ var feMerge = glow.append("feMerge");
259
+ feMerge.append("feMergeNode").attr("in", "coloredBlur");
260
+ feMerge.append("feMergeNode").attr("in", "SourceGraphic");
261
+
262
+ // Stronger glow for hovered adjacency
263
+ var glowStrong = defs.append("filter").attr("id", "glow-strong")
264
+ .attr("x", "-40%")
265
+ .attr("y", "-40%")
266
+ .attr("width", "180%")
267
+ .attr("height", "180%");
268
+ glowStrong.append("feGaussianBlur").attr("stdDeviation", 14).attr("result", "coloredBlur");
269
+ var feMerge2 = glowStrong.append("feMerge");
270
+ feMerge2.append("feMergeNode").attr("in", "coloredBlur");
271
+ feMerge2.append("feMergeNode").attr("in", "SourceGraphic");
272
+
273
+ var currentTransform = d3.zoomIdentity;
274
+ var densityZoomTimer = null;
275
+ var isInteracting = false;
276
+ var labelBaseSize = 10;
277
+ function getGroupKey(d){ return d && (d.type || d.category || d.group || d.color) || 'default'; }
143
278
 
144
279
  var simulation = d3.forceSimulation(nodes)
145
- .force("link", d3.forceLink(links).id(d => d.id).strength(0.1))
146
- .force("charge", d3.forceManyBody().strength(-275))
280
+ .force("link", d3.forceLink(links).id(function(d){ return d.id; }).distance(100).strength(0.2))
281
+ .force("charge", d3.forceManyBody().strength(-180))
282
+ .force("collide", d3.forceCollide().radius(16).iterations(2))
147
283
  .force("center", d3.forceCenter(width / 2, height / 2))
148
- .force("x", d3.forceX().strength(0.1).x(width / 2))
149
- .force("y", d3.forceY().strength(0.1).y(height / 2));
284
+ .force("x", d3.forceX().strength(0.06).x(width / 2))
285
+ .force("y", d3.forceY().strength(0.06).y(height / 2))
286
+ .alphaDecay(0.06)
287
+ .velocityDecay(0.6);
288
+
289
+ // Density layer (sibling of container to avoid double transforms)
290
+ var densityLayer = svg.append("g")
291
+ .attr("class", "density")
292
+ .style("pointer-events", "none");
293
+ if (densityLayer.lower) densityLayer.lower();
150
294
 
151
295
  var link = container.append("g")
152
296
  .attr("class", "links")
153
297
  .selectAll("line")
154
298
  .data(links)
155
299
  .enter().append("line")
300
+ .style("opacity", 0)
301
+ .style("pointer-events", "none")
156
302
  .attr("stroke-width", d => {
157
303
  if (d.weight) return Math.max(2, d.weight * 5);
158
304
  if (d.all_weights && Object.keys(d.all_weights).length > 0) {
@@ -168,6 +314,7 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
168
314
  })
169
315
  .on("mouseover", function(d) {
170
316
  // Create tooltip content for edge
317
+ renderEdgeInfo(d);
171
318
  var content = "<strong>Edge Information</strong><br/>";
172
319
  content += "Relationship: " + d.relation + "<br/>";
173
320
 
@@ -212,6 +359,7 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
212
359
  .data(links)
213
360
  .enter().append("text")
214
361
  .attr("class", "edge-label")
362
+ .style("opacity", 0)
215
363
  .text(d => {
216
364
  var label = d.relation;
217
365
  if (d.all_weights && Object.keys(d.all_weights).length > 1) {
@@ -232,21 +380,225 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
232
380
  .data(nodes)
233
381
  .enter().append("g");
234
382
 
383
+ // Color fallback by type when d.color is missing
384
+ var colorByType = {
385
+ "Entity": "#5C10F4",
386
+ "EntityType": "#A550FF",
387
+ "DocumentChunk": "#0DFF00",
388
+ "TextSummary": "#5C10F4",
389
+ "TableRow": "#A550FF",
390
+ "TableType": "#5C10F4",
391
+ "ColumnValue": "#757470",
392
+ "SchemaTable": "#A550FF",
393
+ "DatabaseSchema": "#5C10F4",
394
+ "SchemaRelationship": "#323332"
395
+ };
396
+
235
397
  var node = nodeGroup.append("circle")
236
398
  .attr("r", 13)
237
- .attr("fill", d => d.color)
399
+ .attr("fill", function(d){ return d.color || colorByType[d.type] || "#D3D3D3"; })
400
+ .style("filter", "url(#glow)")
401
+ .attr("shape-rendering", "geometricPrecision")
238
402
  .call(d3.drag()
239
403
  .on("start", dragstarted)
240
- .on("drag", dragged)
404
+ .on("drag", function(d){ dragged(d); updateDensity(); showAdjacency(d); })
241
405
  .on("end", dragended));
242
406
 
243
- nodeGroup.append("text")
407
+ // Show links only for hovered node adjacency
408
+ function isAdjacent(linkDatum, nodeId) {
409
+ var sid = linkDatum && linkDatum.source && (linkDatum.source.id || linkDatum.source);
410
+ var tid = linkDatum && linkDatum.target && (linkDatum.target.id || linkDatum.target);
411
+ return sid === nodeId || tid === nodeId;
412
+ }
413
+
414
+ function showAdjacency(d) {
415
+ var nodeId = d && (d.id || d.node_id || d.uuid || d.external_id || d.name);
416
+ if (!nodeId) return;
417
+ // Build neighbor set
418
+ var neighborIds = {};
419
+ neighborIds[nodeId] = true;
420
+ for (var i = 0; i < links.length; i++) {
421
+ var l = links[i];
422
+ var sid = l && l.source && (l.source.id || l.source);
423
+ var tid = l && l.target && (l.target.id || l.target);
424
+ if (sid === nodeId) neighborIds[tid] = true;
425
+ if (tid === nodeId) neighborIds[sid] = true;
426
+ }
427
+
428
+ link
429
+ .style("opacity", function(l){ return isAdjacent(l, nodeId) ? 0.95 : 0; })
430
+ .style("stroke", function(l){ return isAdjacent(l, nodeId) ? "rgba(255,255,255,0.95)" : null; })
431
+ .style("stroke-width", function(l){ return isAdjacent(l, nodeId) ? 2.5 : 1.5; });
432
+ edgeLabels.style("opacity", function(l){ return isAdjacent(l, nodeId) ? 1 : 0; });
433
+ densityLayer.style("opacity", 0.35);
434
+
435
+ // Highlight neighbor nodes and dim others
436
+ node
437
+ .style("opacity", function(n){ return neighborIds[n.id] ? 1 : 0.25; })
438
+ .style("filter", function(n){ return neighborIds[n.id] ? "url(#glow-strong)" : "url(#glow)"; })
439
+ .attr("r", function(n){ return neighborIds[n.id] ? 15 : 13; });
440
+ // Raise highlighted nodes
441
+ node.filter(function(n){ return neighborIds[n.id]; }).raise();
442
+ // Neighbor labels brighter
443
+ nodeGroup.select("text")
444
+ .style("opacity", function(n){ return neighborIds[n.id] ? 1 : 0.2; })
445
+ .style("font-size", function(n){
446
+ var size = neighborIds[n.id] ? Math.min(22, labelBaseSize * 1.25) : labelBaseSize;
447
+ return size + "px";
448
+ });
449
+ }
450
+
451
+ function clearAdjacency() {
452
+ link.style("opacity", 0)
453
+ .style("stroke", null)
454
+ .style("stroke-width", 1.5);
455
+ edgeLabels.style("opacity", 0);
456
+ densityLayer.style("opacity", 1);
457
+ node
458
+ .style("opacity", 1)
459
+ .style("filter", "url(#glow)")
460
+ .attr("r", 13);
461
+ nodeGroup.select("text")
462
+ .style("opacity", 1)
463
+ .style("font-size", labelBaseSize + "px");
464
+ }
465
+
466
+ node.on("mouseover", function(d){ showAdjacency(d); })
467
+ .on("mouseout", function(){ clearAdjacency(); });
468
+ node.on("mouseover", function(d){ renderNodeInfo(d); tooltip.style('opacity', 0); });
469
+ // Also bind on the group so labels trigger adjacency too
470
+ nodeGroup.on("mouseover", function(d){ showAdjacency(d); })
471
+ .on("mouseout", function(){ clearAdjacency(); });
472
+
473
+ // Density always on; no hover gating
474
+
475
+ // Add labels sparsely to reduce clutter (every ~50th node), and truncate long text
476
+ nodeGroup
477
+ .filter(function(d, i){ return i % 14 === 0; })
478
+ .append("text")
244
479
  .attr("class", "node-label")
245
480
  .attr("dy", 4)
246
481
  .attr("text-anchor", "middle")
247
- .text(d => d.name);
482
+ .text(function(d){
483
+ var s = d && d.name ? String(d.name) : '';
484
+ return s.length > 40 ? (s.slice(0, 40) + "…") : s;
485
+ })
486
+ .style("font-size", labelBaseSize + "px");
487
+
488
+ function applyLabelSize() {
489
+ var k = (currentTransform && currentTransform.k) || 1;
490
+ // Keep labels readable across zoom levels and hide when too small
491
+ labelBaseSize = Math.max(7, Math.min(18, 10 / Math.sqrt(k)));
492
+ nodeGroup.select("text")
493
+ .style("font-size", labelBaseSize + "px")
494
+ .style("display", (k < 0.35 ? "none" : null));
495
+ }
248
496
 
249
- node.append("title").text(d => JSON.stringify(d));
497
+
498
+
499
+ // Density cloud computation (throttled)
500
+ var densityTick = 0;
501
+ var geoPath = d3.geoPath().projection(null);
502
+ var MAX_POINTS_PER_GROUP = 400;
503
+ function updateDensity() {
504
+ try {
505
+ if (isInteracting) return; // skip during interaction for smoother UX
506
+ if (typeof d3 === 'undefined' || typeof d3.contourDensity !== 'function') {
507
+ return; // d3-contour not available; skip gracefully
508
+ }
509
+ if (!nodes || nodes.length === 0) return;
510
+ var usable = nodes.filter(function(d){ return d && typeof d.x === 'number' && isFinite(d.x) && typeof d.y === 'number' && isFinite(d.y); });
511
+ if (usable.length < 3) return; // not enough positioned points yet
512
+
513
+ var t = currentTransform || d3.zoomIdentity;
514
+ if (t.k && t.k < 0.08) {
515
+ // Skip density at extreme zoom-out to avoid numerical instability/perf issues
516
+ densityLayer.selectAll('*').remove();
517
+ return;
518
+ }
519
+
520
+ function hexToRgb(hex){
521
+ if (!hex) return {r: 0, g: 200, b: 255};
522
+ var c = hex.replace('#','');
523
+ if (c.length === 3) c = c.split('').map(function(x){ return x+x; }).join('');
524
+ var num = parseInt(c, 16);
525
+ return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 };
526
+ }
527
+
528
+ // Build groups across all nodes
529
+ var groups = {};
530
+ for (var i = 0; i < usable.length; i++) {
531
+ var k = getGroupKey(usable[i]);
532
+ if (!groups[k]) groups[k] = [];
533
+ groups[k].push(usable[i]);
534
+ }
535
+
536
+ densityLayer.selectAll('*').remove();
537
+
538
+ Object.keys(groups).forEach(function(key){
539
+ var arr = groups[key];
540
+ if (!arr || arr.length < 3) return;
541
+
542
+ // Transform positions into screen space and sample to cap cost
543
+ var arrT = [];
544
+ var step = Math.max(1, Math.floor(arr.length / MAX_POINTS_PER_GROUP));
545
+ for (var j = 0; j < arr.length; j += step) {
546
+ var nx = t.applyX(arr[j].x);
547
+ var ny = t.applyY(arr[j].y);
548
+ if (isFinite(nx) && isFinite(ny)) {
549
+ arrT.push({ x: nx, y: ny, type: arr[j].type, color: arr[j].color });
550
+ }
551
+ }
552
+ if (arrT.length < 3) return;
553
+
554
+ // Compute adaptive bandwidth based on group spread
555
+ var cx = 0, cy = 0;
556
+ for (var k = 0; k < arrT.length; k++){ cx += arrT[k].x; cy += arrT[k].y; }
557
+ cx /= arrT.length; cy /= arrT.length;
558
+ var sumR = 0;
559
+ for (var k2 = 0; k2 < arrT.length; k2++){
560
+ var dx = arrT[k2].x - cx, dy = arrT[k2].y - cy;
561
+ sumR += Math.sqrt(dx*dx + dy*dy);
562
+ }
563
+ var avgR = sumR / arrT.length;
564
+ var dynamicBandwidth = Math.max(12, Math.min(80, avgR));
565
+ var densityBandwidth = dynamicBandwidth / (t.k || 1);
566
+
567
+ var contours = d3.contourDensity()
568
+ .x(function(d){ return d.x; })
569
+ .y(function(d){ return d.y; })
570
+ .size([width, height])
571
+ .bandwidth(densityBandwidth)
572
+ .thresholds(8)
573
+ (arrT);
574
+
575
+ if (!contours || contours.length === 0) return;
576
+ var maxVal = d3.max(contours, function(d){ return d.value; }) || 1;
577
+
578
+ // Use the first node color in the group or fallback neon palette
579
+ var baseColor = (arr.find(function(d){ return d && d.color; }) || {}).color || '#00c8ff';
580
+ var rgb = hexToRgb(baseColor);
581
+
582
+ var g = densityLayer.append('g').attr('data-group', key);
583
+ g.selectAll('path')
584
+ .data(contours)
585
+ .enter()
586
+ .append('path')
587
+ .attr('d', geoPath)
588
+ .attr('fill', 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')')
589
+ .attr('stroke', 'none')
590
+ .style('opacity', function(d){
591
+ var v = maxVal ? (d.value / maxVal) : 0;
592
+ var alpha = Math.pow(Math.max(0, Math.min(1, v)), 1.6); // accentuate clusters
593
+ return 0.65 * alpha; // up to 0.65 opacity at peak density
594
+ })
595
+ .style('filter', 'blur(2px)');
596
+ });
597
+ } catch (e) {
598
+ // Reduce impact of any runtime errors during zoom
599
+ console.warn('Density update failed:', e);
600
+ }
601
+ }
250
602
 
251
603
  simulation.on("tick", function() {
252
604
  link.attr("x1", d => d.source.x)
@@ -266,16 +618,29 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
266
618
  .attr("y", d => d.y)
267
619
  .attr("dy", 4)
268
620
  .attr("text-anchor", "middle");
621
+
622
+ densityTick += 1;
623
+ if (densityTick % 24 === 0) updateDensity();
269
624
  });
270
625
 
271
- svg.call(d3.zoom().on("zoom", function() {
272
- container.attr("transform", d3.event.transform);
273
- }));
626
+ var zoomBehavior = d3.zoom()
627
+ .on("start", function(){ isInteracting = true; densityLayer.style("opacity", 0.2); })
628
+ .on("zoom", function(){
629
+ currentTransform = d3.event.transform;
630
+ container.attr("transform", currentTransform);
631
+ })
632
+ .on("end", function(){
633
+ if (densityZoomTimer) clearTimeout(densityZoomTimer);
634
+ densityZoomTimer = setTimeout(function(){ isInteracting = false; densityLayer.style("opacity", 1); updateDensity(); }, 140);
635
+ });
636
+ svg.call(zoomBehavior);
274
637
 
275
638
  function dragstarted(d) {
276
639
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
277
640
  d.fx = d.x;
278
641
  d.fy = d.y;
642
+ isInteracting = true;
643
+ densityLayer.style("opacity", 0.2);
279
644
  }
280
645
 
281
646
  function dragged(d) {
@@ -287,6 +652,8 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
287
652
  if (!d3.event.active) simulation.alphaTarget(0);
288
653
  d.fx = null;
289
654
  d.fy = null;
655
+ if (densityZoomTimer) clearTimeout(densityZoomTimer);
656
+ densityZoomTimer = setTimeout(function(){ isInteracting = false; densityLayer.style("opacity", 1); updateDensity(); }, 140);
290
657
  }
291
658
 
292
659
  window.addEventListener("resize", function() {
@@ -295,7 +662,13 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
295
662
  svg.attr("width", width).attr("height", height);
296
663
  simulation.force("center", d3.forceCenter(width / 2, height / 2));
297
664
  simulation.alpha(1).restart();
665
+ updateDensity();
666
+ applyLabelSize();
298
667
  });
668
+
669
+ // Initial density draw
670
+ updateDensity();
671
+ applyLabelSize();
299
672
  </script>
300
673
 
301
674
  <svg style="position: fixed; bottom: 10px; right: 10px; width: 150px; height: auto; z-index: 9999;" viewBox="0 0 158 44" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -305,8 +678,12 @@ async def cognee_network_visualization(graph_data, destination_file_path: str =
305
678
  </html>
306
679
  """
307
680
 
308
- html_content = html_template.replace("{nodes}", json.dumps(nodes_list))
309
- html_content = html_content.replace("{links}", json.dumps(links_list))
681
+ # Safely embed JSON inside <script> by escaping </ to avoid prematurely closing the tag
682
+ def _safe_json_embed(obj):
683
+ return json.dumps(obj).replace("</", "<\\/")
684
+
685
+ html_content = html_template.replace("{nodes}", _safe_json_embed(nodes_list))
686
+ html_content = html_content.replace("{links}", _safe_json_embed(links_list))
310
687
 
311
688
  if not destination_file_path:
312
689
  home_dir = os.path.expanduser("~")
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import sys
3
3
  import logging
4
+ import tempfile
4
5
  import structlog
5
6
  import traceback
6
7
  import platform
@@ -76,14 +77,38 @@ log_levels = {
76
77
  # Track if structlog logging has been configured
77
78
  _is_structlog_configured = False
78
79
 
79
- # Path to logs directory
80
- LOGS_DIR = Path(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs"))
81
80
 
82
- try:
83
- LOGS_DIR.mkdir(exist_ok=True) # Create logs dir if it doesn't exist
84
- except Exception as e:
85
- # Note: mkdir cant be used when the file system is read only.
86
- print(f"Warning: Could not create logs directory at {LOGS_DIR}: {e}")
81
+ def resolve_logs_dir():
82
+ """Resolve a writable logs directory.
83
+
84
+ Priority:
85
+ 1) BaseConfig.logs_root_directory (respects COGNEE_LOGS_DIR)
86
+ 2) /tmp/cognee_logs (default, best-effort create)
87
+
88
+ Returns a Path or None if none are writable/creatable.
89
+ """
90
+ from cognee.base_config import get_base_config
91
+
92
+ base_config = get_base_config()
93
+ logs_root_directory = Path(base_config.logs_root_directory)
94
+
95
+ try:
96
+ logs_root_directory.mkdir(parents=True, exist_ok=True)
97
+ if os.access(logs_root_directory, os.W_OK):
98
+ return logs_root_directory
99
+ except Exception:
100
+ pass
101
+
102
+ try:
103
+ tmp_log_path = Path(os.path.join("/tmp", "cognee_logs"))
104
+ tmp_log_path.mkdir(parents=True, exist_ok=True)
105
+ if os.access(tmp_log_path, os.W_OK):
106
+ return tmp_log_path
107
+ except Exception:
108
+ pass
109
+
110
+ return None
111
+
87
112
 
88
113
  # Maximum number of log files to keep
89
114
  MAX_LOG_FILES = 10
@@ -444,15 +469,18 @@ def setup_logging(log_level=None, name=None):
444
469
  # can define their own levels.
445
470
  root_logger.setLevel(logging.NOTSET)
446
471
 
472
+ # Resolve logs directory with env and safe fallbacks
473
+ logs_dir = resolve_logs_dir()
474
+
447
475
  # Check if we already have a log file path from the environment
448
476
  # NOTE: environment variable must be used here as it allows us to
449
477
  # log to a single file with a name based on a timestamp in a multiprocess setting.
450
478
  # Without it, we would have a separate log file for every process.
451
479
  log_file_path = os.environ.get("LOG_FILE_NAME")
452
- if not log_file_path:
480
+ if not log_file_path and logs_dir is not None:
453
481
  # Create a new log file name with the cognee start time
454
482
  start_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
455
- log_file_path = os.path.join(LOGS_DIR, f"{start_time}.log")
483
+ log_file_path = str((logs_dir / f"{start_time}.log").resolve())
456
484
  os.environ["LOG_FILE_NAME"] = log_file_path
457
485
 
458
486
  try:
@@ -478,7 +506,8 @@ def setup_logging(log_level=None, name=None):
478
506
  )
479
507
 
480
508
  # Clean up old log files, keeping only the most recent ones
481
- cleanup_old_logs(LOGS_DIR, MAX_LOG_FILES)
509
+ if logs_dir is not None:
510
+ cleanup_old_logs(logs_dir, MAX_LOG_FILES)
482
511
 
483
512
  # Mark logging as configured
484
513
  _is_structlog_configured = True
@@ -502,6 +531,10 @@ def setup_logging(log_level=None, name=None):
502
531
 
503
532
  # Get a configured logger and log system information
504
533
  logger = structlog.get_logger(name if name else __name__)
534
+
535
+ if logs_dir is not None:
536
+ logger.info(f"Log file created at: {log_file_path}", log_file=log_file_path)
537
+
505
538
  # Detailed initialization for regular usage
506
539
  logger.info(
507
540
  "Logging initialized",