From bb950d4087e8543779f8737cb3c11802c60aa7b4 Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Sat, 18 Apr 2026 00:10:29 -0700 Subject: [PATCH] bug fixes --- packages/tools/package.json | 1 + packages/tools/src/shared/memory-client.ts | 303 ++++++++++++++++----- packages/tools/src/vercel/util.ts | 18 +- 3 files changed, 255 insertions(+), 67 deletions(-) diff --git a/packages/tools/package.json b/packages/tools/package.json index 025492f1..f617adb1 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -44,6 +44,7 @@ "./claude-memory": "./dist/claude-memory.js", "./openai": "./dist/openai/index.js", "./mastra": "./dist/mastra/index.js", + "./voltagent": "./dist/voltagent/index.js", "./package.json": "./package.json" }, "repository": { diff --git a/packages/tools/src/shared/memory-client.ts b/packages/tools/src/shared/memory-client.ts index e33e4eb5..7e8d30e7 100644 --- a/packages/tools/src/shared/memory-client.ts +++ b/packages/tools/src/shared/memory-client.ts @@ -1,4 +1,4 @@ -import { deduplicateMemories } from "../tools-shared" +import Supermemory from "supermemory" import type { Logger, MemoryMode, @@ -7,10 +7,8 @@ import type { ProfileStructure, PromptTemplate, } from "./types" -import { - convertProfileToMarkdown, - defaultPromptTemplate, -} from "./prompt-builder" +import { defaultPromptTemplate } from "./prompt-builder" +import { normalizeBaseUrl } from "./context" /** * Fetches profile and search results from the Supermemory API. @@ -84,10 +82,97 @@ export interface BuildMemoriesTextOptions { searchLimit?: number } +/** + * Searches for document chunks using the Supermemory search API. + * + * @param client - The Supermemory client instance + * @param containerTag - The container tag for scoping + * @param queryText - The search query + * @param limit - Maximum number of results + * @returns Array of chunk content strings + */ +const searchDocumentChunks = async ( + client: Supermemory, + containerTag: string, + queryText: string, + limit: number, +): Promise => { + if (!queryText) return [] + + const response = await client.search.documents({ + q: queryText, + containerTags: [containerTag], + limit, + }) + + const chunks: string[] = [] + for (const result of response.results) { + for (const chunk of result.chunks) { + if (chunk.isRelevant) { + chunks.push(chunk.content) + } + } + } + return chunks +} + +/** + * Searches for memories with optional document chunks using the Supermemory search API. + * + * @param client - The Supermemory client instance + * @param containerTag - The container tag for scoping + * @param queryText - The search query + * @param limit - Maximum number of results + * @param includeChunks - Whether to include document chunks (hybrid mode) + * @returns Object with memories array and optional chunks array + */ +const searchMemoriesWithChunks = async ( + client: Supermemory, + containerTag: string, + queryText: string, + limit: number, + includeChunks: boolean, +): Promise<{ memories: string[]; chunks: string[] }> => { + if (!queryText) return { memories: [], chunks: [] } + + const response = await client.search.memories({ + q: queryText, + containerTag, + limit, + include: { + chunks: includeChunks, + }, + }) + + const memories: string[] = [] + const chunks: string[] = [] + + for (const result of response.results) { + memories.push(result.memory) + if (includeChunks && result.chunks) { + for (const chunk of result.chunks) { + chunks.push(chunk.content) + } + } + } + + return { memories, chunks } +} + /** * Fetches memories from the API, deduplicates them, and formats them into * the final string to be injected into the system prompt. * + * Memory modes (controls profile API /v4/profile): + * - "profile": Fetches user profile without query-based filtering + * - "query": Fetches user profile with query-based semantic search + * - "full": Same as "query" - fetches profile with query search + * + * Search modes (controls search endpoints - independent of mode): + * - "memories": Uses search.memories endpoint only (memory entries) + * - "hybrid": Uses both search.memories AND search.documents (memories + chunks) + * - "documents": Uses search.documents endpoint only (document chunks) + * * @param options - Configuration for building memories text * @returns The final formatted memories string ready for injection */ @@ -102,80 +187,172 @@ export const buildMemoriesText = async ( apiKey, logger, promptTemplate = defaultPromptTemplate, + searchMode = "memories", + searchLimit = 10, } = options - const memoriesResponse = await supermemoryProfileSearch( - containerTag, - queryText, - baseUrl, - apiKey, + const normalizedBaseUrl = normalizeBaseUrl(baseUrl) + + // Determine if we need the Supermemory client (for search operations) + const needsSearchClient = queryText && searchMode !== undefined + + let client: Supermemory | null = null + if (needsSearchClient) { + client = new Supermemory({ + apiKey, + ...(normalizedBaseUrl !== "https://api.supermemory.ai" + ? { baseURL: normalizedBaseUrl } + : {}), + }) + } + + // 1. Fetch profile based on mode (MemoryMode) + let profileData: ProfileStructure | null = null + const profileQueryText = mode === "profile" ? "" : queryText + + try { + profileData = await supermemoryProfileSearch( + containerTag, + profileQueryText, + normalizedBaseUrl, + apiKey, + ) + logger.info("Profile search completed", { + containerTag, + mode, + hasStatic: (profileData.profile?.static?.length ?? 0) > 0, + hasDynamic: (profileData.profile?.dynamic?.length ?? 0) > 0, + hasSearchResults: (profileData.searchResults?.results?.length ?? 0) > 0, + }) + } catch (error) { + logger.error("Profile search failed", { + error: error instanceof Error ? error.message : "Unknown error", + }) + throw error + } + + // 2. Execute search based on searchMode (SearchMode) - independent of profile + let searchMemories: string[] = [] + let documentChunks: string[] = [] + + if (queryText && client) { + if (searchMode === "memories") { + // Memories only - use search.memories + const result = await searchMemoriesWithChunks( + client, + containerTag, + queryText, + searchLimit, + false, // no chunks + ) + searchMemories = result.memories + logger.info("Memory search completed", { + containerTag, + searchMode, + memoryCount: searchMemories.length, + queryText: + queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""), + }) + } else if (searchMode === "documents") { + // Documents only - use search.documents + documentChunks = await searchDocumentChunks( + client, + containerTag, + queryText, + searchLimit, + ) + logger.info("Document search completed", { + containerTag, + searchMode, + chunkCount: documentChunks.length, + queryText: + queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""), + }) + } else if (searchMode === "hybrid") { + // Hybrid - use both search.memories AND search.documents + const [memoriesResult, chunksResult] = await Promise.all([ + searchMemoriesWithChunks( + client, + containerTag, + queryText, + searchLimit, + false, + ), + searchDocumentChunks(client, containerTag, queryText, searchLimit), + ]) + searchMemories = memoriesResult.memories + documentChunks = chunksResult + logger.info("Hybrid search completed", { + containerTag, + searchMode, + memoryCount: searchMemories.length, + chunkCount: documentChunks.length, + queryText: + queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""), + }) + } + } else if (!queryText) { + logger.debug("No query text provided, skipping search", { + containerTag, + searchMode, + }) + } + + // 3. Build the combined result from profile AND search + // Extract profile memories + const staticMemories = + profileData?.profile?.static?.map((m) => m.memory).filter(Boolean) ?? [] + const dynamicMemories = + profileData?.profile?.dynamic?.map((m) => m.memory).filter(Boolean) ?? [] + const profileSearchResults = + profileData?.searchResults?.results?.map((m) => m.memory).filter(Boolean) ?? + [] + + // Combine all profile-based memories + const allProfileMemories = [ + ...staticMemories, + ...dynamicMemories, + ...profileSearchResults, + ] + + // Deduplicate memories (profile + search) + const allMemories = Array.from( + new Set([...allProfileMemories, ...searchMemories]), ) - const memoryCountStatic = memoriesResponse.profile.static?.length || 0 - const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0 + // Build user memories section (from profile) + let userMemories = "" + if (allProfileMemories.length > 0) { + userMemories = allProfileMemories.map((memory) => `- ${memory}`).join("\n") + } - logger.info("Memory search completed", { - containerTag, - memoryCountStatic, - memoryCountDynamic, - queryText: - queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""), - mode, - }) + // Build search results section (from searchMode) + let generalSearchMemories = "" + if (searchMemories.length > 0) { + generalSearchMemories = `Relevant memories:\n${searchMemories.map((memory) => `- ${memory}`).join("\n")}` + } - const deduplicated = deduplicateMemories({ - static: memoriesResponse.profile.static, - dynamic: memoriesResponse.profile.dynamic, - searchResults: memoriesResponse.searchResults?.results, - }) - - logger.debug("Memory deduplication completed", { - static: { - original: memoryCountStatic, - deduplicated: deduplicated.static.length, - }, - dynamic: { - original: memoryCountDynamic, - deduplicated: deduplicated.dynamic.length, - }, - searchResults: { - original: memoriesResponse.searchResults?.results?.length, - deduplicated: deduplicated.searchResults?.length, - }, - }) - - const userMemories = - mode !== "query" - ? convertProfileToMarkdown({ - profile: { - static: deduplicated.static, - dynamic: deduplicated.dynamic, - }, - searchResults: { results: [] }, - }) - : "" - const generalSearchMemories = - mode !== "profile" - ? `Search results for user's recent message: \n${deduplicated.searchResults - .map((memory) => `- ${memory}`) - .join("\n")}` - : "" + // Add document chunks section + if (documentChunks.length > 0) { + const prefix = generalSearchMemories ? "\n\n" : "" + generalSearchMemories += `${prefix}Relevant document excerpts:\n${documentChunks.map((chunk) => `- ${chunk}`).join("\n")}` + } const promptData: MemoryPromptData = { userMemories, generalSearchMemories, - searchResults: memoriesResponse.searchResults?.results ?? [], + searchResults: allMemories.map((memory) => ({ memory })), } - const memories = promptTemplate(promptData) - if (memories) { + const result = promptTemplate(promptData) + if (result) { logger.debug("Memory content preview", { - content: memories, - fullLength: memories.length, + content: result, + fullLength: result.length, }) } - return memories + return result } /** diff --git a/packages/tools/src/vercel/util.ts b/packages/tools/src/vercel/util.ts index 572f44a3..6fc8df03 100644 --- a/packages/tools/src/vercel/util.ts +++ b/packages/tools/src/vercel/util.ts @@ -84,11 +84,21 @@ export const getLastUserMessage = (params: LanguageModelCallOptions) => { .slice() .reverse() .find((prompt: LanguageModelMessage) => prompt.role === "user") - const memories = lastUserMessage?.content - .filter((content) => content.type === "text") - .map((content) => (content as { type: "text"; text: string }).text) + + const content = lastUserMessage?.content + if (!content) return undefined + + // Handle string content (allowed by Vercel AI SDK) + if (typeof content === "string") { + return content + } + + // Handle array content + // biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 + return (content as any[]) + .filter((part) => part.type === "text") + .map((part) => part.text || "") .join(" ") - return memories } export const filterOutSupermemories = (content: string) => {