diff --git a/interface/src/api/client.ts b/interface/src/api/client.ts index 837861d77..857282435 100644 --- a/interface/src/api/client.ts +++ b/interface/src/api/client.ts @@ -378,6 +378,32 @@ export interface MemoriesSearchParams { memory_type?: MemoryType; } +export interface MemoryCreateRequest { + agent_id: string; + content: string; + memory_type?: MemoryType; + importance?: number; +} + +export interface MemoryUpdateRequest { + agent_id: string; + memory_id: string; + content?: string; + memory_type?: MemoryType; + importance?: number; +} + +export interface MemoryWriteResponse { + success: boolean; + memory: MemoryItem; +} + +export interface MemoryDeleteResponse { + success: boolean; + forgotten: boolean; + message: string; +} + export type CortexEventType = | "bulletin_generated" | "bulletin_failed" @@ -961,6 +987,38 @@ export const api = { if (params.sort) search.set("sort", params.sort); return fetchJson(`/agents/memories?${search}`); }, + createMemory: async (request: MemoryCreateRequest) => { + const response = await fetch(`${API_BASE}/agents/memories`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + return response.json() as Promise; + }, + updateMemory: async (request: MemoryUpdateRequest) => { + const response = await fetch(`${API_BASE}/agents/memories`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + return response.json() as Promise; + }, + deleteMemory: async (agentId: string, memoryId: string) => { + const search = new URLSearchParams({ agent_id: agentId, memory_id: memoryId }); + const response = await fetch(`${API_BASE}/agents/memories?${search}`, { + method: "DELETE", + }); + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + return response.json() as Promise; + }, searchMemories: (agentId: string, query: string, params: MemoriesSearchParams = {}) => { const search = new URLSearchParams({ agent_id: agentId, q: query }); if (params.limit) search.set("limit", String(params.limit)); diff --git a/interface/src/routes/AgentMemories.tsx b/interface/src/routes/AgentMemories.tsx index 3aba1ec27..dd4f69dde 100644 --- a/interface/src/routes/AgentMemories.tsx +++ b/interface/src/routes/AgentMemories.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useVirtualizer } from "@tanstack/react-virtual"; import { AnimatePresence, motion } from "framer-motion"; import { @@ -13,10 +13,23 @@ import { CortexChatPanel } from "@/components/CortexChatPanel"; import { MemoryGraph } from "@/components/MemoryGraph"; import { Button, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Slider, + TextArea, ToggleGroup, SearchInput, FilterButton, @@ -27,6 +40,28 @@ import { HugeiconsIcon } from "@hugeicons/react"; type ViewMode = "list" | "graph"; +interface MemoryFormData { + content: string; + memory_type: MemoryType; + importance: number; +} + +function emptyFormData(): MemoryFormData { + return { + content: "", + memory_type: "fact", + importance: 0.6, + }; +} + +function memoryToFormData(memory: MemoryItem): MemoryFormData { + return { + content: memory.content, + memory_type: memory.memory_type, + importance: memory.importance, + }; +} + const SORT_OPTIONS: { value: MemorySort; label: string }[] = [ { value: "recent", label: "Recent" }, { value: "importance", label: "Importance" }, @@ -71,6 +106,7 @@ interface AgentMemoriesProps { } export function AgentMemories({ agentId }: AgentMemoriesProps) { + const queryClient = useQueryClient(); const [viewMode, setViewMode] = useState("list"); const [searchQuery, setSearchQuery] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); @@ -78,6 +114,11 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { const [typeFilter, setTypeFilter] = useState(null); const [expandedId, setExpandedId] = useState(null); const [chatOpen, setChatOpen] = useState(true); + const [editorOpen, setEditorOpen] = useState(false); + const [editingMemory, setEditingMemory] = useState(null); + const [formData, setFormData] = useState(emptyFormData()); + const [formError, setFormError] = useState(null); + const [deleteConfirmMemoryId, setDeleteConfirmMemoryId] = useState(null); const parentRef = useRef(null); @@ -128,15 +169,110 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { const isLoading = isSearching ? searchQueryResult.isLoading : listQuery.isLoading; const isError = isSearching ? searchQueryResult.isError : listQuery.isError; + const refreshMemories = () => { + queryClient.invalidateQueries({ queryKey: ["memories", agentId] }); + queryClient.invalidateQueries({ queryKey: ["memories-search", agentId] }); + }; + + const createMutation = useMutation({ + mutationFn: () => { + return api.createMemory({ + agent_id: agentId, + content: formData.content, + memory_type: formData.memory_type, + importance: formData.importance, + }); + }, + onSuccess: () => { + refreshMemories(); + setEditorOpen(false); + setEditingMemory(null); + setFormData(emptyFormData()); + setFormError(null); + }, + onError: () => setFormError("Failed to create memory"), + }); + + const updateMutation = useMutation({ + mutationFn: () => { + if (!editingMemory) throw new Error("No memory selected"); + return api.updateMemory({ + agent_id: agentId, + memory_id: editingMemory.id, + content: formData.content, + memory_type: formData.memory_type, + importance: formData.importance, + }); + }, + onSuccess: () => { + refreshMemories(); + setEditorOpen(false); + setEditingMemory(null); + setFormData(emptyFormData()); + setFormError(null); + }, + onError: () => setFormError("Failed to update memory"), + }); + + const deleteMutation = useMutation({ + mutationFn: (memoryId: string) => api.deleteMemory(agentId, memoryId), + onSuccess: (_response, memoryId) => { + refreshMemories(); + if (expandedId === memoryId) { + setExpandedId(null); + } + setDeleteConfirmMemoryId(null); + }, + }); + + const openCreateDialog = () => { + setEditingMemory(null); + setFormData(emptyFormData()); + setFormError(null); + setEditorOpen(true); + }; + + const openEditDialog = (memory: MemoryItem) => { + setEditingMemory(memory); + setFormData(memoryToFormData(memory)); + setFormError(null); + setEditorOpen(true); + }; + + const saveMemory = () => { + const trimmedContent = formData.content.trim(); + if (!trimmedContent) { + setFormError("Content is required"); + return; + } + + if (formData.importance < 0 || formData.importance > 1) { + setFormError("Importance must be between 0 and 1"); + return; + } + + setFormError(null); + if (editingMemory) { + updateMutation.mutate(); + } else { + createMutation.mutate(); + } + }; + const virtualizer = useVirtualizer({ count: memories.length, getScrollElement: () => parentRef.current, + getItemKey: (index) => memories[index]?.id ?? index, estimateSize: useCallback((index: number) => { return expandedId === memories[index]?.id ? 200 : 48; }, [expandedId, memories]), overscan: 10, }); + useEffect(() => { + virtualizer.measure(); + }, [memories.length, expandedId, virtualizer]); + // Reset expanded when data changes useEffect(() => { setExpandedId(null); @@ -174,6 +310,10 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { + + {/* View mode toggle */} {/* Table header */} -
+
Type {isSearching ? "Content / Score" : "Content"} Importance - Source Created
@@ -277,7 +416,7 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { + +
+
ID: {memory.id} Accessed: {memory.access_count}x Last accessed: {formatTimeAgo(memory.last_accessed_at)} Updated: {formatTimeAgo(memory.updated_at)} - {memory.channel_id && ( - Channel: {memory.channel_id} - )}
@@ -355,6 +500,119 @@ export function AgentMemories({ agentId }: AgentMemoriesProps) { )} + + !open && setEditorOpen(false)}> + + + {editingMemory ? "Edit memory" : "Add memory"} + +
+
+ +