From ef7b4db93905f620cfee74e4c6256f32d736137b Mon Sep 17 00:00:00 2001 From: 0xbbjoker <0xbbjoker@proton.me> Date: Sun, 13 Jul 2025 15:06:55 +0200 Subject: [PATCH 1/8] feat: optimize knowledge graph --- package.json | 2 +- src/frontend/ui/index.ts | 8 + src/frontend/ui/knowledge-tab.tsx | 32 +- src/frontend/ui/memory-graph-optimized.tsx | 339 +++++++++++++++++++++ src/routes.ts | 282 +++++++++++++++++ 5 files changed, 649 insertions(+), 14 deletions(-) create mode 100644 src/frontend/ui/index.ts create mode 100644 src/frontend/ui/memory-graph-optimized.tsx diff --git a/package.json b/package.json index 649c438..b46d652 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@elizaos/plugin-knowledge", "description": "Plugin for Knowledge", - "version": "1.2.0", + "version": "1.2.1", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/frontend/ui/index.ts b/src/frontend/ui/index.ts new file mode 100644 index 0000000..4cce0cd --- /dev/null +++ b/src/frontend/ui/index.ts @@ -0,0 +1,8 @@ +// Export all UI components +export { MemoryGraph } from './memory-graph'; +export { MemoryGraphOptimized } from './memory-graph-optimized'; +export { KnowledgeTab } from './knowledge-tab'; +export { Badge } from './badge'; +export { Button } from './button'; +export { Card } from './card'; +export { Input } from './input'; diff --git a/src/frontend/ui/knowledge-tab.tsx b/src/frontend/ui/knowledge-tab.tsx index 5198f69..87f7665 100644 --- a/src/frontend/ui/knowledge-tab.tsx +++ b/src/frontend/ui/knowledge-tab.tsx @@ -25,6 +25,7 @@ import { Button } from './button'; import { Card } from './card'; import { Input } from './input'; import { MemoryGraph } from './memory-graph'; +import { MemoryGraphOptimized } from './memory-graph-optimized'; // Local utility function instead of importing from client const cn = (...classes: (string | undefined | null | false)[]) => { @@ -1046,8 +1047,8 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { const isFragment = metadata?.type === 'fragment'; return ( -
-
+
+

{isFragment ? ( @@ -1098,9 +1099,15 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) {

-
-
-
+        
+
+
               {memory.content?.text || 'No content available'}
             
@@ -1124,9 +1131,8 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { return (

Knowledge

@@ -1410,8 +1416,7 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) {
- { setSelectedMemory(memory); @@ -1422,6 +1427,7 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { } }} selectedMemoryId={selectedMemory?.id} + agentId={agentId} /> {viewMode === 'graph' && graphLoading && selectedDocumentForGraph && (
@@ -1435,7 +1441,7 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { {/* Display details of selected node */} {selectedMemory && ( -
+
)} @@ -1610,8 +1616,8 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { } else { // For all other documents, display as plain text return ( -
-
+                    
+
                         {viewingContent.content?.text || 'No content available'}
                       
diff --git a/src/frontend/ui/memory-graph-optimized.tsx b/src/frontend/ui/memory-graph-optimized.tsx new file mode 100644 index 0000000..e745027 --- /dev/null +++ b/src/frontend/ui/memory-graph-optimized.tsx @@ -0,0 +1,339 @@ +import React, { useEffect, useRef, useState, useCallback } from 'react'; +import type { Memory, UUID } from '@elizaos/core'; +// @ts-ignore +import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from 'react-force-graph-2d'; + +interface GraphNode extends NodeObject { + id: UUID; + type: 'document' | 'fragment'; + label?: string; + loading?: boolean; + val?: number; +} + +interface GraphLink extends LinkObject { + source: UUID; + target: UUID; +} + +interface MemoryGraphOptimizedProps { + onNodeClick: (memory: Memory) => void; + selectedMemoryId?: UUID; + agentId: UUID; +} + +interface GraphData { + nodes: GraphNode[]; + links: GraphLink[]; +} + +interface PaginationInfo { + currentPage: number; + totalPages: number; + hasMore: boolean; + totalDocuments: number; +} + +export function MemoryGraphOptimized({ + onNodeClick, + selectedMemoryId, + agentId +}: MemoryGraphOptimizedProps) { + const graphRef = useRef(null); + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 800, height: 600 }); + const [graphData, setGraphData] = useState({ nodes: [], links: [] }); + const [pagination, setPagination] = useState(null); + const [loadingNodes, setLoadingNodes] = useState>(new Set()); + const [nodeDetails, setNodeDetails] = useState>(new Map()); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [graphVersion, setGraphVersion] = useState(0); + + // Update dimensions on resize + useEffect(() => { + const updateDimensions = () => { + if (containerRef.current) { + const { offsetWidth, offsetHeight } = containerRef.current; + setDimensions({ + width: offsetWidth, + height: offsetHeight, + }); + } + }; + + updateDimensions(); + window.addEventListener('resize', updateDimensions); + return () => window.removeEventListener('resize', updateDimensions); + }, []); + + // Fetch initial graph nodes (documents with fragments) + const loadGraphNodes = useCallback(async (page = 1) => { + setIsLoading(true); + setError(null); + + try { + const params = new URLSearchParams(); + params.append('agentId', agentId); + params.append('page', page.toString()); + params.append('limit', '20'); + // Don't specify type to get documents with fragments + + const response = await fetch( + `/api/graph/nodes?${params.toString()}` + ); + + if (!response.ok) { + throw new Error(`Failed to load graph nodes: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.success && result.data) { + const { nodes, links, pagination } = result.data; + + // Convert to graph nodes with initial properties + const graphNodes: GraphNode[] = nodes.map((node: any) => ({ + id: node.id, + type: node.type, + loading: false, + val: node.type === 'document' ? 8 : 4, + })); + + if (page === 1) { + setGraphData({ nodes: graphNodes, links }); + setGraphVersion(1); // Reset version for initial load + } else { + // Append to existing nodes + setGraphData(prev => ({ + nodes: [...prev.nodes, ...graphNodes], + links: [...prev.links, ...links] + })); + setGraphVersion(prev => prev + 1); // Increment for additions + } + + setPagination(pagination); + } + } catch (err) { + console.error('Error loading graph nodes:', err); + setError(err instanceof Error ? err.message : 'Failed to load graph'); + } finally { + setIsLoading(false); + } + }, [agentId]); + + // Load more documents + const loadMore = useCallback(() => { + if (pagination && pagination.hasMore) { + loadGraphNodes(pagination.currentPage + 1); + } + }, [pagination, loadGraphNodes]); + + // Fetch full node details when clicked + const fetchNodeDetails = useCallback(async (nodeId: UUID) => { + // Check cache first + if (nodeDetails.has(nodeId)) { + const memory = nodeDetails.get(nodeId)!; + onNodeClick(memory); + return; + } + + // Mark as loading + setLoadingNodes(prev => new Set(prev).add(nodeId)); + + try { + const params = new URLSearchParams(); + params.append('agentId', agentId); + + const response = await fetch( + `/api/graph/node/${nodeId}?${params.toString()}` + ); + + if (!response.ok) { + throw new Error(`Failed to fetch node details: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.success && result.data) { + // Convert to Memory format + const memory: Memory = { + id: result.data.id, + content: result.data.content, + metadata: result.data.metadata, + createdAt: result.data.createdAt, + entityId: agentId, // Use agentId as entityId + roomId: agentId, // Use agentId as roomId + }; + + // Cache the details + setNodeDetails(prev => new Map(prev).set(nodeId, memory)); + + // Trigger the callback + onNodeClick(memory); + } + } catch (err) { + console.error('Error fetching node details:', err); + } finally { + setLoadingNodes(prev => { + const newSet = new Set(prev); + newSet.delete(nodeId); + return newSet; + }); + } + }, [agentId, nodeDetails, onNodeClick]); + + // Handle node click + const handleNodeClick = useCallback((node: GraphNode) => { + console.log('Node clicked:', node); + + // Just fetch details to show in sidebar + fetchNodeDetails(node.id); + }, [fetchNodeDetails]); + + // Initialize graph + useEffect(() => { + loadGraphNodes(1); + }, [loadGraphNodes]); + + // Debug effect to monitor graph data changes + useEffect(() => { + console.log('Graph data changed:', { + nodeCount: graphData.nodes.length, + linkCount: graphData.links.length, + nodes: graphData.nodes, + links: graphData.links + }); + }, [graphData]); + + // Node color based on state + const getNodeColor = useCallback((node: GraphNode) => { + const isSelected = selectedMemoryId === node.id; + const isLoading = loadingNodes.has(node.id); + + if (isLoading) { + return 'hsl(210, 70%, 80%)'; // Light blue for loading + } + + if (node.type === 'document') { + if (isSelected) return 'hsl(30, 100%, 60%)'; + return 'hsl(30, 100%, 50%)'; // Orange + } else { + if (isSelected) return 'hsl(200, 70%, 70%)'; + return 'hsl(200, 70%, 60%)'; // Light blue (matches blue-300) + } + }, [selectedMemoryId, loadingNodes]); + + // Render loading state + if (isLoading && graphData.nodes.length === 0) { + return ( +
+
Loading graph...
+
+ ); + } + + // Render error state + if (error) { + return ( +
+
Error: {error}
+
+ ); + } + + return ( +
+ {/* Legend */} +
+
Legend
+
+
+
+ Document +
+
+
+ Fragment +
+
+
+ + {/* Pagination */} + {pagination && pagination.hasMore && ( +
+ +
+ )} + + {/* Graph */} + 'hsla(var(--muted-foreground), 0.2)'} + linkWidth={1} + linkDirectionalParticles={2} + linkDirectionalParticleSpeed={0.005} + nodeRelSize={1} + nodeVal={(node: GraphNode) => node.val || 4} + nodeColor={getNodeColor} + nodeLabel={(node: GraphNode) => { + if (loadingNodes.has(node.id)) return 'Loading...'; + const typeLabel = node.type === 'document' ? 'Document' : 'Fragment'; + return `${typeLabel}: ${node.id.substring(0, 8)}`; + }} + onNodeClick={handleNodeClick} + enableNodeDrag={true} + enableZoomInteraction={true} + enablePanInteraction={true} + d3AlphaDecay={0.02} + d3VelocityDecay={0.3} + warmupTicks={100} + cooldownTicks={0} + nodeCanvasObject={(node: GraphNode, ctx, globalScale) => { + const size = (node.val || 4); + const isSelected = selectedMemoryId === node.id; + const isLoading = loadingNodes.has(node.id); + + // Draw node circle + ctx.beginPath(); + ctx.arc(node.x!, node.y!, size, 0, 2 * Math.PI); + ctx.fillStyle = getNodeColor(node); + ctx.fill(); + + // Border + ctx.strokeStyle = isSelected ? 'hsl(var(--primary))' : 'hsl(var(--border))'; + ctx.lineWidth = isSelected ? 2 : 1; + ctx.stroke(); + + // Loading indicator + if (isLoading) { + ctx.beginPath(); + ctx.arc(node.x!, node.y!, size * 1.5, 0, Math.PI * 2 * 0.3); + ctx.strokeStyle = 'hsl(var(--primary))'; + ctx.lineWidth = 2; + ctx.stroke(); + } + + // Don't draw labels - they will be shown on hover via nodeLabel + }} + onEngineStop={() => { + // Center the graph when physics settle + if (graphRef.current) { + graphRef.current.zoomToFit(400); + } + }} + /> +
+ ); +} \ No newline at end of file diff --git a/src/routes.ts b/src/routes.ts index 4daa26c..a2027bd 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -843,6 +843,272 @@ async function searchKnowledgeHandler(req: any, res: any, runtime: IAgentRuntime } } +// New Graph-specific handlers for ultra-lightweight rendering + +async function getGraphNodesHandler(req: any, res: any, runtime: IAgentRuntime) { + const service = runtime.getService(KnowledgeService.serviceType); + if (!service) { + return sendError(res, 500, 'SERVICE_NOT_FOUND', 'KnowledgeService not found'); + } + + try { + // Parse pagination parameters + const parsedPage = req.query.page ? Number.parseInt(req.query.page as string, 10) : 1; + const parsedLimit = req.query.limit ? Number.parseInt(req.query.limit as string, 10) : 20; + const type = req.query.type as 'document' | 'fragment' | undefined; + const agentId = (req.query.agentId as UUID) || runtime.agentId; + + const page = Number.isNaN(parsedPage) || parsedPage < 1 ? 1 : parsedPage; + const limit = Number.isNaN(parsedLimit) || parsedLimit < 1 ? 20 : Math.min(parsedLimit, 50); + const offset = (page - 1) * limit; + + logger.debug( + `[Graph API] 📊 Fetching graph nodes: page=${page}, limit=${limit}, type=${type || 'all'}, agent=${agentId}` + ); + + // Get documents filtered by agent + const documents = await service.getMemories({ + tableName: 'documents', + roomId: agentId, // Filter by agent + count: 10000, // Get all to calculate total count + }); + + // Calculate pagination + const totalDocuments = documents.length; + const totalPages = Math.ceil(totalDocuments / limit); + const hasMore = page < totalPages; + + // Paginate documents + const paginatedDocuments = documents.slice(offset, offset + limit); + + // Build lightweight nodes + const nodes: Array<{ id: UUID; type: 'document' | 'fragment' }> = []; + const links: Array<{ source: UUID; target: UUID }> = []; + + // Add document nodes + paginatedDocuments.forEach((doc) => { + nodes.push({ id: doc.id!, type: 'document' }); + }); + + // Always include fragments for the paginated documents (unless specifically requesting documents only) + if (type !== 'document') { + // Get all fragments for the paginated documents + const allFragments = await service.getMemories({ + tableName: 'knowledge', + roomId: agentId, + count: 100000, + }); + + logger.debug(`[Graph API] 📊 Total fragments found: ${allFragments.length}`); + + // Debug: Log the first few fragment metadata to understand structure + if (allFragments.length > 0) { + logger.debug( + `[Graph API] 📊 Sample fragment metadata:`, + allFragments.slice(0, 3).map((f) => ({ + id: f.id, + metadata: f.metadata, + })) + ); + } + + // For each document, find its fragments and add them + paginatedDocuments.forEach((doc) => { + const docFragments = allFragments.filter((fragment) => { + const metadata = fragment.metadata as any; + // Check both documentId match and that it's a fragment type (case-insensitive) + const isFragment = + metadata?.type?.toLowerCase() === 'fragment' || + metadata?.type === MemoryType.FRAGMENT || + // If no type but has documentId, assume it's a fragment + (!metadata?.type && metadata?.documentId); + return metadata?.documentId === doc.id && isFragment; + }); + + if (docFragments.length > 0) { + logger.debug(`[Graph API] 📊 Document ${doc.id} has ${docFragments.length} fragments`); + } + + // Add fragment nodes and links + docFragments.forEach((frag) => { + nodes.push({ id: frag.id!, type: 'fragment' }); + links.push({ source: doc.id!, target: frag.id! }); + }); + }); + + logger.info( + `[Graph API] 📊 Final graph: ${nodes.length} nodes (${paginatedDocuments.length} documents), ${links.length} links` + ); + } + + // Return the graph data with nodes and links + sendSuccess(res, { + nodes, + links, + pagination: { + currentPage: page, + totalPages, + hasMore, + totalDocuments, + }, + }); + } catch (error: any) { + logger.error('[Graph API] ❌ Error fetching graph nodes:', error); + sendError(res, 500, 'GRAPH_ERROR', 'Failed to fetch graph nodes', error.message); + } +} + +async function getGraphNodeDetailsHandler(req: any, res: any, runtime: IAgentRuntime) { + const service = runtime.getService(KnowledgeService.serviceType); + if (!service) { + return sendError(res, 500, 'SERVICE_NOT_FOUND', 'KnowledgeService not found'); + } + + const nodeId = req.params.nodeId as UUID; + const agentId = (req.query.agentId as UUID) || runtime.agentId; + + if (!nodeId || nodeId.length < 36) { + return sendError(res, 400, 'INVALID_ID', 'Invalid node ID format'); + } + + try { + logger.debug(`[Graph API] 📊 Fetching node details for: ${nodeId}, agent: ${agentId}`); + + // Try to find in documents first + const documents = await service.getMemories({ + tableName: 'documents', + roomId: agentId, // Filter by agent + count: 10000, + }); + + const document = documents.find((doc) => doc.id === nodeId); + + if (document) { + // Return document details without embedding + sendSuccess(res, { + id: document.id, + type: 'document', + content: document.content, + metadata: document.metadata, + createdAt: document.createdAt, + }); + return; + } + + // If not found in documents, try fragments + const fragments = await service.getMemories({ + tableName: 'knowledge', + roomId: agentId, // Filter by agent + count: 100000, // High limit + }); + + const fragment = fragments.find((frag) => frag.id === nodeId); + + if (fragment) { + sendSuccess(res, { + id: fragment.id, + type: 'fragment', + content: fragment.content, + metadata: fragment.metadata, + createdAt: fragment.createdAt, + }); + return; + } + + sendError(res, 404, 'NOT_FOUND', `Node with ID ${nodeId} not found`); + } catch (error: any) { + logger.error(`[Graph API] ❌ Error fetching node details for ${nodeId}:`, error); + sendError(res, 500, 'GRAPH_ERROR', 'Failed to fetch node details', error.message); + } +} + +async function expandDocumentGraphHandler(req: any, res: any, runtime: IAgentRuntime) { + const service = runtime.getService(KnowledgeService.serviceType); + if (!service) { + return sendError(res, 500, 'SERVICE_NOT_FOUND', 'KnowledgeService not found'); + } + + const documentId = req.params.documentId as UUID; + const agentId = (req.query.agentId as UUID) || runtime.agentId; + + if (!documentId || documentId.length < 36) { + return sendError(res, 400, 'INVALID_ID', 'Invalid document ID format'); + } + + try { + logger.debug(`[Graph API] 📊 Expanding document: ${documentId}, agent: ${agentId}`); + + // Get all fragments for this document + const allFragments = await service.getMemories({ + tableName: 'knowledge', + roomId: agentId, // Filter by agent + count: 100000, // High limit + }); + + logger.debug(`[Graph API] 📊 Total fragments in knowledge table: ${allFragments.length}`); + + // Log a sample fragment to see its structure + if (allFragments.length > 0) { + logger.debug(`[Graph API] 📊 Sample fragment metadata:`, allFragments[0].metadata); + + // Log all unique metadata types found + const uniqueTypes = new Set(allFragments.map((f) => (f.metadata as any)?.type)); + logger.debug( + `[Graph API] 📊 Unique metadata types found in knowledge table:`, + Array.from(uniqueTypes) + ); + + // Log metadata of all fragments for this specific document + const relevantFragments = allFragments.filter((fragment) => { + const metadata = fragment.metadata as any; + const hasDocumentId = metadata?.documentId === documentId; + if (hasDocumentId) { + logger.debug(`[Graph API] 📊 Fragment ${fragment.id} metadata:`, metadata); + } + return hasDocumentId; + }); + + logger.debug( + `[Graph API] 📊 Fragments with matching documentId: ${relevantFragments.length}` + ); + } + + const documentFragments = allFragments.filter((fragment) => { + const metadata = fragment.metadata as any; + // Check both documentId match and that it's a fragment type (case-insensitive) + const isFragment = + metadata?.type?.toLowerCase() === 'fragment' || + metadata?.type === MemoryType.FRAGMENT || + // If no type but has documentId, assume it's a fragment + (!metadata?.type && metadata?.documentId); + return metadata?.documentId === documentId && isFragment; + }); + + // Build fragment nodes and links + const nodes = documentFragments.map((frag) => ({ + id: frag.id!, + type: 'fragment' as const, + })); + + const links = documentFragments.map((frag) => ({ + source: documentId, + target: frag.id!, + })); + + logger.info(`[Graph API] 📊 Found ${nodes.length} fragments for document ${documentId}`); + + sendSuccess(res, { + documentId, + nodes, + links, + fragmentCount: nodes.length, + }); + } catch (error: any) { + logger.error(`[Graph API] ❌ Error expanding document ${documentId}:`, error); + sendError(res, 500, 'GRAPH_ERROR', 'Failed to expand document', error.message); + } +} + // Wrapper handler that applies multer middleware before calling the upload handler async function uploadKnowledgeWithMulter(req: any, res: any, runtime: IAgentRuntime) { const upload = createUploadMiddleware(runtime); @@ -905,4 +1171,20 @@ export const knowledgeRoutes: Route[] = [ path: '/search', handler: searchKnowledgeHandler, }, + // New graph routes + { + type: 'GET', + path: '/graph/nodes', + handler: getGraphNodesHandler, + }, + { + type: 'GET', + path: '/graph/node/:nodeId', + handler: getGraphNodeDetailsHandler, + }, + { + type: 'GET', + path: '/graph/expand/:documentId', + handler: expandDocumentGraphHandler, + }, ]; From decc1535b197f8ae853cec97856cce01fa46550f Mon Sep 17 00:00:00 2001 From: 0xbbjoker <0xbbjoker@proton.me> Date: Sun, 5 Oct 2025 17:34:41 +0900 Subject: [PATCH 2/8] fix: api base --- src/frontend/ui/memory-graph-optimized.tsx | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/frontend/ui/memory-graph-optimized.tsx b/src/frontend/ui/memory-graph-optimized.tsx index e745027..5faac46 100644 --- a/src/frontend/ui/memory-graph-optimized.tsx +++ b/src/frontend/ui/memory-graph-optimized.tsx @@ -3,6 +3,24 @@ import type { Memory, UUID } from '@elizaos/core'; // @ts-ignore import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from 'react-force-graph-2d'; +// Declare global window extension for TypeScript +declare global { + interface Window { + ELIZA_CONFIG?: { + agentId: string; + apiBase: string; + }; + } +} + +// Get the API base path from the injected configuration +const getApiBase = () => { + if (window.ELIZA_CONFIG?.apiBase) { + return window.ELIZA_CONFIG.apiBase; + } + return '/api'; +}; + interface GraphNode extends NodeObject { id: UUID; type: 'document' | 'fragment'; @@ -79,8 +97,9 @@ export function MemoryGraphOptimized({ params.append('limit', '20'); // Don't specify type to get documents with fragments + const apiBase = getApiBase(); const response = await fetch( - `/api/graph/nodes?${params.toString()}` + `${apiBase}/graph/nodes?${params.toString()}` ); if (!response.ok) { @@ -145,8 +164,9 @@ export function MemoryGraphOptimized({ const params = new URLSearchParams(); params.append('agentId', agentId); + const apiBase = getApiBase(); const response = await fetch( - `/api/graph/node/${nodeId}?${params.toString()}` + `${apiBase}/graph/node/${nodeId}?${params.toString()}` ); if (!response.ok) { From c529d7b082eb796cac8fac77df7661b4825d4a3d Mon Sep 17 00:00:00 2001 From: 0xbbjoker <0xbbjoker@proton.me> Date: Sun, 5 Oct 2025 18:30:10 +0900 Subject: [PATCH 3/8] fix(plugin-knowledge): fix graph node click and document display in graph view --- package.json | 2 +- src/frontend/ui/knowledge-tab.tsx | 105 +++++++++++----- src/frontend/ui/memory-graph-optimized.tsx | 33 +++-- src/routes.ts | 135 +++++++++++++++------ 4 files changed, 189 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index 105e064..8eabd4c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@elizaos/plugin-knowledge", "description": "Plugin for Knowledge", - "version": "1.5.10", + "version": "1.5.11", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/frontend/ui/knowledge-tab.tsx b/src/frontend/ui/knowledge-tab.tsx index 984d085..e17ce0a 100644 --- a/src/frontend/ui/knowledge-tab.tsx +++ b/src/frontend/ui/knowledge-tab.tsx @@ -567,6 +567,25 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { } }, [handleScroll]); + // Track changes to selectedMemory and scroll to it + const detailsPanelRef = useRef(null); + + useEffect(() => { + if (selectedMemory && detailsPanelRef.current) { + // Scroll the details panel into view smoothly + detailsPanelRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + } + }, [selectedMemory]); + + // Memoized callback for graph node clicks to prevent unnecessary re-renders + // MUST be defined here (before early returns) to follow Rules of Hooks + const handleGraphNodeClick = useCallback((memory: Memory) => { + setSelectedMemory(memory); + }, []); + if (isLoading && (!memories || memories.length === 0) && !showSearch) { return (
Loading knowledge documents...
@@ -1071,11 +1090,12 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { const MemoryDetails = ({ memory }: { memory: Memory }) => { const metadata = memory.metadata as MemoryMetadata; const isFragment = metadata?.type === 'fragment'; + const isDocument = metadata?.type === 'document'; return (
-
+

{isFragment ? ( @@ -1115,28 +1135,58 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) {

- +
+ {isDocument && ( + + )} + +
-
-
-              {memory.content?.text || 'No content available'}
-            
-
+ {isDocument ? ( +
+ +

+ {metadata?.title || metadata?.filename || 'Document'} +

+

+ Click "Open Document" to view the full content +

+ {metadata?.fileExt && ( + + {metadata.fileExt.toUpperCase()} + + )} +
+ ) : ( +
+
+                {memory.content?.text || 'No content available'}
+              
+
+ )} {memory.embedding && (
@@ -1443,15 +1493,7 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { className={`p-4 overflow-hidden ${selectedMemory ? 'h-1/3' : 'flex-1'} transition-all duration-300`} > { - setSelectedMemory(memory); - - // If clicking on a document, load its fragments - const metadata = memory.metadata as any; - if (metadata?.type === 'document') { - setSelectedDocumentForGraph(memory.id as UUID); - } - }} + onNodeClick={handleGraphNodeClick} selectedMemoryId={selectedMemory?.id} agentId={agentId} /> @@ -1467,7 +1509,10 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) { {/* Display details of selected node */} {selectedMemory && ( -
+
)} diff --git a/src/frontend/ui/memory-graph-optimized.tsx b/src/frontend/ui/memory-graph-optimized.tsx index 5faac46..5e4a1f9 100644 --- a/src/frontend/ui/memory-graph-optimized.tsx +++ b/src/frontend/ui/memory-graph-optimized.tsx @@ -165,25 +165,28 @@ export function MemoryGraphOptimized({ params.append('agentId', agentId); const apiBase = getApiBase(); - const response = await fetch( - `${apiBase}/graph/node/${nodeId}?${params.toString()}` - ); + const url = `${apiBase}/graph/node/${nodeId}?${params.toString()}`; + + const response = await fetch(url); if (!response.ok) { + const errorText = await response.text(); + console.error('API error response:', errorText); throw new Error(`Failed to fetch node details: ${response.statusText}`); } const result = await response.json(); if (result.success && result.data) { - // Convert to Memory format + // Convert to Memory format with all required fields const memory: Memory = { id: result.data.id, content: result.data.content, metadata: result.data.metadata, createdAt: result.data.createdAt, - entityId: agentId, // Use agentId as entityId - roomId: agentId, // Use agentId as roomId + entityId: agentId, + roomId: agentId, + agentId: agentId, }; // Cache the details @@ -191,9 +194,13 @@ export function MemoryGraphOptimized({ // Trigger the callback onNodeClick(memory); + } else { + console.error('Invalid API response format:', result); + throw new Error('Invalid response format from API'); } } catch (err) { console.error('Error fetching node details:', err); + alert(`Failed to load node details: ${err instanceof Error ? err.message : 'Unknown error'}`); } finally { setLoadingNodes(prev => { const newSet = new Set(prev); @@ -205,9 +212,7 @@ export function MemoryGraphOptimized({ // Handle node click const handleNodeClick = useCallback((node: GraphNode) => { - console.log('Node clicked:', node); - - // Just fetch details to show in sidebar + // Fetch details to show in sidebar fetchNodeDetails(node.id); }, [fetchNodeDetails]); @@ -216,16 +221,6 @@ export function MemoryGraphOptimized({ loadGraphNodes(1); }, [loadGraphNodes]); - // Debug effect to monitor graph data changes - useEffect(() => { - console.log('Graph data changed:', { - nodeCount: graphData.nodes.length, - linkCount: graphData.links.length, - nodes: graphData.nodes, - links: graphData.links - }); - }, [graphData]); - // Node color based on state const getNodeColor = useCallback((node: GraphNode) => { const isSelected = selectedMemoryId === node.id; diff --git a/src/routes.ts b/src/routes.ts index 59134a2..81de73d 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -906,9 +906,13 @@ async function getGraphNodesHandler(req: any, res: any, runtime: IAgentRuntime) const nodes: Array<{ id: UUID; type: 'document' | 'fragment' }> = []; const links: Array<{ source: UUID; target: UUID }> = []; - // Add document nodes + // Add document nodes (filter out any without IDs) paginatedDocuments.forEach((doc) => { - nodes.push({ id: doc.id!, type: 'document' }); + if (!doc.id) { + logger.warn(`[Graph API] ⚠️ Skipping document without ID`); + return; + } + nodes.push({ id: doc.id, type: 'document' }); }); // Always include fragments for the paginated documents (unless specifically requesting documents only) @@ -925,21 +929,29 @@ async function getGraphNodesHandler(req: any, res: any, runtime: IAgentRuntime) // Debug: Log the first few fragment metadata to understand structure if (allFragments.length > 0) { logger.debug( - `[Graph API] 📊 Sample fragment metadata:`, - allFragments.slice(0, 3).map((f) => ({ - id: f.id, - metadata: f.metadata, - })) + `[Graph API] 📊 Sample fragment metadata: ${JSON.stringify( + allFragments.slice(0, 3).map((f) => ({ + id: f.id, + metadata: f.metadata, + })) + )}` ); } // For each document, find its fragments and add them paginatedDocuments.forEach((doc) => { + // Skip documents without IDs + if (!doc.id) { + return; + } + const docFragments = allFragments.filter((fragment) => { const metadata = fragment.metadata as any; // Check both documentId match and that it's a fragment type (case-insensitive) + // Safely handle type checking with string validation + const typeString = typeof metadata?.type === 'string' ? metadata.type : null; const isFragment = - metadata?.type?.toLowerCase() === 'fragment' || + (typeString && typeString.toLowerCase() === 'fragment') || metadata?.type === MemoryType.FRAGMENT || // If no type but has documentId, assume it's a fragment (!metadata?.type && metadata?.documentId); @@ -950,10 +962,18 @@ async function getGraphNodesHandler(req: any, res: any, runtime: IAgentRuntime) logger.debug(`[Graph API] 📊 Document ${doc.id} has ${docFragments.length} fragments`); } - // Add fragment nodes and links + // Add fragment nodes and links (filter out any without IDs) docFragments.forEach((frag) => { - nodes.push({ id: frag.id!, type: 'fragment' }); - links.push({ source: doc.id!, target: frag.id! }); + // TypeScript guard: doc.id is already checked above, but reassign for type safety + const docId = doc.id; + if (!frag.id || !docId) { + logger.warn( + `[Graph API] ⚠️ Skipping fragment without ID for document ${docId || 'unknown'}` + ); + return; + } + nodes.push({ id: frag.id, type: 'fragment' }); + links.push({ source: docId, target: frag.id }); }); }); @@ -993,18 +1013,32 @@ async function getGraphNodeDetailsHandler(req: any, res: any, runtime: IAgentRun } try { - logger.debug(`[Graph API] 📊 Fetching node details for: ${nodeId}, agent: ${agentId}`); + logger.info(`[Graph API] 📊 Fetching node details for: ${nodeId}, agent: ${agentId}`); - // Try to find in documents first - const documents = await service.getMemories({ + // Try to find in documents first - don't filter by roomId initially to see if node exists at all + const allDocuments = await service.getMemories({ tableName: 'documents', - roomId: agentId, // Filter by agent count: 10000, }); - const document = documents.find((doc) => doc.id === nodeId); + logger.debug(`[Graph API] 📊 Total documents in DB: ${allDocuments.length}`); + + // First try exact match with roomId filter + let document = allDocuments.find((doc) => doc.id === nodeId && doc.roomId === agentId); + + // If not found with roomId filter, try without filter (for backward compatibility) + if (!document) { + logger.debug(`[Graph API] 📊 Document not found with roomId filter, trying without filter`); + document = allDocuments.find((doc) => doc.id === nodeId); + if (document) { + logger.warn( + `[Graph API] ⚠️ Document ${nodeId} found but has different roomId: ${document.roomId} vs ${agentId}` + ); + } + } if (document) { + logger.info(`[Graph API] ✅ Found document: ${nodeId}`); // Return document details without embedding sendSuccess(res, { id: document.id, @@ -1012,30 +1046,50 @@ async function getGraphNodeDetailsHandler(req: any, res: any, runtime: IAgentRun content: document.content, metadata: document.metadata, createdAt: document.createdAt, + roomId: document.roomId, + agentId: document.agentId, }); return; } // If not found in documents, try fragments - const fragments = await service.getMemories({ + logger.debug(`[Graph API] 📊 Document not found, searching in fragments`); + const allFragments = await service.getMemories({ tableName: 'knowledge', - roomId: agentId, // Filter by agent count: 100000, // High limit }); - const fragment = fragments.find((frag) => frag.id === nodeId); + logger.debug(`[Graph API] 📊 Total fragments in DB: ${allFragments.length}`); + + // First try exact match with roomId filter + let fragment = allFragments.find((frag) => frag.id === nodeId && frag.roomId === agentId); + + // If not found with roomId filter, try without filter + if (!fragment) { + logger.debug(`[Graph API] 📊 Fragment not found with roomId filter, trying without filter`); + fragment = allFragments.find((frag) => frag.id === nodeId); + if (fragment) { + logger.warn( + `[Graph API] ⚠️ Fragment ${nodeId} found but has different roomId: ${fragment.roomId} vs ${agentId}` + ); + } + } if (fragment) { + logger.info(`[Graph API] ✅ Found fragment: ${nodeId}`); sendSuccess(res, { id: fragment.id, type: 'fragment', content: fragment.content, metadata: fragment.metadata, createdAt: fragment.createdAt, + roomId: fragment.roomId, + agentId: fragment.agentId, }); return; } + logger.error(`[Graph API] ❌ Node ${nodeId} not found in documents or fragments`); sendError(res, 404, 'NOT_FOUND', `Node with ID ${nodeId} not found`); } catch (error: any) { logger.error(`[Graph API] ❌ Error fetching node details for ${nodeId}:`, error); @@ -1068,15 +1122,16 @@ async function expandDocumentGraphHandler(req: any, res: any, runtime: IAgentRun logger.debug(`[Graph API] 📊 Total fragments in knowledge table: ${allFragments.length}`); - // Log a sample fragment to see its structure - if (allFragments.length > 0) { - logger.debug(`[Graph API] 📊 Sample fragment metadata:`, allFragments[0].metadata); + // Log a sample fragment to see its structure (only in development/debug mode) + if (allFragments.length > 0 && process.env.NODE_ENV !== 'production') { + logger.debug( + `[Graph API] 📊 Sample fragment metadata: ${JSON.stringify(allFragments[0].metadata)}` + ); // Log all unique metadata types found const uniqueTypes = new Set(allFragments.map((f) => (f.metadata as any)?.type)); logger.debug( - `[Graph API] 📊 Unique metadata types found in knowledge table:`, - Array.from(uniqueTypes) + `[Graph API] 📊 Unique metadata types found in knowledge table: ${JSON.stringify(Array.from(uniqueTypes))}` ); // Log metadata of all fragments for this specific document @@ -1084,7 +1139,9 @@ async function expandDocumentGraphHandler(req: any, res: any, runtime: IAgentRun const metadata = fragment.metadata as any; const hasDocumentId = metadata?.documentId === documentId; if (hasDocumentId) { - logger.debug(`[Graph API] 📊 Fragment ${fragment.id} metadata:`, metadata); + logger.debug( + `[Graph API] 📊 Fragment ${fragment.id} metadata: ${JSON.stringify(metadata)}` + ); } return hasDocumentId; }); @@ -1097,24 +1154,30 @@ async function expandDocumentGraphHandler(req: any, res: any, runtime: IAgentRun const documentFragments = allFragments.filter((fragment) => { const metadata = fragment.metadata as any; // Check both documentId match and that it's a fragment type (case-insensitive) + // Safely handle type checking with string validation + const typeString = typeof metadata?.type === 'string' ? metadata.type : null; const isFragment = - metadata?.type?.toLowerCase() === 'fragment' || + (typeString && typeString.toLowerCase() === 'fragment') || metadata?.type === MemoryType.FRAGMENT || // If no type but has documentId, assume it's a fragment (!metadata?.type && metadata?.documentId); return metadata?.documentId === documentId && isFragment; }); - // Build fragment nodes and links - const nodes = documentFragments.map((frag) => ({ - id: frag.id!, - type: 'fragment' as const, - })); - - const links = documentFragments.map((frag) => ({ - source: documentId, - target: frag.id!, - })); + // Build fragment nodes and links (filter out any without IDs) + const nodes = documentFragments + .filter((frag) => frag.id !== undefined) + .map((frag) => ({ + id: frag.id as UUID, + type: 'fragment' as const, + })); + + const links = documentFragments + .filter((frag) => frag.id !== undefined) + .map((frag) => ({ + source: documentId, + target: frag.id as UUID, + })); logger.info(`[Graph API] 📊 Found ${nodes.length} fragments for document ${documentId}`); From 152d1c65055e5d793655420cf12f106f6bbc6b4d Mon Sep 17 00:00:00 2001 From: 0xbbjoker <0xbbjoker@proton.me> Date: Sun, 5 Oct 2025 18:57:26 +0900 Subject: [PATCH 4/8] fix(plugin-knowledge): use API response data for Memory fields in graph node details --- src/frontend/ui/memory-graph-optimized.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/ui/memory-graph-optimized.tsx b/src/frontend/ui/memory-graph-optimized.tsx index 5e4a1f9..814e1a5 100644 --- a/src/frontend/ui/memory-graph-optimized.tsx +++ b/src/frontend/ui/memory-graph-optimized.tsx @@ -184,9 +184,9 @@ export function MemoryGraphOptimized({ content: result.data.content, metadata: result.data.metadata, createdAt: result.data.createdAt, - entityId: agentId, - roomId: agentId, - agentId: agentId, + entityId: result.data.entityId, + roomId: result.data.roomId, + agentId: result.data.agentId, }; // Cache the details From 6c185a8b3ef843c039ddbe3fb601c5c694c9d223 Mon Sep 17 00:00:00 2001 From: 0xbbjoker <0xbbjoker@proton.me> Date: Sun, 5 Oct 2025 19:14:53 +0900 Subject: [PATCH 5/8] fix(plugin-knowledge): implement efficient pagination and fix memory field mapping --- src/routes.ts | 18 +++++++++++------- src/service.ts | 25 ++++++++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/routes.ts b/src/routes.ts index 81de73d..2499902 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -887,20 +887,24 @@ async function getGraphNodesHandler(req: any, res: any, runtime: IAgentRuntime) `[Graph API] 📊 Fetching graph nodes: page=${page}, limit=${limit}, type=${type || 'all'}, agent=${agentId}` ); - // Get documents filtered by agent - const documents = await service.getMemories({ + // Get total count of documents for pagination metadata + const totalDocuments = await service.countMemories({ tableName: 'documents', - roomId: agentId, // Filter by agent - count: 10000, // Get all to calculate total count + roomId: agentId, + unique: false, // Count all documents, not just unique ones }); // Calculate pagination - const totalDocuments = documents.length; const totalPages = Math.ceil(totalDocuments / limit); const hasMore = page < totalPages; - // Paginate documents - const paginatedDocuments = documents.slice(offset, offset + limit); + // Fetch only the required page of documents using database-level pagination + const paginatedDocuments = await service.getMemories({ + tableName: 'documents', + roomId: agentId, + count: limit, + offset: offset, + }); // Build lightweight nodes const nodes: Array<{ id: UUID; type: 'document' | 'fragment' }> = []; diff --git a/src/service.ts b/src/service.ts index 91ecfa0..e79884e 100644 --- a/src/service.ts +++ b/src/service.ts @@ -839,10 +839,7 @@ export class KnowledgeService extends Service { return chunks.map((chunk, index) => { // Create a unique ID for the fragment based on document ID, index, and timestamp const fragmentIdContent = `${document.id}-fragment-${index}-${Date.now()}`; - const fragmentId = createUniqueUuid( - this.runtime, - fragmentIdContent - ); + const fragmentId = createUniqueUuid(this.runtime, fragmentIdContent); return { id: fragmentId, @@ -877,14 +874,32 @@ export class KnowledgeService extends Service { tableName: string; // Should be 'documents' or 'knowledge' for this service roomId?: UUID; count?: number; + offset?: number; // For pagination end?: number; // timestamp for "before" }): Promise { return this.runtime.getMemories({ - ...params, // includes tableName, roomId, count, end + ...params, // includes tableName, roomId, count, offset, end agentId: this.runtime.agentId, }); } + /** + * Counts memories for pagination. + * Corresponds to counting documents or fragments. + */ + async countMemories(params: { + tableName: string; + roomId?: UUID; + unique?: boolean; + }): Promise { + // runtime.countMemories expects (roomId, unique, tableName) as positional parameters + const roomId = params.roomId || this.runtime.agentId; + const unique = params.unique ?? false; + const tableName = params.tableName; + + return this.runtime.countMemories(roomId, unique, tableName); + } + /** * Deletes a specific memory item (knowledge document) by its ID. * Corresponds to DELETE /plugins/knowledge/documents/:knowledgeId From d598a7dd3b34c2ab4aac4202a37846c25d06ad4a Mon Sep 17 00:00:00 2001 From: 0xbbjoker <0xbbjoker@proton.me> Date: Sun, 5 Oct 2025 19:20:47 +0900 Subject: [PATCH 6/8] fix(plugin-knowledge): add entityId to API responses and deduplicate graph links --- src/frontend/ui/memory-graph-optimized.tsx | 23 +++++++++++++++++----- src/routes.ts | 2 ++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/frontend/ui/memory-graph-optimized.tsx b/src/frontend/ui/memory-graph-optimized.tsx index 814e1a5..4fa31af 100644 --- a/src/frontend/ui/memory-graph-optimized.tsx +++ b/src/frontend/ui/memory-graph-optimized.tsx @@ -123,11 +123,24 @@ export function MemoryGraphOptimized({ setGraphData({ nodes: graphNodes, links }); setGraphVersion(1); // Reset version for initial load } else { - // Append to existing nodes - setGraphData(prev => ({ - nodes: [...prev.nodes, ...graphNodes], - links: [...prev.links, ...links] - })); + // Append to existing nodes and deduplicate links + setGraphData(prev => { + // Create a set of existing link IDs for fast lookup + const existingLinkIds = new Set( + prev.links.map((link: GraphLink) => `${link.source}->${link.target}`) + ); + + // Filter out duplicate links + const newLinks = links.filter((link: GraphLink) => { + const linkId = `${link.source}->${link.target}`; + return !existingLinkIds.has(linkId); + }); + + return { + nodes: [...prev.nodes, ...graphNodes], + links: [...prev.links, ...newLinks] + }; + }); setGraphVersion(prev => prev + 1); // Increment for additions } diff --git a/src/routes.ts b/src/routes.ts index 2499902..08931c4 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1050,6 +1050,7 @@ async function getGraphNodeDetailsHandler(req: any, res: any, runtime: IAgentRun content: document.content, metadata: document.metadata, createdAt: document.createdAt, + entityId: document.entityId, roomId: document.roomId, agentId: document.agentId, }); @@ -1087,6 +1088,7 @@ async function getGraphNodeDetailsHandler(req: any, res: any, runtime: IAgentRun content: fragment.content, metadata: fragment.metadata, createdAt: fragment.createdAt, + entityId: fragment.entityId, roomId: fragment.roomId, agentId: fragment.agentId, }); From c5feaf164b5a1bc7d4bfe1e4b68cc997bd4db471 Mon Sep 17 00:00:00 2001 From: 0xbbjoker <0xbbjoker@proton.me> Date: Mon, 6 Oct 2025 12:52:11 +0900 Subject: [PATCH 7/8] fix(plugin-knowledge): fix graph pagination deduplication and missing worldId in Memory objects --- src/frontend/ui/memory-graph-optimized.tsx | 15 +++++++++++++-- src/routes.ts | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/frontend/ui/memory-graph-optimized.tsx b/src/frontend/ui/memory-graph-optimized.tsx index 4fa31af..2945365 100644 --- a/src/frontend/ui/memory-graph-optimized.tsx +++ b/src/frontend/ui/memory-graph-optimized.tsx @@ -123,8 +123,18 @@ export function MemoryGraphOptimized({ setGraphData({ nodes: graphNodes, links }); setGraphVersion(1); // Reset version for initial load } else { - // Append to existing nodes and deduplicate links + // Append to existing nodes and deduplicate both nodes and links setGraphData(prev => { + // Create a set of existing node IDs for fast lookup + const existingNodeIds = new Set( + prev.nodes.map((node: GraphNode) => node.id) + ); + + // Filter out duplicate nodes + const newNodes = graphNodes.filter((node: GraphNode) => { + return !existingNodeIds.has(node.id); + }); + // Create a set of existing link IDs for fast lookup const existingLinkIds = new Set( prev.links.map((link: GraphLink) => `${link.source}->${link.target}`) @@ -137,7 +147,7 @@ export function MemoryGraphOptimized({ }); return { - nodes: [...prev.nodes, ...graphNodes], + nodes: [...prev.nodes, ...newNodes], links: [...prev.links, ...newLinks] }; }); @@ -200,6 +210,7 @@ export function MemoryGraphOptimized({ entityId: result.data.entityId, roomId: result.data.roomId, agentId: result.data.agentId, + worldId: result.data.worldId, }; // Cache the details diff --git a/src/routes.ts b/src/routes.ts index 08931c4..e4130de 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1053,6 +1053,7 @@ async function getGraphNodeDetailsHandler(req: any, res: any, runtime: IAgentRun entityId: document.entityId, roomId: document.roomId, agentId: document.agentId, + worldId: document.worldId, }); return; } @@ -1091,6 +1092,7 @@ async function getGraphNodeDetailsHandler(req: any, res: any, runtime: IAgentRun entityId: fragment.entityId, roomId: fragment.roomId, agentId: fragment.agentId, + worldId: fragment.worldId, }); return; } From bc3e1ed8960d82510f914cd2c8e785ce4bfec4df Mon Sep 17 00:00:00 2001 From: 0xbbjoker <0xbbjoker@proton.me> Date: Tue, 7 Oct 2025 16:00:02 +0900 Subject: [PATCH 8/8] fix: reduce fragment fetch limit from 100k to 50k to optimize memory usage --- src/routes.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/routes.ts b/src/routes.ts index e4130de..f99a353 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -705,9 +705,10 @@ async function getKnowledgeChunksHandler(req: any, res: any, runtime: IAgentRunt // If specific document requested, get ALL its fragments if (documentId) { + // High limit to support documents with many fragments, but reduced from 100k to prevent memory issues const allFragments = await service.getMemories({ tableName: 'knowledge', - count: 100000, // Very high limit to get all fragments + count: 50000, // Reduced from 100000 - still high enough for large documents }); const documentFragments = allFragments.filter((fragment) => { @@ -922,10 +923,11 @@ async function getGraphNodesHandler(req: any, res: any, runtime: IAgentRuntime) // Always include fragments for the paginated documents (unless specifically requesting documents only) if (type !== 'document') { // Get all fragments for the paginated documents + // High limit to support documents with many fragments, but reduced from 100k to prevent memory issues const allFragments = await service.getMemories({ tableName: 'knowledge', roomId: agentId, - count: 100000, + count: 50000, // Reduced from 100000 - still high enough for large documents }); logger.debug(`[Graph API] 📊 Total fragments found: ${allFragments.length}`); @@ -1060,9 +1062,10 @@ async function getGraphNodeDetailsHandler(req: any, res: any, runtime: IAgentRun // If not found in documents, try fragments logger.debug(`[Graph API] 📊 Document not found, searching in fragments`); + // High limit to support documents with many fragments, but reduced from 100k to prevent memory issues const allFragments = await service.getMemories({ tableName: 'knowledge', - count: 100000, // High limit + count: 50000, // Reduced from 100000 - still high enough for large documents }); logger.debug(`[Graph API] 📊 Total fragments in DB: ${allFragments.length}`); @@ -1122,10 +1125,11 @@ async function expandDocumentGraphHandler(req: any, res: any, runtime: IAgentRun logger.debug(`[Graph API] 📊 Expanding document: ${documentId}, agent: ${agentId}`); // Get all fragments for this document + // High limit to support documents with many fragments, but reduced from 100k to prevent memory issues const allFragments = await service.getMemories({ tableName: 'knowledge', roomId: agentId, // Filter by agent - count: 100000, // High limit + count: 50000, // Reduced from 100000 - still high enough for large documents }); logger.debug(`[Graph API] 📊 Total fragments in knowledge table: ${allFragments.length}`);