bug fixes

This commit is contained in:
Sreeram Sreedhar 2026-04-18 00:10:29 -07:00
parent 4b51f4e595
commit bb950d4087
3 changed files with 255 additions and 67 deletions

View file

@ -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": {

View file

@ -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<string[]> => {
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
}
/**

View file

@ -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) => {