-
+
+
+
{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 (
+
+ );
+ }
+
+ // Render error state
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Legend */}
+
+
+ {/* Pagination */}
+ {pagination && pagination.hasMore && (
+
+
+ Load More Documents ({pagination.currentPage}/{pagination.totalPages})
+
+
+ )}
+
+ {/* 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 }) {
-
setSelectedMemory(null)}
- className="text-xs h-7 px-2"
- >
- Close
-
+
+ {isDocument && (
+ setViewingContent(memory)}
+ className="text-xs h-7 px-3"
+ >
+
+ Open Document
+
+ )}
+ setSelectedMemory(null)}
+ className="text-xs h-7 px-2"
+ >
+ Close
+
+
-
-
- {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}`);