AgentGraph / frontend /src /components /features /traces /TraceKnowledgeGraphView.tsx
wu981526092's picture
add
7bc750c
import React, { useState, useEffect, useCallback } from "react";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Trace, KnowledgeGraph } from "@/types";
import { useAgentGraph } from "@/context/AgentGraphContext";
import { useKGDisplayMode } from "@/context/KGDisplayModeContext";
import { filterKnowledgeGraphsByMode } from "@/lib/kg-selection";
import { AgentGraphsSection } from "@/components/features/traces/AgentGraphsSection";
import { TraceOverviewSection } from "@/components/features/traces/TraceOverviewSection";
import { ContextDocumentsModal } from "@/components/shared/modals/ContextDocumentsModal";
import { TraceContentModal } from "@/components/shared/modals/TraceContentModal";
import { api } from "@/lib/api";
import { useModal } from "@/context/ModalContext";
import { useNotification } from "@/context/NotificationContext";
import { useNavigation } from "@/context/NavigationContext";
import {
SplitterSelectionModal,
SplitterType,
// ChunkingConfig,
} from "@/components/shared/modals/SplitterSelectionModal";
import { useTaskPolling } from "@/hooks/useTaskPolling";
import { MetadataCardConfig } from "@/components/shared/CompactMetadataCard";
import {
MetadataCardSelector,
AVAILABLE_CARDS,
DEFAULT_CARDS,
} from "@/components/shared/modals/MetadataCardSelector";
interface TraceKnowledgeGraphViewProps {
trace: Trace;
knowledgeGraphs: KnowledgeGraph[];
}
export function TraceKnowledgeGraphView({
trace,
knowledgeGraphs: _knowledgeGraphs,
}: TraceKnowledgeGraphViewProps) {
const { actions } = useAgentGraph();
const {
setActiveView, // Used in TraceContentModal onEditTrace
} = actions;
const { mode: kgDisplayMode } = useKGDisplayMode();
const [kgData, setKgData] = useState<KnowledgeGraph[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useModal(); // Keep for potential future use
const { showNotification } = useNotification();
const { actions: navigationActions } = useNavigation();
// Add state for trace content
const [traceContent, setTraceContent] = useState<string>("");
const [traceContentLoading, setTraceContentLoading] = useState(false);
const [traceContentError, setTraceContentError] = useState<string | null>(
null
);
// State for metadata regeneration
const [metadataOutdated, setMetadataOutdated] = useState(false);
const [updatedMetadata, setUpdatedMetadata] = useState<any>(null);
const [updatedTraceData, setUpdatedTraceData] = useState<any>(null);
// State for enhanced statistics
const [enhancedStats, setEnhancedStats] = useState<any>(null);
const [enhancedStatsLoading, setEnhancedStatsLoading] = useState(false);
// Get current metadata (use updated if available, otherwise original)
const currentMetadata = updatedMetadata || trace.metadata;
// Helper to show both toast and persistent notifications
const showSystemNotification = useCallback(
(notification: {
type: "success" | "error" | "warning" | "info";
title: string;
message: string;
}) => {
// Show toast notification
showNotification(notification);
// Add to persistent notifications for all types
navigationActions.addNotification({
type: notification.type,
title: notification.title,
message: notification.message,
});
},
[showNotification, navigationActions]
);
// Function to check if metadata is outdated
const checkMetadataFreshness = useCallback(
(trace: Trace, content: string, metadata: any, updatedTraceData?: any) => {
if (!metadata?.schema_analytics || !content) {
return false; // No metadata to check
}
// Check 1: JSON validity - if current content is invalid JSON, metadata is definitely outdated
try {
JSON.parse(content);
} catch (e) {
return true; // Invalid JSON means metadata is outdated
}
// Check 2: Content length difference (>5% threshold)
const currentLength = content.length;
// Use updated trace data if available, otherwise use original trace
const metadataLength =
updatedTraceData?.character_count || trace.character_count || 0;
if (metadataLength > 0) {
const lengthDifference =
Math.abs(currentLength - metadataLength) / metadataLength;
if (lengthDifference > 0.05) {
return true;
}
}
// Check 3: Character count mismatch (exact)
if (currentLength !== metadataLength) {
return true;
}
return false;
},
[]
);
// Check metadata freshness when trace content is loaded
useEffect(() => {
if (traceContent && trace) {
const outdated = checkMetadataFreshness(
trace,
traceContent,
currentMetadata,
updatedTraceData
);
setMetadataOutdated(outdated);
}
}, [
traceContent,
trace,
currentMetadata,
updatedTraceData,
checkMetadataFreshness,
]);
// Auto-refresh metadata when outdated
useEffect(() => {
const autoRefreshMetadata = async () => {
if (!metadataOutdated || !trace.trace_id) return;
try {
// Call the universal parser endpoint to regenerate metadata
const response = await fetch(
`/api/traces/${trace.trace_id}/regenerate-metadata`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error("Failed to regenerate metadata");
}
// Fetch updated trace data
const traceResponse = await fetch(`/api/traces/${trace.trace_id}`);
if (traceResponse.ok) {
const response = await traceResponse.json();
const updatedTrace = response.trace; // Extract trace from response wrapper
// Store the updated metadata and trace data locally
setUpdatedMetadata(updatedTrace.metadata);
setUpdatedTraceData(updatedTrace);
// Reset metadata outdated flag
setMetadataOutdated(false);
}
} catch (error) {
console.error("Auto-refresh metadata failed:", error);
}
};
// Auto-refresh metadata every 90 seconds if outdated (reduced frequency for HF Spaces)
const interval = setInterval(() => {
autoRefreshMetadata();
}, 90000); // 90 seconds (increased from 30s to avoid 429 errors)
return () => clearInterval(interval);
}, [metadataOutdated, trace.trace_id]);
const [isSplitterModalOpen, setIsSplitterModalOpen] = useState(false);
const [isContextDocumentsModalOpen, setIsContextDocumentsModalOpen] =
useState(false);
const [isTraceContentModalOpen, setIsTraceContentModalOpen] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [generationProgress, setGenerationProgress] = useState(0);
// Load trace content (clean content for overview display)
const fetchTraceContent = useCallback(async () => {
if (!trace.trace_id) return;
setTraceContentLoading(true);
setTraceContentError(null);
try {
// Use clean content for overview display, not numbered content
const content = await api.traces.getContent(trace.trace_id);
setTraceContent(content);
} catch (err) {
setTraceContentError(
err instanceof Error ? err.message : "Failed to load trace content"
);
} finally {
setTraceContentLoading(false);
}
}, [trace.trace_id]);
const fetchKnowledgeGraphs = useCallback(async () => {
if (!trace.trace_id) return;
setLoading(true);
setError(null);
try {
// Also check if trace already has knowledge graphs
console.log("Trace object:", trace);
console.log("Trace knowledge_graphs:", trace.knowledge_graphs);
const response = await fetch(
`/api/traces/${trace.trace_id}/knowledge-graphs`
);
if (!response.ok) {
throw new Error(
`Failed to fetch knowledge graphs: ${response.statusText}`
);
}
const data = await response.json();
console.log("Raw API response:", data);
if (data?.knowledge_graphs?.length) {
console.log(
"Total knowledge graphs received:",
data.knowledge_graphs.length
);
// Log each KG to see their structure
data.knowledge_graphs.forEach((kg: any, index: number) => {
console.log(`KG ${index}:`, {
id: kg.id,
kg_id: kg.kg_id,
is_final: kg.is_final,
window_index: kg.window_index,
window_total: kg.window_total,
filename: kg.filename,
});
});
// Filter for final graphs using the exact logic from stage_processor.js
const finalKGs = data.knowledge_graphs.filter((kg: any) => {
const hasValidId = Boolean(kg.kg_id || kg.id);
const isFinal =
kg.is_final === true ||
(kg.window_index === null && kg.window_total !== null);
if (!hasValidId) {
console.warn(
"Found knowledge graph with missing ID - skipping:",
kg
);
return false;
}
if (!isFinal) {
console.log(
"Skipping non-final KG:",
kg.kg_id,
"window_index:",
kg.window_index,
"is_final:",
kg.is_final
);
} else {
console.log(
"Including final KG:",
kg.kg_id,
"is_final:",
kg.is_final,
"window_index:",
kg.window_index,
"window_total:",
kg.window_total
);
}
return isFinal;
});
console.log("Filtered final KGs:", finalKGs.length, finalKGs);
// Apply display mode filtering
const filteredByMode = filterKnowledgeGraphsByMode(
finalKGs,
kgDisplayMode
);
console.log(
`Applied ${kgDisplayMode} mode filtering:`,
filteredByMode.length,
"KGs"
);
setKgData(filteredByMode);
} else {
console.log(
"No knowledge graphs in API response, checking trace object"
);
// Fallback to trace.knowledge_graphs if API returns empty
if (trace.knowledge_graphs && trace.knowledge_graphs.length > 0) {
console.log("Using trace.knowledge_graphs:", trace.knowledge_graphs);
setKgData(trace.knowledge_graphs);
} else {
setKgData([]);
}
}
} catch (err) {
console.log("API call failed, trying trace.knowledge_graphs fallback");
setError(
err instanceof Error ? err.message : "Failed to fetch knowledge graphs"
);
// Fallback to trace data if API fails
if (trace.knowledge_graphs && trace.knowledge_graphs.length > 0) {
console.log(
"Using trace.knowledge_graphs as fallback:",
trace.knowledge_graphs
);
setKgData(trace.knowledge_graphs);
setError(null); // Clear error since we found data
} else {
setKgData([]);
}
} finally {
setLoading(false);
}
}, [trace, kgDisplayMode]);
// Fetch enhanced statistics
const fetchEnhancedStatistics = useCallback(async () => {
if (!trace.trace_id) return;
setEnhancedStatsLoading(true);
try {
const response = await api.traces.getEnhancedStatistics(trace.trace_id);
setEnhancedStats(response.enhanced_statistics);
} catch (err) {
console.error("Failed to fetch enhanced statistics:", err);
setEnhancedStats(null);
} finally {
setEnhancedStatsLoading(false);
}
}, [trace.trace_id]);
useEffect(() => {
fetchKnowledgeGraphs();
fetchTraceContent();
fetchEnhancedStatistics();
}, [fetchKnowledgeGraphs, fetchTraceContent, fetchEnhancedStatistics]);
// Auto-refresh knowledge graphs every 10 seconds
useEffect(() => {
const interval = setInterval(() => {
if (!loading && !isGenerating) {
fetchKnowledgeGraphs();
}
}, 10000); // 10 seconds
return () => clearInterval(interval);
}, [fetchKnowledgeGraphs, loading, isGenerating]);
const handleGenerateKG = () => {
setIsSplitterModalOpen(true);
};
// Task polling setup for knowledge graph generation
const { pollTaskStatus, isPolling } = useTaskPolling({
onSuccess: (taskId) => {
console.log("Knowledge graph generation completed:", taskId);
showSystemNotification({
type: "success",
title: "Agent Graph Generation Complete",
message: "Your agent graph has been generated successfully!",
});
// Refresh knowledge graphs list
fetchKnowledgeGraphs();
setIsGenerating(false);
setGenerationProgress(0);
},
onError: (error, taskId) => {
console.error("Knowledge graph generation failed:", error, taskId);
// Check if this is a timeout vs a real failure
const isTimeout =
error.includes("timeout") || error.includes("timed out");
showSystemNotification({
type: isTimeout ? "warning" : "error",
title: isTimeout
? "Generation Taking Longer Than Expected"
: "Agent Graph Generation Failed",
message: isTimeout
? `${error} You can refresh the page to check if it completed.`
: error,
});
// Only reset state if it's not a timeout (user might want to keep waiting)
if (!isTimeout) {
setIsGenerating(false);
setGenerationProgress(0);
}
},
onProgress: (progress, message) => {
setGenerationProgress(progress);
if (message) {
console.log("Generation progress:", progress, message);
}
},
maxAttempts: 180, // 15 minutes with exponential backoff
interval: 5000, // Start with 5 seconds
enableExponentialBackoff: true,
});
const handleSplitterConfirm = async (
splitterType: SplitterType,
methodName?: string,
model?: string,
chunkingConfig?: { min_chunk_size?: number; max_chunk_size?: number }
) => {
const finalMethodName = methodName || "production";
const finalModel = model || "gpt-5-mini";
console.log("TraceKnowledgeGraphView: Using method name:", finalMethodName);
console.log("TraceKnowledgeGraphView: Using model:", finalModel);
console.log(
"TraceKnowledgeGraphView: Using chunking config:",
chunkingConfig
);
setIsGenerating(true);
setIsSplitterModalOpen(false); // Safety fallback - modal should already be closed
setGenerationProgress(0);
try {
const response = await api.traces.generateKnowledgeGraph(
trace.trace_id,
splitterType,
true, // force_regenerate = true to allow generating new graphs even if existing ones exist
finalMethodName,
finalModel,
chunkingConfig
);
showSystemNotification({
type: "info",
title: "Agent Graph Generation Started",
message: `Using ${getSplitterDisplayName(
splitterType
)} splitter with ${finalMethodName} method. This may take several minutes.`,
});
// Start polling for task status if we got a task_id
if (response.task_id) {
pollTaskStatus(response.task_id);
} else {
// If no task_id, just refresh immediately and mark as complete
await fetchKnowledgeGraphs();
setIsGenerating(false);
showSystemNotification({
type: "success",
title: "Agent Graph Generation Complete",
message: "Your agent graph has been generated successfully!",
});
}
} catch (error) {
console.error("Failed to generate knowledge graph:", error);
showNotification({
type: "error",
title: "Agent Graph Generation Failed",
message: "Failed to start agent graph generation. Please try again.",
});
setIsGenerating(false);
setGenerationProgress(0);
}
};
// Helper function to get display name for splitter type (matching stage_processor.js)
const getSplitterDisplayName = (splitterType: SplitterType) => {
switch (splitterType) {
case "agent_semantic":
return "Agent Semantic";
case "json":
return "JSON";
case "prompt_interaction":
return "Prompt Interaction";
default:
return splitterType;
}
};
// Metadata cards state management
const [selectedMetadataCards, setSelectedMetadataCards] = useState<string[]>(
() => {
const saved = localStorage.getItem("metadata-cards-selection");
return saved ? JSON.parse(saved) : DEFAULT_CARDS;
}
);
const [isMetadataCardSelectorOpen, setIsMetadataCardSelectorOpen] =
useState(false);
// Save metadata card selection to localStorage
const handleSaveMetadataCards = (cards: string[]) => {
setSelectedMetadataCards(cards);
localStorage.setItem("metadata-cards-selection", JSON.stringify(cards));
};
// Generate metadata card configs
const generateMetadataCards = (): MetadataCardConfig[] => {
const formatDate = (timestamp?: string) => {
if (!timestamp) return "N/A";
return new Date(timestamp).toLocaleDateString();
};
const formatCurrency = (amount: number | string): string => {
const num = typeof amount === "string" ? parseFloat(amount) : amount;
if (isNaN(num) || num === 0) return "$0.00";
if (num < 0.01) return `$${num.toFixed(6)}`;
return `$${num.toFixed(4)}`;
};
const getCardValue = (cardId: string): string | number => {
// Get schema analytics from trace metadata
const schemaAnalytics = currentMetadata?.schema_analytics;
// Use enhanced stats if available and not loading
const stats =
!enhancedStatsLoading && enhancedStats ? enhancedStats : null;
switch (cardId) {
case "size":
return trace.character_count || 0;
case "type":
return trace.trace_type || "Unknown";
case "uploaded":
return formatDate(trace.upload_timestamp);
case "graphs":
return kgData.length;
case "modified":
return formatDate(trace.upload_timestamp);
case "source":
return "Manual Upload";
case "method":
return "Production";
// Schema analytics cards
case "depth":
return (
stats?.components?.max_depth ||
schemaAnalytics?.numerical_overview?.component_stats?.max_depth ||
"N/A"
);
case "execution_time": {
const totalTimeMs =
stats?.performance?.total_execution_time_ms ||
schemaAnalytics?.numerical_overview?.timing_analytics
?.total_execution_time_ms;
if (totalTimeMs && totalTimeMs > 0) {
if (totalTimeMs >= 1000) {
return `${(totalTimeMs / 1000).toFixed(1)}s`;
} else {
return `${totalTimeMs}ms`;
}
}
return "N/A";
}
case "total_tokens":
return (
stats?.tokens?.total_tokens ||
schemaAnalytics?.numerical_overview?.token_analytics
?.total_tokens ||
"N/A"
);
case "prompt_calls":
return (
stats?.prompt_calls?.total_calls ||
schemaAnalytics?.prompt_analytics?.prompt_calls_detected ||
"N/A"
);
case "components":
return (
stats?.components?.total_components ||
schemaAnalytics?.numerical_overview?.component_stats
?.total_components ||
"N/A"
);
case "success_rate": {
const successRate =
stats?.components?.success_rate ||
schemaAnalytics?.numerical_overview?.component_stats?.success_rate;
if (successRate !== undefined && successRate !== null) {
return `${successRate.toFixed(1)}%`;
}
return "N/A";
}
// Cost cards
case "total_cost":
return enhancedStatsLoading
? "Loading..."
: stats?.cost?.total_cost_usd
? formatCurrency(stats.cost.total_cost_usd)
: "N/A";
case "cost_per_call":
return enhancedStatsLoading
? "Loading..."
: stats?.cost?.avg_cost_per_call_usd
? formatCurrency(stats.cost.avg_cost_per_call_usd)
: "N/A";
case "input_cost":
return enhancedStatsLoading
? "Loading..."
: stats?.cost?.input_cost_usd
? formatCurrency(stats.cost.input_cost_usd)
: "N/A";
case "output_cost":
return enhancedStatsLoading
? "Loading..."
: stats?.cost?.output_cost_usd
? formatCurrency(stats.cost.output_cost_usd)
: "N/A";
// Token analytics cards
case "avg_input_tokens":
return enhancedStatsLoading
? "Loading..."
: stats?.prompt_calls?.avg_prompt_tokens_per_call
? Math.round(stats.prompt_calls.avg_prompt_tokens_per_call)
: "N/A";
case "avg_output_tokens":
return enhancedStatsLoading
? "Loading..."
: stats?.prompt_calls?.avg_completion_tokens_per_call
? Math.round(stats.prompt_calls.avg_completion_tokens_per_call)
: "N/A";
default:
return "N/A";
}
};
return selectedMetadataCards
.map((cardId) => {
const cardConfig = AVAILABLE_CARDS.find((card) => card.id === cardId);
if (!cardConfig) return null;
return {
...cardConfig,
value: getCardValue(cardId),
};
})
.filter((card): card is MetadataCardConfig => card !== null);
};
return (
<TooltipProvider delayDuration={300} skipDelayDuration={100}>
<div className="flex-1 flex flex-col p-4 space-y-4 min-h-0">
{/* Main Content */}
<div className="flex-1 space-y-4">
{/* Main Side-by-Side Section - Agent Graphs (Left) and Trace Overview (Right) */}
<div className="flex-1 flex bg-background border border-border/30 rounded-lg min-h-0 gap-4 p-4">
{/* Left Column - Agent Graphs */}
<div className="flex-1 flex flex-col min-h-0">
<AgentGraphsSection
trace={trace}
kgData={kgData}
loading={loading}
error={error}
isGenerating={isGenerating}
isPolling={isPolling}
generationProgress={generationProgress}
onGenerateKG={handleGenerateKG}
onRefreshKnowledgeGraphs={fetchKnowledgeGraphs}
onOpenContextDocuments={() =>
setIsContextDocumentsModalOpen(true)
}
/>
</div>
{/* Right Column - Trace Overview */}
<div className="flex-1 flex flex-col min-h-0">
<TraceOverviewSection
trace={trace}
metadataOutdated={metadataOutdated}
currentMetadata={currentMetadata}
enhancedStats={enhancedStats}
enhancedStatsLoading={enhancedStatsLoading}
onOpenTraceContent={() => setIsTraceContentModalOpen(true)}
metadataCards={generateMetadataCards()}
onOpenMetadataCardSelector={() =>
setIsMetadataCardSelectorOpen(true)
}
/>
</div>
</div>
</div>
{/* Splitter Selection Modal */}
<SplitterSelectionModal
open={isSplitterModalOpen}
onOpenChange={setIsSplitterModalOpen}
onConfirm={handleSplitterConfirm}
isLoading={isGenerating}
/>
{/* Metadata Card Selector Modal */}
<MetadataCardSelector
open={isMetadataCardSelectorOpen}
onOpenChange={setIsMetadataCardSelectorOpen}
selectedCards={selectedMetadataCards}
onSave={handleSaveMetadataCards}
trace={trace}
graphCount={kgData.length}
/>
{/* Context Documents Modal */}
<ContextDocumentsModal
isOpen={isContextDocumentsModalOpen}
onClose={() => setIsContextDocumentsModalOpen(false)}
traceId={trace.trace_id}
/>
{/* Trace Content Modal */}
<TraceContentModal
isOpen={isTraceContentModalOpen}
onClose={() => setIsTraceContentModalOpen(false)}
trace={trace}
traceContent={traceContent}
traceContentLoading={traceContentLoading}
traceContentError={traceContentError}
onEditTrace={() => setActiveView("trace-editor")}
/>
</div>
</TooltipProvider>
);
}