AgentGraph / frontend /src /lib /graph-utils.ts
wu981526092's picture
🚀 Deploy AgentGraph: Complete agent monitoring and knowledge graph system
c2ea5ed
import {
UniversalGraphData,
UniversalNode,
UniversalLink,
GraphVisualizationConfig,
GraphStats,
GraphExportOptions,
} from "@/types/graph-visualization";
// Color schemes for different node types
export const NODE_COLOR_SCHEMES = {
default: [
"#3b82f6",
"#10b981",
"#f59e0b",
"#ef4444",
"#8b5cf6",
"#06b6d4",
"#84cc16",
"#f97316",
"#ec4899",
"#6b7280",
],
knowledge_graph: [
"#ff6b6b",
"#4ecdc4",
"#45b7d1",
"#96ceb4",
"#feca57",
"#ff9ff3",
],
temporal: ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6"],
};
// Link color schemes
export const LINK_COLOR_SCHEMES = {
default: ["#6b7280", "#3b82f6", "#10b981", "#ef4444"],
relationship_types: [
"#999999",
"#ff9800",
"#4caf50",
"#2196f3",
"#9c27b0",
"#e91e63",
],
};
// Get color for a node based on its type
export function getNodeColor(
node: UniversalNode,
colorScheme: string[] = NODE_COLOR_SCHEMES.default
): string {
if (node.color) return node.color;
const typeIndex = node.group || 0;
const color = colorScheme[typeIndex % colorScheme.length];
return color || colorScheme[0] || "#3b82f6";
}
// Get color for a link based on its type
export function getLinkColor(
link: UniversalLink,
colorScheme: string[] = LINK_COLOR_SCHEMES.default
): string {
if (link.color) return link.color;
// Map common relationship types to colors
const typeColorMap: Record<string, string> = {
depends_on: "#ff9800",
creates: "#4caf50",
uses: "#2196f3",
inputs_to: "#9c27b0",
outputs_from: "#e91e63",
communicates_with: "#00bcd4",
contains: "#8bc34a",
relates_to: "#ff5722",
requires_tool: "#ffd600",
assigned_to: "#ff5252",
consumes: "#a259ff",
intervenes: "#00b894",
performs: "#00cec9",
produces: "#fdcb6e",
reviews: "#e17055",
};
if (link.type) {
const linkType = link.type.toLowerCase();
if (typeColorMap[linkType]) {
return typeColorMap[linkType];
}
}
return colorScheme[0] || "#6b7280"; // Default color with fallback
}
// Get node size based on its properties
export function getNodeSize(
node: UniversalNode,
config: GraphVisualizationConfig
): number {
if (node.size) return node.size;
const baseRadius = config.nodeRadius || 10;
const minRadius = config.nodeMinRadius || 5;
const maxRadius = config.nodeMaxRadius || 30;
// Size based on degree (number of connections) if available
if (node.properties?.degree) {
const normalizedDegree = Math.min(node.properties.degree / 10, 1);
return minRadius + (maxRadius - minRadius) * normalizedDegree;
}
return baseRadius;
}
// Get link width based on its properties
export function getLinkWidth(
link: UniversalLink,
config: GraphVisualizationConfig
): number {
const baseWidth = config.linkWidth || 2;
if (link.weight && link.weight > 0) {
return Math.max(1, baseWidth * Math.sqrt(link.weight));
}
if (link.value && link.value > 0) {
return Math.max(1, baseWidth * Math.sqrt(link.value));
}
return baseWidth;
}
// Format tooltip text for nodes
export function formatNodeTooltip(node: UniversalNode): string {
const parts = [];
if (node.name || node.label) {
parts.push(`Name: ${node.name || node.label}`);
}
if (node.type) {
parts.push(`Type: ${node.type}`);
}
if (node.raw_prompt) {
parts.push(
`Prompt: ${node.raw_prompt.substring(0, 100)}${
node.raw_prompt.length > 100 ? "..." : ""
}`
);
}
if (node.properties?.degree) {
parts.push(`Connections: ${node.properties.degree}`);
}
return parts.join("\n") || `Node: ${node.id}`;
}
// Format tooltip text for links
export function formatLinkTooltip(link: UniversalLink): string {
const parts = [];
if (link.type || link.label) {
parts.push(`Type: ${link.type || link.label}`);
}
if (link.relationship) {
parts.push(`Relationship: ${link.relationship}`);
}
if (link.interaction_prompt) {
parts.push(
`Interaction: ${link.interaction_prompt.substring(0, 100)}${
link.interaction_prompt.length > 100 ? "..." : ""
}`
);
}
if (link.weight) {
parts.push(`Weight: ${link.weight}`);
}
const sourceId =
typeof link.source === "string" ? link.source : link.source.id;
const targetId =
typeof link.target === "string" ? link.target : link.target.id;
parts.push(`${sourceId}${targetId}`);
return parts.join("\n");
}
// Validate graph data structure
export function validateGraphData(data: any): UniversalGraphData {
if (!data || typeof data !== "object") {
throw new Error("Invalid graph data: must be an object");
}
if (!Array.isArray(data.nodes)) {
throw new Error("Invalid graph data: nodes must be an array");
}
if (!Array.isArray(data.links)) {
throw new Error("Invalid graph data: links must be an array");
}
// Validate nodes
data.nodes.forEach((node: any, index: number) => {
if (!node.id) {
throw new Error(`Invalid node at index ${index}: missing id`);
}
});
// Validate links
data.links.forEach((link: any, index: number) => {
if (!link.id) {
throw new Error(`Invalid link at index ${index}: missing id`);
}
if (!link.source) {
throw new Error(`Invalid link at index ${index}: missing source`);
}
if (!link.target) {
throw new Error(`Invalid link at index ${index}: missing target`);
}
});
return data as UniversalGraphData;
}
// Calculate graph statistics
export function calculateGraphStatistics(data: UniversalGraphData): GraphStats {
const nodeTypes: Record<string, number> = {};
const linkTypes: Record<string, number> = {};
const nodeDegrees: Record<string, number> = {};
// Initialize node degrees
data.nodes.forEach((node) => {
nodeDegrees[node.id] = 0;
const type = node.type || "Unknown";
nodeTypes[type] = (nodeTypes[type] || 0) + 1;
});
// Count link types and calculate degrees
data.links.forEach((link) => {
const type = link.type || "Unknown";
linkTypes[type] = (linkTypes[type] || 0) + 1;
const sourceId =
typeof link.source === "string" ? link.source : link.source.id;
const targetId =
typeof link.target === "string" ? link.target : link.target.id;
if (nodeDegrees[sourceId] !== undefined) {
nodeDegrees[sourceId]++;
}
if (nodeDegrees[targetId] !== undefined) {
nodeDegrees[targetId]++;
}
});
const degrees = Object.values(nodeDegrees);
const averageDegree =
degrees.length > 0
? degrees.reduce((a, b) => a + b, 0) / degrees.length
: 0;
const maxDegree = degrees.length > 0 ? Math.max(...degrees) : 0;
return {
nodeCount: data.nodes.length,
linkCount: data.links.length,
nodeTypes,
linkTypes,
averageDegree: Math.round(averageDegree * 100) / 100,
maxDegree,
connectedComponents: 1, // Simplified
};
}
// Export graph data to different formats
export function exportGraphData(
data: UniversalGraphData,
options: GraphExportOptions
): string | Blob {
switch (options.format) {
case "json":
return JSON.stringify(data, null, 2);
case "csv":
return exportToCSV(data);
case "svg":
throw new Error("SVG export requires the actual SVG element");
case "png":
throw new Error("PNG export requires canvas conversion");
default:
throw new Error(`Unsupported export format: ${options.format}`);
}
}
// Export to CSV format
function exportToCSV(data: UniversalGraphData): string {
const nodesCsv = [
"id,name,type,raw_prompt",
...data.nodes.map(
(node) =>
`"${node.id}","${node.name || ""}","${node.type || ""}","${
node.raw_prompt || ""
}"`
),
].join("\n");
const linksCsv = [
"id,source,target,type,relationship",
...data.links.map((link) => {
const sourceId =
typeof link.source === "string" ? link.source : link.source.id;
const targetId =
typeof link.target === "string" ? link.target : link.target.id;
return `"${link.id}","${sourceId}","${targetId}","${link.type || ""}","${
link.relationship || ""
}"`;
}),
].join("\n");
return `NODES:\n${nodesCsv}\n\nLINKS:\n${linksCsv}`;
}
// Search and filter utilities
export function searchNodes(
nodes: UniversalNode[],
searchTerm: string
): UniversalNode[] {
if (!searchTerm.trim()) return nodes;
const term = searchTerm.toLowerCase();
return nodes.filter(
(node) =>
node.id.toLowerCase().includes(term) ||
node.name?.toLowerCase().includes(term) ||
node.label?.toLowerCase().includes(term) ||
node.type?.toLowerCase().includes(term) ||
node.raw_prompt?.toLowerCase().includes(term)
);
}
export function filterNodesByType(
nodes: UniversalNode[],
types: string[]
): UniversalNode[] {
if (types.length === 0) return nodes;
return nodes.filter((node) => types.includes(node.type || "Unknown"));
}
export function filterLinksByType(
links: UniversalLink[],
types: string[]
): UniversalLink[] {
if (types.length === 0) return links;
return links.filter((link) => types.includes(link.type || "Unknown"));
}