diff --git a/packages/tools/src/shared/memory-client.ts b/packages/tools/src/shared/memory-client.ts index 5b9dd433..97bbfa5e 100644 --- a/packages/tools/src/shared/memory-client.ts +++ b/packages/tools/src/shared/memory-client.ts @@ -1,3 +1,4 @@ +import Supermemory from "supermemory" import { deduplicateMemories } from "../tools-shared" import type { Logger, @@ -11,6 +12,7 @@ import { convertProfileToMarkdown, defaultPromptTemplate, } from "./prompt-builder" +import { createSupermemoryClient } from "./context" /** * Fetches profile and search results from the Supermemory API. @@ -84,10 +86,142 @@ export interface BuildMemoriesTextOptions { searchLimit?: number } +/** + * Search result structure for unified handling. + */ +interface SearchResult { + content: string + metadata?: Record +} + +/** + * Searches for memories using the Supermemory SDK. + * + * @param client - Supermemory client instance + * @param containerTag - Container tag for scoping + * @param query - Search query text + * @param limit - Maximum number of results + * @returns Array of search results + */ +async function searchMemoriesSDK( + client: Supermemory, + containerTag: string, + query: string, + limit: number, +): Promise { + const response = await client.search.memories({ + q: query, + containerTag, + limit, + include: { chunks: false }, + }) + return (response.results || []).map((r) => ({ + content: r.memory || "", + metadata: r.metadata ?? undefined, + })) +} + +/** + * Searches for document chunks using the Supermemory SDK. + * Only includes chunks marked as relevant (isRelevant: true). + * + * @param client - Supermemory client instance + * @param containerTag - Container tag for scoping + * @param query - Search query text + * @param limit - Maximum number of results + * @returns Array of search results containing only relevant chunks + */ +async function searchDocumentsSDK( + client: Supermemory, + containerTag: string, + query: string, + limit: number, +): Promise { + const response = await client.search.documents({ + q: query, + containerTags: [containerTag], + limit, + }) + // Extract only relevant chunks from each document result + const results: SearchResult[] = [] + for (const doc of response.results || []) { + for (const chunk of doc.chunks || []) { + // Only include chunks marked as relevant + if (chunk.isRelevant) { + results.push({ + content: chunk.content || "", + metadata: doc.metadata ?? undefined, + }) + } + } + } + return results +} + +/** + * Performs search based on the specified search mode. + * + * @param client - Supermemory client instance + * @param containerTag - Container tag for scoping + * @param query - Search query text + * @param searchMode - Search mode: "memories", "hybrid", or "documents" + * @param limit - Maximum number of results per search type + * @param logger - Logger instance + * @returns Combined array of search results + */ +async function performSearch( + client: Supermemory, + containerTag: string, + query: string, + searchMode: SearchMode, + limit: number, + logger: Logger, +): Promise { + logger.debug("Performing search", { searchMode, containerTag, limit }) + + switch (searchMode) { + case "memories": + return searchMemoriesSDK(client, containerTag, query, limit) + + case "documents": + return searchDocumentsSDK(client, containerTag, query, limit) + + case "hybrid": { + // Run both searches in parallel + const [memoriesResults, documentsResults] = await Promise.all([ + searchMemoriesSDK(client, containerTag, query, limit), + searchDocumentsSDK(client, containerTag, query, limit), + ]) + + logger.debug("Hybrid search completed", { + memoriesCount: memoriesResults.length, + documentsCount: documentsResults.length, + }) + + // Combine results, memories first + return [...memoriesResults, ...documentsResults] + } + + default: + logger.warn(`Unknown search mode: ${searchMode}, defaulting to memories`) + return searchMemoriesSDK(client, containerTag, query, limit) + } +} + /** * Fetches memories from the API, deduplicates them, and formats them into * the final string to be injected into the system prompt. * + * When searchMode is specified, uses the Supermemory SDK search endpoints: + * - "memories": Uses search.memories() for memory entries + * - "documents": Uses search.documents() for document chunks + * - "hybrid": Uses both endpoints in parallel + * + * The mode option controls whether profile data is included: + * - "profile": Only profile data (static/dynamic), no search + * - "query": Only search results, no profile data + * - "full": Both profile data and search results + * * @param options - Configuration for building memories text * @returns The final formatted memories string ready for injection */ @@ -102,48 +236,77 @@ export const buildMemoriesText = async ( apiKey, logger, promptTemplate = defaultPromptTemplate, + searchMode = "memories", + searchLimit = 10, } = options - const memoriesResponse = await supermemoryProfileSearch( - containerTag, - queryText, - baseUrl, - apiKey, - ) + // Fetch profile data when mode includes profile (profile or full) + let profileData: ProfileStructure | null = null + if (mode !== "query") { + profileData = await supermemoryProfileSearch( + containerTag, + mode === "profile" ? "" : queryText, // Only send query for full mode + baseUrl, + apiKey, + ) - const memoryCountStatic = memoriesResponse.profile.static?.length || 0 - const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0 + const memoryCountStatic = profileData.profile.static?.length || 0 + const memoryCountDynamic = profileData.profile.dynamic?.length || 0 - logger.info("Memory search completed", { - containerTag, - memoryCountStatic, - memoryCountDynamic, - queryText: - queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""), - mode, - }) + logger.info("Profile search completed", { + containerTag, + memoryCountStatic, + memoryCountDynamic, + mode, + }) + } + // Perform SDK-based search when mode includes query (query or full) + let searchResults: SearchResult[] = [] + if (mode !== "profile" && queryText) { + const client = createSupermemoryClient({ apiKey, baseUrl }) + searchResults = await performSearch( + client, + containerTag, + queryText, + searchMode, + searchLimit, + logger, + ) + + logger.info("Search completed", { + containerTag, + searchMode, + searchLimit, + resultCount: searchResults.length, + queryText: + queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""), + }) + } + + // Deduplicate profile memories const deduplicated = deduplicateMemories({ - static: memoriesResponse.profile.static, - dynamic: memoriesResponse.profile.dynamic, - searchResults: memoriesResponse.searchResults?.results, + static: profileData?.profile.static, + dynamic: profileData?.profile.dynamic, + searchResults: searchResults.map((r) => ({ memory: r.content })), }) logger.debug("Memory deduplication completed", { static: { - original: memoryCountStatic, + original: profileData?.profile.static?.length || 0, deduplicated: deduplicated.static.length, }, dynamic: { - original: memoryCountDynamic, + original: profileData?.profile.dynamic?.length || 0, deduplicated: deduplicated.dynamic.length, }, searchResults: { - original: memoriesResponse.searchResults?.results?.length, + original: searchResults.length, deduplicated: deduplicated.searchResults?.length, }, }) + // Build user memories from profile (static + dynamic) const userMemories = mode !== "query" ? convertProfileToMarkdown({ @@ -154,9 +317,11 @@ export const buildMemoriesText = async ( searchResults: { results: [] }, }) : "" + + // Build search results text const generalSearchMemories = - mode !== "profile" - ? `Search results for user's recent message: \n${deduplicated.searchResults + mode !== "profile" && deduplicated.searchResults.length > 0 + ? `Search results for user's recent message:\n${deduplicated.searchResults .map((memory) => `- ${memory}`) .join("\n")}` : "" @@ -164,7 +329,10 @@ export const buildMemoriesText = async ( const promptData: MemoryPromptData = { userMemories, generalSearchMemories, - searchResults: memoriesResponse.searchResults?.results ?? [], + searchResults: searchResults.map((r) => ({ + memory: r.content, + metadata: r.metadata, + })), } const memories = promptTemplate(promptData)