wu981526092's picture
🚀 Deploy AgentGraph: Complete agent monitoring and knowledge graph system
c2ea5ed
import React, { useMemo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { useAgentGraph } from "@/context/AgentGraphContext";
import { Network, BarChart3, Target, Activity } from "lucide-react";
interface ConnectedComponent {
id: number;
nodeIds: string[];
size: number;
density: number;
maxDegree: number;
avgDegree: number;
}
interface DegreeDistribution {
[degree: number]: {
count: number;
percentage: number;
entityTypes: string[];
};
}
interface StructuralPattern {
type: "star" | "chain" | "cluster" | "hub" | "isolated";
description: string;
nodeIds: string[];
centralNodeId?: string;
score: number;
}
interface StructuralData {
totalNodes: number;
totalEdges: number;
networkDensity: number;
avgDegree: number;
maxDegree: number;
connectedComponents: ConnectedComponent[];
degreeDistribution: DegreeDistribution;
structuralPatterns: StructuralPattern[];
networkHealth: number;
isolatedNodes: string[];
}
export function StructuralConnectivityAnalysis() {
const { state } = useAgentGraph();
const structuralData = useMemo<StructuralData>(() => {
const nodeMap = new Map<
string,
{ id: string; type: string; name: string }
>();
const adjacencyList = new Map<string, Set<string>>();
const edgeSet = new Set<string>();
// Process all traces and their knowledge graphs
state.traces.forEach((trace) => {
trace.knowledge_graphs?.forEach((kg: any) => {
if (kg.graph_data) {
try {
const graphData =
typeof kg.graph_data === "string"
? JSON.parse(kg.graph_data)
: kg.graph_data;
// Build node map
if (graphData.entities && Array.isArray(graphData.entities)) {
graphData.entities.forEach((entity: any) => {
nodeMap.set(entity.id, {
id: entity.id,
type: entity.type || "Unknown",
name: entity.name || entity.id,
});
adjacencyList.set(entity.id, new Set());
});
}
// Build adjacency list and count edges
if (graphData.relations && Array.isArray(graphData.relations)) {
graphData.relations.forEach((relation: any) => {
const sourceId = relation.source;
const targetId = relation.target;
const edgeKey = [sourceId, targetId].sort().join("-");
if (
nodeMap.has(sourceId) &&
nodeMap.has(targetId) &&
!edgeSet.has(edgeKey)
) {
adjacencyList.get(sourceId)?.add(targetId);
adjacencyList.get(targetId)?.add(sourceId);
edgeSet.add(edgeKey);
}
});
}
} catch (error) {
console.warn("Error parsing graph_data:", error);
}
}
});
});
const totalNodes = nodeMap.size;
const totalEdges = edgeSet.size;
// Calculate basic metrics
const networkDensity =
totalNodes > 1 ? (2 * totalEdges) / (totalNodes * (totalNodes - 1)) : 0;
const degrees = Array.from(adjacencyList.values()).map(
(neighbors) => neighbors.size
);
const avgDegree =
degrees.length > 0
? Math.round(
(degrees.reduce((sum, d) => sum + d, 0) / degrees.length) * 10
) / 10
: 0;
const maxDegree = Math.max(...degrees, 0);
// Find connected components using DFS
const visited = new Set<string>();
const connectedComponents: ConnectedComponent[] = [];
const dfs = (nodeId: string, component: string[]): void => {
if (visited.has(nodeId)) return;
visited.add(nodeId);
component.push(nodeId);
const neighbors = adjacencyList.get(nodeId) || new Set();
neighbors.forEach((neighborId) => {
if (!visited.has(neighborId)) {
dfs(neighborId, component);
}
});
};
let componentId = 0;
nodeMap.forEach((_, nodeId) => {
if (!visited.has(nodeId)) {
const component: string[] = [];
dfs(nodeId, component);
if (component.length > 0) {
const componentEdges =
component.reduce((count, nodeId) => {
const neighbors = adjacencyList.get(nodeId) || new Set();
const internalNeighbors = Array.from(neighbors).filter((n) =>
component.includes(n)
);
return count + internalNeighbors.length;
}, 0) / 2; // Divide by 2 since each edge is counted twice
const componentDensity =
component.length > 1
? (2 * componentEdges) /
(component.length * (component.length - 1))
: 0;
const componentDegrees = component.map(
(nodeId) => (adjacencyList.get(nodeId) || new Set()).size
);
connectedComponents.push({
id: componentId++,
nodeIds: component,
size: component.length,
density: Math.round(componentDensity * 1000) / 1000,
maxDegree: Math.max(...componentDegrees, 0),
avgDegree:
componentDegrees.length > 0
? Math.round(
(componentDegrees.reduce((sum, d) => sum + d, 0) /
componentDegrees.length) *
10
) / 10
: 0,
});
}
}
});
// Calculate degree distribution
const degreeDistribution: DegreeDistribution = {};
Array.from(nodeMap.entries()).forEach(([nodeId, node]) => {
const degree = (adjacencyList.get(nodeId) || new Set()).size;
if (!degreeDistribution[degree]) {
degreeDistribution[degree] = {
count: 0,
percentage: 0,
entityTypes: [],
};
}
degreeDistribution[degree].count++;
if (!degreeDistribution[degree].entityTypes.includes(node.type)) {
degreeDistribution[degree].entityTypes.push(node.type);
}
});
// Calculate percentages
Object.values(degreeDistribution).forEach((dist) => {
dist.percentage =
totalNodes > 0 ? Math.round((dist.count / totalNodes) * 100) : 0;
});
// Identify structural patterns
const structuralPatterns: StructuralPattern[] = [];
// Find star patterns (one central node with many connections, others mostly connected to center)
Array.from(nodeMap.keys()).forEach((nodeId) => {
const neighbors = adjacencyList.get(nodeId) || new Set();
if (neighbors.size >= 3) {
// Check if this is a potential star center
const neighborDegrees = Array.from(neighbors).map(
(nId) => (adjacencyList.get(nId) || new Set()).size
);
const avgNeighborDegree =
neighborDegrees.reduce((sum, d) => sum + d, 0) /
neighborDegrees.length;
if (neighbors.size >= avgNeighborDegree * 1.5) {
structuralPatterns.push({
type: "star",
description: `Star pattern centered on ${
nodeMap.get(nodeId)?.name
}`,
nodeIds: [nodeId, ...Array.from(neighbors)],
centralNodeId: nodeId,
score: neighbors.size,
});
}
}
});
// Find hub patterns (nodes with significantly higher degree than average)
const degreeThreshold = avgDegree * 2;
Array.from(nodeMap.keys()).forEach((nodeId) => {
const degree = (adjacencyList.get(nodeId) || new Set()).size;
if (degree >= degreeThreshold && degree >= 4) {
structuralPatterns.push({
type: "hub",
description: `High-degree hub: ${nodeMap.get(nodeId)?.name}`,
nodeIds: [nodeId],
centralNodeId: nodeId,
score: degree,
});
}
});
// Find chain patterns (nodes with degree 2 in sequence)
const visited_chains = new Set<string>();
Array.from(nodeMap.keys()).forEach((nodeId) => {
if (visited_chains.has(nodeId)) return;
const neighbors = adjacencyList.get(nodeId) || new Set();
if (neighbors.size === 2) {
// Start building a chain
const chain = [nodeId];
visited_chains.add(nodeId);
let current: string = nodeId;
let prev: string | null = null;
// Follow the chain
let maxChainLength = 50; // Prevent infinite loops
while (maxChainLength-- > 0) {
const currentNeighbors = Array.from(
adjacencyList.get(current) || new Set<string>()
);
const next = currentNeighbors.find((n: string) => n !== prev);
if (!next || visited_chains.has(next)) break;
const nextNeighbors = adjacencyList.get(next) || new Set();
if (nextNeighbors.size > 2) break; // End of chain
chain.push(next);
visited_chains.add(next);
prev = current;
current = next;
if (nextNeighbors.size === 1) break; // End node
}
if (chain.length >= 3) {
structuralPatterns.push({
type: "chain",
description: `Chain of ${chain.length} nodes`,
nodeIds: chain,
score: chain.length,
});
}
}
});
// Find isolated nodes
const isolatedNodes = Array.from(nodeMap.keys()).filter(
(nodeId) => (adjacencyList.get(nodeId) || new Set()).size === 0
);
// Calculate network health (0-100)
const healthFactors = {
connectivity:
connectedComponents.length === 1
? 100
: Math.max(0, 100 - connectedComponents.length * 10),
isolation:
isolatedNodes.length === 0
? 100
: Math.max(0, 100 - isolatedNodes.length * 20),
density: Math.min(100, networkDensity * 200), // Scale density to 0-100
distribution:
avgDegree > 0
? Math.min(100, (1 - (maxDegree - avgDegree) / maxDegree) * 100)
: 100,
};
const networkHealth = Math.round(
healthFactors.connectivity * 0.3 +
healthFactors.isolation * 0.3 +
healthFactors.density * 0.2 +
healthFactors.distribution * 0.2
);
// Sort patterns by score
structuralPatterns.sort((a, b) => b.score - a.score);
return {
totalNodes,
totalEdges,
networkDensity: Math.round(networkDensity * 1000) / 1000,
avgDegree,
maxDegree,
connectedComponents: connectedComponents.sort((a, b) => b.size - a.size),
degreeDistribution,
structuralPatterns: structuralPatterns.slice(0, 6),
networkHealth,
isolatedNodes,
};
}, [state.traces]);
const getHealthColor = (health: number) => {
if (health >= 80) return "text-green-600";
if (health >= 60) return "text-yellow-600";
if (health >= 40) return "text-orange-600";
return "text-red-600";
};
const getPatternColor = (type: string) => {
const colorMap: { [key: string]: string } = {
star: "bg-yellow-100 text-yellow-800 border-yellow-200",
hub: "bg-red-100 text-red-800 border-red-200",
chain: "bg-blue-100 text-blue-800 border-blue-200",
cluster: "bg-green-100 text-green-800 border-green-200",
isolated: "bg-gray-100 text-gray-800 border-gray-200",
};
return colorMap[type] || "bg-gray-100 text-gray-800 border-gray-200";
};
const getTypeColor = (index: number) => {
const colors = [
"bg-blue-500",
"bg-green-500",
"bg-yellow-500",
"bg-purple-500",
"bg-pink-500",
"bg-indigo-500",
];
return colors[index % colors.length];
};
if (structuralData.totalNodes === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Network className="h-5 w-5" />
Structural Connectivity Analysis
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center py-8 text-muted-foreground">
<Network className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No structural data available</p>
<p className="text-sm">
Generate some knowledge graphs to see connectivity analysis
</p>
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-6">
{/* Summary Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-100">
<Network className="h-4 w-4 text-blue-600" />
</div>
<div>
<p className="text-sm text-muted-foreground">Network Density</p>
<p className="text-2xl font-bold">
{structuralData.networkDensity}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-green-100">
<Target className="h-4 w-4 text-green-600" />
</div>
<div>
<p className="text-sm text-muted-foreground">Components</p>
<p className="text-2xl font-bold">
{structuralData.connectedComponents.length}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-purple-100">
<BarChart3 className="h-4 w-4 text-purple-600" />
</div>
<div>
<p className="text-sm text-muted-foreground">Avg Degree</p>
<p className="text-2xl font-bold">{structuralData.avgDegree}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-orange-100">
<Activity className="h-4 w-4 text-orange-600" />
</div>
<div>
<p className="text-sm text-muted-foreground">Network Health</p>
<p
className={`text-2xl font-bold ${getHealthColor(
structuralData.networkHealth
)}`}
>
{structuralData.networkHealth}%
</p>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Connected Components */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Target className="h-5 w-5" />
Connected Components
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{structuralData.connectedComponents
.slice(0, 6)
.map((component, index) => (
<div
key={component.id}
className="flex items-center justify-between p-3 rounded-lg border"
>
<div className="flex items-center gap-3">
<div
className={`w-2 h-2 rounded-full ${getTypeColor(
index
)}`}
/>
<div>
<p className="font-medium">
Component {component.id + 1}
</p>
<p className="text-sm text-muted-foreground">
{component.size} nodes • Density: {component.density}
</p>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Badge variant="outline">
Max: {component.maxDegree}
</Badge>
<Badge variant="outline">
Avg: {component.avgDegree}
</Badge>
</div>
</div>
))}
{structuralData.isolatedNodes.length > 0 && (
<div className="flex items-center justify-between p-3 rounded-lg border border-dashed border-gray-300">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-gray-400" />
<div>
<p className="font-medium text-gray-600">
Isolated Nodes
</p>
<p className="text-sm text-muted-foreground">
{structuralData.isolatedNodes.length} disconnected nodes
</p>
</div>
</div>
<Badge className="bg-gray-100 text-gray-800 border-gray-200">
Isolated
</Badge>
</div>
)}
</div>
</CardContent>
</Card>
{/* Structural Patterns */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Structural Patterns
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{structuralData.structuralPatterns.length > 0 ? (
structuralData.structuralPatterns.map((pattern, index) => (
<div
key={index}
className="flex items-center justify-between p-3 rounded-lg border"
>
<div className="flex items-center gap-3">
<div
className={`w-2 h-2 rounded-full ${getTypeColor(
index
)}`}
/>
<div>
<p className="font-medium capitalize">
{pattern.type} Pattern
</p>
<p
className="text-sm text-muted-foreground truncate"
title={pattern.description}
>
{pattern.description}
</p>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Badge className={getPatternColor(pattern.type)}>
{pattern.type.toUpperCase()}
</Badge>
<Badge variant="outline">
{Math.round(pattern.score)}
</Badge>
</div>
</div>
))
) : (
<div className="text-center py-4 text-muted-foreground">
<p className="text-sm">No significant patterns detected</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
{/* Degree Distribution */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Degree Distribution
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{Object.entries(structuralData.degreeDistribution)
.sort(([a], [b]) => parseInt(a) - parseInt(b))
.slice(0, 8)
.map(([degree, stats], index) => (
<div key={degree} className="p-4 rounded-lg border">
<div className="flex items-center gap-2 mb-2">
<div
className={`w-3 h-3 rounded-full ${getTypeColor(index)}`}
/>
<h4 className="font-medium">Degree {degree}</h4>
</div>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Nodes:</span>
<span>{stats.count}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Percentage:</span>
<span>{stats.percentage}%</span>
</div>
<div className="mt-2">
<p className="text-muted-foreground text-xs mb-1">
Entity Types:
</p>
<div className="flex flex-wrap gap-1">
{stats.entityTypes
.slice(0, 3)
.map((type: string, _typeIndex: number) => (
<Badge
key={type}
variant="outline"
className="text-xs"
>
{type}
</Badge>
))}
{stats.entityTypes.length > 3 && (
<Badge variant="outline" className="text-xs">
+{stats.entityTypes.length - 3}
</Badge>
)}
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}