mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
feat(@supermemory/tools): vercel ai sdk compatbile with v5 and v6 (#628)
This commit is contained in:
parent
5493455f69
commit
0e1f062fa9
5 changed files with 327 additions and 366 deletions
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "@supermemory/tools",
|
||||
"type": "module",
|
||||
"version": "1.4.4",
|
||||
"version": "1.3.62",
|
||||
"description": "Memory tools for AI SDK and OpenAI function calling with supermemory",
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch --ignore-watch .turbo",
|
||||
"dev": "tsdown --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest --testTimeout 100000",
|
||||
"test:watch": "vitest --watch --testTimeout 100000"
|
||||
|
|
@ -14,31 +14,22 @@
|
|||
"@ai-sdk/anthropic": "^2.0.25",
|
||||
"@ai-sdk/openai": "^2.0.23",
|
||||
"ai": "^5.0.29",
|
||||
"lru-cache": "^11.2.6",
|
||||
"openai": "^4.104.0",
|
||||
"supermemory": "^3.0.0-alpha.26",
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/provider": "^3.0.0",
|
||||
"@anthropic-ai/sdk": "^0.65.0",
|
||||
"@voltagent/core": "^2.6.12",
|
||||
"@mastra/core": "^1.0.0",
|
||||
"@total-typescript/tsconfig": "^1.0.4",
|
||||
"@types/bun": "^1.2.21",
|
||||
"dotenv": "^16.6.1",
|
||||
"tsdown": "^0.14.2",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^3.2.4",
|
||||
"@anthropic-ai/sdk": "^0.65.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ai-sdk/provider": "^2.0.0 || ^3.0.0",
|
||||
"@voltagent/core": "^2.6.12"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@voltagent/core": {
|
||||
"optional": true
|
||||
}
|
||||
"@ai-sdk/provider": "^2.0.0 || ^3.0.0"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
|
|
@ -50,9 +41,7 @@
|
|||
".": "./dist/index.js",
|
||||
"./ai-sdk": "./dist/ai-sdk.js",
|
||||
"./claude-memory": "./dist/claude-memory.js",
|
||||
"./mastra": "./dist/mastra.js",
|
||||
"./openai": "./dist/openai/index.js",
|
||||
"./voltagent": "./dist/voltagent/index.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -10,46 +10,14 @@ import {
|
|||
extractAssistantResponseText,
|
||||
saveMemoryAfterResponse,
|
||||
} from "./middleware"
|
||||
import type { PromptTemplate, MemoryPromptData } from "./memory-prompt"
|
||||
|
||||
interface WrapVercelLanguageModelOptions {
|
||||
/** Optional conversation ID to group messages for contextual memory generation */
|
||||
conversationId?: string
|
||||
/** Enable detailed logging of memory search and injection */
|
||||
verbose?: boolean
|
||||
/**
|
||||
* Memory retrieval mode:
|
||||
* - "profile": Retrieves user profile memories (static + dynamic) without query filtering
|
||||
* - "query": Searches memories based on semantic similarity to the user's message
|
||||
* - "full": Combines both profile and query-based results
|
||||
*/
|
||||
mode?: "profile" | "query" | "full"
|
||||
/**
|
||||
* Memory persistence mode:
|
||||
* - "always": Automatically save conversations as memories
|
||||
* - "never": Only retrieve memories, don't store new ones
|
||||
*/
|
||||
addMemory?: "always" | "never"
|
||||
/** Supermemory API key (falls back to SUPERMEMORY_API_KEY env var) */
|
||||
apiKey?: string
|
||||
/** Custom Supermemory API base URL */
|
||||
baseUrl?: string
|
||||
/**
|
||||
* Custom function to format memory data into the system prompt.
|
||||
* If not provided, uses the default "User Supermemories:" format.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* promptTemplate: (data) => `
|
||||
* <user_memories>
|
||||
* Here is some information about your past conversations:
|
||||
* ${data.userMemories}
|
||||
* ${data.generalSearchMemories}
|
||||
* </user_memories>
|
||||
* `.trim()
|
||||
* ```
|
||||
*/
|
||||
promptTemplate?: PromptTemplate
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -116,121 +84,103 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
mode: options?.mode ?? "profile",
|
||||
addMemory: options?.addMemory ?? "never",
|
||||
baseUrl: options?.baseUrl,
|
||||
promptTemplate: options?.promptTemplate,
|
||||
})
|
||||
|
||||
// Proxy keeps prototype/getter fields (e.g. provider, modelId) that `{ ...model }` drops.
|
||||
return new Proxy(model, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === "doGenerate") {
|
||||
return async (params: LanguageModelCallOptions) => {
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(
|
||||
params,
|
||||
ctx,
|
||||
)
|
||||
const wrappedModel = {
|
||||
...model,
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
const result = await target.doGenerate(transformedParams as any)
|
||||
doGenerate: async (params: LanguageModelCallOptions) => {
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(params, ctx)
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
const result = await model.doGenerate(transformedParams as any)
|
||||
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (ctx.addMemory === "always" && userMessage && userMessage.trim()) {
|
||||
const assistantResponseText = extractAssistantResponseText(
|
||||
result.content as unknown[],
|
||||
)
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
assistantResponseText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error generating response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
doStream: async (params: LanguageModelCallOptions) => {
|
||||
let generatedText = ""
|
||||
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(params, ctx)
|
||||
|
||||
const { stream, ...rest } = await model.doStream(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
transformedParams as any,
|
||||
)
|
||||
|
||||
const transformStream = new TransformStream<
|
||||
LanguageModelStreamPart,
|
||||
LanguageModelStreamPart
|
||||
>({
|
||||
transform(chunk, controller) {
|
||||
if (chunk.type === "text-delta") {
|
||||
generatedText += chunk.delta
|
||||
}
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
flush: async () => {
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
const assistantResponseText = extractAssistantResponseText(
|
||||
result.content as unknown[],
|
||||
)
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
assistantResponseText,
|
||||
generatedText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error generating response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
return {
|
||||
stream: stream.pipeThrough(transformStream),
|
||||
...rest,
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error streaming response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
if (prop === "doStream") {
|
||||
return async (params: LanguageModelCallOptions) => {
|
||||
let generatedText = ""
|
||||
|
||||
try {
|
||||
const transformedParams = await transformParamsWithMemory(
|
||||
params,
|
||||
ctx,
|
||||
)
|
||||
|
||||
const { stream, ...rest } = await target.doStream(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3
|
||||
transformedParams as any,
|
||||
)
|
||||
|
||||
const transformStream = new TransformStream<
|
||||
LanguageModelStreamPart,
|
||||
LanguageModelStreamPart
|
||||
>({
|
||||
transform(chunk, controller) {
|
||||
if (chunk.type === "text-delta") {
|
||||
generatedText += chunk.delta
|
||||
}
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
flush: async () => {
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
generatedText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
stream: stream.pipeThrough(transformStream),
|
||||
...rest,
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error("Error streaming response", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.get(target, prop, receiver)
|
||||
},
|
||||
}) as T
|
||||
} as T
|
||||
|
||||
return wrappedModel
|
||||
}
|
||||
|
||||
export {
|
||||
wrapVercelLanguageModel as withSupermemory,
|
||||
type WrapVercelLanguageModelOptions as WithSupermemoryOptions,
|
||||
type PromptTemplate,
|
||||
type MemoryPromptData,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +1,144 @@
|
|||
// Re-export shared types and functions
|
||||
export {
|
||||
type MemoryPromptData,
|
||||
type PromptTemplate,
|
||||
defaultPromptTemplate,
|
||||
normalizeBaseUrl,
|
||||
buildMemoriesText,
|
||||
type BuildMemoriesTextOptions,
|
||||
} from "../shared"
|
||||
import { deduplicateMemories } from "../shared"
|
||||
import type { Logger } from "./logger"
|
||||
import {
|
||||
type LanguageModelCallOptions,
|
||||
convertProfileToMarkdown,
|
||||
type ProfileStructure,
|
||||
} from "./util"
|
||||
|
||||
import type { Logger, MemoryPromptData } from "../shared"
|
||||
import type { LanguageModelCallOptions } from "./util"
|
||||
|
||||
/**
|
||||
* Extracts the query text from params based on mode.
|
||||
* For "profile" mode, returns empty string (no query needed).
|
||||
* For "query" or "full" mode, extracts the last user message text.
|
||||
*
|
||||
* @param params - The language model call options
|
||||
* @param mode - The memory retrieval mode
|
||||
* @returns The query text for memory search
|
||||
*/
|
||||
export const extractQueryText = (
|
||||
params: LanguageModelCallOptions,
|
||||
mode: "profile" | "query" | "full",
|
||||
): string => {
|
||||
if (mode === "profile") {
|
||||
return ""
|
||||
}
|
||||
|
||||
const userMessage = params.prompt
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((prompt: { role: string }) => prompt.role === "user")
|
||||
|
||||
const content = userMessage?.content
|
||||
if (!content) return ""
|
||||
|
||||
if (typeof content === "string") {
|
||||
return 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(" ")
|
||||
export const normalizeBaseUrl = (url?: string): string => {
|
||||
const defaultUrl = "https://api.supermemory.ai"
|
||||
if (!url) return defaultUrl
|
||||
return url.endsWith("/") ? url.slice(0, -1) : url
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects memories string into params by appending to existing system prompt
|
||||
* or creating a new one. Pure function - does not mutate the original params.
|
||||
*
|
||||
* @param params - The language model call options
|
||||
* @param memories - The formatted memories string to inject
|
||||
* @param logger - Logger for debug output
|
||||
* @returns New params with memories injected into the system prompt
|
||||
*/
|
||||
export const injectMemoriesIntoParams = (
|
||||
const supermemoryProfileSearch = async (
|
||||
containerTag: string,
|
||||
queryText: string,
|
||||
baseUrl: string,
|
||||
): Promise<ProfileStructure> => {
|
||||
const payload = queryText
|
||||
? JSON.stringify({
|
||||
q: queryText,
|
||||
containerTag: containerTag,
|
||||
})
|
||||
: JSON.stringify({
|
||||
containerTag: containerTag,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v4/profile`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
|
||||
},
|
||||
body: payload,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "Unknown error")
|
||||
throw new Error(
|
||||
`Supermemory profile search failed: ${response.status} ${response.statusText}. ${errorText}`,
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw error
|
||||
}
|
||||
throw new Error(`Supermemory API request failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const addSystemPrompt = async (
|
||||
params: LanguageModelCallOptions,
|
||||
memories: string,
|
||||
containerTag: string,
|
||||
logger: Logger,
|
||||
): LanguageModelCallOptions => {
|
||||
mode: "profile" | "query" | "full",
|
||||
baseUrl = "https://api.supermemory.ai",
|
||||
): Promise<LanguageModelCallOptions> => {
|
||||
const systemPromptExists = params.prompt.some(
|
||||
(prompt) => prompt.role === "system",
|
||||
)
|
||||
|
||||
const queryText =
|
||||
mode !== "profile"
|
||||
? params.prompt
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((prompt) => prompt.role === "user")
|
||||
?.content?.filter((content) => content.type === "text")
|
||||
?.map((content) => (content.type === "text" ? content.text : ""))
|
||||
?.join(" ") || ""
|
||||
: ""
|
||||
|
||||
const memoriesResponse = await supermemoryProfileSearch(
|
||||
containerTag,
|
||||
queryText,
|
||||
baseUrl,
|
||||
)
|
||||
|
||||
const memoryCountStatic = memoriesResponse.profile.static?.length || 0
|
||||
const memoryCountDynamic = memoriesResponse.profile.dynamic?.length || 0
|
||||
|
||||
logger.info("Memory search completed", {
|
||||
containerTag,
|
||||
memoryCountStatic,
|
||||
memoryCountDynamic,
|
||||
queryText:
|
||||
queryText.substring(0, 100) + (queryText.length > 100 ? "..." : ""),
|
||||
mode,
|
||||
})
|
||||
|
||||
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 profileData =
|
||||
mode !== "query"
|
||||
? convertProfileToMarkdown({
|
||||
profile: {
|
||||
static: deduplicated.static,
|
||||
dynamic: deduplicated.dynamic,
|
||||
},
|
||||
searchResults: { results: [] },
|
||||
})
|
||||
: ""
|
||||
const searchResultsMemories =
|
||||
mode !== "profile"
|
||||
? `Search results for user's recent message: \n${deduplicated.searchResults
|
||||
.map((memory) => `- ${memory}`)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
|
||||
const memories =
|
||||
`User Supermemories: \n${profileData}\n${searchResultsMemories}`.trim()
|
||||
if (memories) {
|
||||
logger.debug("Memory content preview", {
|
||||
content: memories,
|
||||
fullLength: memories.length,
|
||||
})
|
||||
}
|
||||
|
||||
if (systemPromptExists) {
|
||||
logger.debug("Added memories to existing system prompt")
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Union type compatibility between V2 and V3 prompt types
|
||||
|
|
@ -86,35 +160,3 @@ export const injectMemoriesIntoParams = (
|
|||
] as any
|
||||
return { ...params, prompt: newPrompt } as LanguageModelCallOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds memories to the system prompt by fetching from API and injecting.
|
||||
* This is the original combined function, now implemented via helpers.
|
||||
*
|
||||
* @deprecated Prefer using buildMemoriesText + injectMemoriesIntoParams for caching support
|
||||
*/
|
||||
export const addSystemPrompt = async (
|
||||
params: LanguageModelCallOptions,
|
||||
containerTag: string,
|
||||
logger: Logger,
|
||||
mode: "profile" | "query" | "full",
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
promptTemplate?: (data: MemoryPromptData) => string,
|
||||
): Promise<LanguageModelCallOptions> => {
|
||||
const { buildMemoriesText } = await import("../shared")
|
||||
|
||||
const queryText = extractQueryText(params, mode)
|
||||
|
||||
const memories = await buildMemoriesText({
|
||||
containerTag,
|
||||
queryText,
|
||||
mode,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
logger,
|
||||
promptTemplate,
|
||||
})
|
||||
|
||||
return injectMemoriesIntoParams(params, memories, logger)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,23 +3,17 @@ import {
|
|||
addConversation,
|
||||
type ConversationMessage,
|
||||
} from "../conversations-client"
|
||||
import {
|
||||
createLogger,
|
||||
normalizeBaseUrl,
|
||||
MemoryCache,
|
||||
buildMemoriesText,
|
||||
type Logger,
|
||||
type PromptTemplate,
|
||||
type MemoryMode,
|
||||
} from "../shared"
|
||||
import { createLogger, type Logger } from "./logger"
|
||||
import {
|
||||
type LanguageModelCallOptions,
|
||||
type LanguageModelStreamPart,
|
||||
type OutputContentItem,
|
||||
getLastUserMessage,
|
||||
filterOutSupermemories,
|
||||
} from "./util"
|
||||
import { extractQueryText, injectMemoriesIntoParams } from "./memory-prompt"
|
||||
import { addSystemPrompt, normalizeBaseUrl } from "./memory-prompt"
|
||||
|
||||
const getConversationContent = (params: LanguageModelCallOptions) => {
|
||||
export const getConversationContent = (params: LanguageModelCallOptions) => {
|
||||
return params.prompt
|
||||
.filter((msg) => msg.role !== "system" && msg.role !== "tool")
|
||||
.map((msg) => {
|
||||
|
|
@ -38,31 +32,31 @@ const getConversationContent = (params: LanguageModelCallOptions) => {
|
|||
.join("\n\n")
|
||||
}
|
||||
|
||||
const convertToConversationMessages = (
|
||||
export const convertToConversationMessages = (
|
||||
params: LanguageModelCallOptions,
|
||||
assistantResponseText: string,
|
||||
): ConversationMessage[] => {
|
||||
const messages: ConversationMessage[] = []
|
||||
|
||||
for (const msg of params.prompt) {
|
||||
if (msg.role === "system") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
if (msg.content) {
|
||||
const filteredContent = filterOutSupermemories(msg.content)
|
||||
if (filteredContent) {
|
||||
messages.push({
|
||||
role: msg.role as "user" | "assistant" | "tool",
|
||||
content: msg.content,
|
||||
role: msg.role as "user" | "assistant" | "system" | "tool",
|
||||
content: filteredContent,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const contentParts = msg.content
|
||||
.map((c) => {
|
||||
if (c.type === "text" && c.text) {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: c.text,
|
||||
if (c.type === "text") {
|
||||
const filteredText = filterOutSupermemories(c.text)
|
||||
if (filteredText) {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: filteredText,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
|
@ -81,7 +75,7 @@ const convertToConversationMessages = (
|
|||
|
||||
if (contentParts.length > 0) {
|
||||
messages.push({
|
||||
role: msg.role as "user" | "assistant" | "tool",
|
||||
role: msg.role as "user" | "assistant" | "system" | "tool",
|
||||
content: contentParts,
|
||||
})
|
||||
}
|
||||
|
|
@ -139,7 +133,7 @@ export const saveMemoryAfterResponse = async (
|
|||
? `${getConversationContent(params)} \n\n Assistant: ${assistantResponseText}`
|
||||
: `User: ${userMessage} \n\n Assistant: ${assistantResponseText}`
|
||||
|
||||
const response = await client.add({
|
||||
const response = await client.memories.add({
|
||||
content,
|
||||
containerTags: [containerTag],
|
||||
customId,
|
||||
|
|
@ -159,52 +153,25 @@ export const saveMemoryAfterResponse = async (
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for the Supermemory middleware.
|
||||
*/
|
||||
interface SupermemoryMiddlewareOptions {
|
||||
/** Container tag/identifier for memory search (e.g., user ID, project ID) */
|
||||
export interface SupermemoryMiddlewareOptions {
|
||||
containerTag: string
|
||||
/** Supermemory API key */
|
||||
apiKey: string
|
||||
/** Optional conversation ID to group messages for contextual memory generation */
|
||||
conversationId?: string
|
||||
/** Enable detailed logging of memory search and injection */
|
||||
verbose?: boolean
|
||||
/**
|
||||
* Memory retrieval mode:
|
||||
* - "profile": Retrieves user profile memories (static + dynamic) without query filtering
|
||||
* - "query": Searches memories based on semantic similarity to the user's message
|
||||
* - "full": Combines both profile and query-based results
|
||||
*/
|
||||
mode?: MemoryMode
|
||||
/**
|
||||
* Memory persistence mode:
|
||||
* - "always": Automatically save conversations as memories
|
||||
* - "never": Only retrieve memories, don't store new ones
|
||||
*/
|
||||
mode?: "profile" | "query" | "full"
|
||||
addMemory?: "always" | "never"
|
||||
/** Custom Supermemory API base URL */
|
||||
baseUrl?: string
|
||||
/** Custom function to format memory data into the system prompt */
|
||||
promptTemplate?: PromptTemplate
|
||||
}
|
||||
|
||||
interface SupermemoryMiddlewareContext {
|
||||
export interface SupermemoryMiddlewareContext {
|
||||
client: Supermemory
|
||||
logger: Logger
|
||||
containerTag: string
|
||||
conversationId?: string
|
||||
mode: MemoryMode
|
||||
mode: "profile" | "query" | "full"
|
||||
addMemory: "always" | "never"
|
||||
normalizedBaseUrl: string
|
||||
apiKey: string
|
||||
promptTemplate?: PromptTemplate
|
||||
/**
|
||||
* Per-turn memory cache. Stores the injected memories string for each
|
||||
* user turn (keyed by turnKey) to avoid redundant API calls during tool-call
|
||||
*/
|
||||
memoryCache: MemoryCache<string>
|
||||
}
|
||||
|
||||
export const createSupermemoryContext = (
|
||||
|
|
@ -218,7 +185,6 @@ export const createSupermemoryContext = (
|
|||
mode = "profile",
|
||||
addMemory = "never",
|
||||
baseUrl,
|
||||
promptTemplate,
|
||||
} = options
|
||||
|
||||
const logger = createLogger(verbose)
|
||||
|
|
@ -240,35 +206,9 @@ export const createSupermemoryContext = (
|
|||
addMemory,
|
||||
normalizedBaseUrl,
|
||||
apiKey,
|
||||
promptTemplate,
|
||||
memoryCache: new MemoryCache<string>(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cache key for the current turn based on context and user message.
|
||||
* Uses the shared MemoryCache.makeTurnKey implementation.
|
||||
*/
|
||||
const makeTurnKey = (
|
||||
ctx: SupermemoryMiddlewareContext,
|
||||
userMessage: string,
|
||||
): string => {
|
||||
return MemoryCache.makeTurnKey(
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
ctx.mode,
|
||||
userMessage,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is a new user turn (last message is from user)
|
||||
*/
|
||||
const isNewUserTurn = (params: LanguageModelCallOptions): boolean => {
|
||||
const lastMessage = params.prompt.at(-1)
|
||||
return lastMessage?.role === "user"
|
||||
}
|
||||
|
||||
export const transformParamsWithMemory = async (
|
||||
params: LanguageModelCallOptions,
|
||||
ctx: SupermemoryMiddlewareContext,
|
||||
|
|
@ -282,42 +222,20 @@ export const transformParamsWithMemory = async (
|
|||
}
|
||||
}
|
||||
|
||||
const turnKey = makeTurnKey(ctx, userMessage || "")
|
||||
const isNewTurn = isNewUserTurn(params)
|
||||
|
||||
// Check if we can use cached memories
|
||||
const cachedMemories = ctx.memoryCache.get(turnKey)
|
||||
if (!isNewTurn && cachedMemories) {
|
||||
ctx.logger.debug("Using cached memories: ", {
|
||||
turnKey,
|
||||
})
|
||||
return injectMemoriesIntoParams(params, cachedMemories, ctx.logger)
|
||||
}
|
||||
|
||||
ctx.logger.info("Starting memory search", {
|
||||
containerTag: ctx.containerTag,
|
||||
conversationId: ctx.conversationId,
|
||||
mode: ctx.mode,
|
||||
isNewTurn,
|
||||
cacheHit: false,
|
||||
})
|
||||
|
||||
const queryText = extractQueryText(params, ctx.mode)
|
||||
|
||||
const memories = await buildMemoriesText({
|
||||
containerTag: ctx.containerTag,
|
||||
queryText,
|
||||
mode: ctx.mode,
|
||||
baseUrl: ctx.normalizedBaseUrl,
|
||||
apiKey: ctx.apiKey,
|
||||
logger: ctx.logger,
|
||||
promptTemplate: ctx.promptTemplate,
|
||||
})
|
||||
|
||||
ctx.memoryCache.set(turnKey, memories)
|
||||
ctx.logger.debug("Cached memories for turn", { turnKey })
|
||||
|
||||
return injectMemoriesIntoParams(params, memories, ctx.logger)
|
||||
const transformedParams = await addSystemPrompt(
|
||||
params,
|
||||
ctx.containerTag,
|
||||
ctx.logger,
|
||||
ctx.mode,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
return transformedParams
|
||||
}
|
||||
|
||||
export const extractAssistantResponseText = (content: unknown[]): string => {
|
||||
|
|
@ -325,3 +243,47 @@ export const extractAssistantResponseText = (content: unknown[]): string => {
|
|||
.map((item) => (item.type === "text" ? item.text || "" : ""))
|
||||
.join("")
|
||||
}
|
||||
|
||||
export const createStreamTransform = (
|
||||
ctx: SupermemoryMiddlewareContext,
|
||||
params: LanguageModelCallOptions,
|
||||
): {
|
||||
transform: TransformStream<LanguageModelStreamPart, LanguageModelStreamPart>
|
||||
getGeneratedText: () => string
|
||||
} => {
|
||||
let generatedText = ""
|
||||
|
||||
const transform = new TransformStream<
|
||||
LanguageModelStreamPart,
|
||||
LanguageModelStreamPart
|
||||
>({
|
||||
transform(chunk, controller) {
|
||||
if (chunk.type === "text-delta") {
|
||||
generatedText += chunk.delta
|
||||
}
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
flush: async () => {
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (ctx.addMemory === "always" && userMessage && userMessage.trim()) {
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
generatedText,
|
||||
params,
|
||||
ctx.logger,
|
||||
ctx.apiKey,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
transform,
|
||||
getGeneratedText: () => generatedText,
|
||||
}
|
||||
}
|
||||
|
||||
export { createLogger, type Logger, type OutputContentItem }
|
||||
|
|
|
|||
|
|
@ -9,12 +9,6 @@ import type {
|
|||
LanguageModelV3StreamPart,
|
||||
} from "@ai-sdk/provider"
|
||||
|
||||
// Re-export shared types for backward compatibility
|
||||
export type {
|
||||
ProfileStructure,
|
||||
ProfileMarkdownData,
|
||||
} from "../shared"
|
||||
|
||||
// Union types for dual SDK version support (V2 = SDK 5, V3 = SDK 6)
|
||||
export type LanguageModel = LanguageModelV2 | LanguageModelV3
|
||||
export type LanguageModelCallOptions =
|
||||
|
|
@ -27,6 +21,26 @@ export type LanguageModelStreamPart =
|
|||
| LanguageModelV2StreamPart
|
||||
| LanguageModelV3StreamPart
|
||||
|
||||
export interface ProfileStructure {
|
||||
profile: {
|
||||
static?: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
dynamic?: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
}
|
||||
searchResults: {
|
||||
results: Array<{ memory: string; metadata?: Record<string, unknown> }>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProfileMarkdownData {
|
||||
profile: {
|
||||
static?: string[]
|
||||
dynamic?: string[]
|
||||
}
|
||||
searchResults: {
|
||||
results: Array<{ memory: string }>
|
||||
}
|
||||
}
|
||||
|
||||
export type OutputContentItem =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "reasoning"; text: string }
|
||||
|
|
@ -44,33 +58,37 @@ export type OutputContentItem =
|
|||
title: string
|
||||
}
|
||||
|
||||
// Re-export convertProfileToMarkdown from shared for backward compatibility
|
||||
export { convertProfileToMarkdown } from "../shared"
|
||||
/**
|
||||
* Convert profile data to markdown format
|
||||
* @param data Profile data with string arrays for static and dynamic memories
|
||||
* @returns Markdown string with profile sections
|
||||
*/
|
||||
export function convertProfileToMarkdown(data: ProfileMarkdownData): string {
|
||||
const sections: string[] = []
|
||||
|
||||
export const getLastUserMessage = (
|
||||
params: LanguageModelCallOptions,
|
||||
): string | undefined => {
|
||||
if (data.profile.static && data.profile.static.length > 0) {
|
||||
sections.push("## Static Profile")
|
||||
sections.push(data.profile.static.map((item) => `- ${item}`).join("\n"))
|
||||
}
|
||||
|
||||
if (data.profile.dynamic && data.profile.dynamic.length > 0) {
|
||||
sections.push("## Dynamic Profile")
|
||||
sections.push(data.profile.dynamic.map((item) => `- ${item}`).join("\n"))
|
||||
}
|
||||
|
||||
return sections.join("\n\n")
|
||||
}
|
||||
|
||||
export const getLastUserMessage = (params: LanguageModelCallOptions) => {
|
||||
const lastUserMessage = params.prompt
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((prompt: LanguageModelMessage) => prompt.role === "user")
|
||||
|
||||
if (!lastUserMessage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const content = lastUserMessage.content
|
||||
|
||||
// Handle string content directly
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
|
||||
// Handle array content - extract text parts
|
||||
return content
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => (part as { type: "text"; text: string }).text)
|
||||
const memories = lastUserMessage?.content
|
||||
.filter((content) => content.type === "text")
|
||||
.map((content) => (content as { type: "text"; text: string }).text)
|
||||
.join(" ")
|
||||
return memories
|
||||
}
|
||||
|
||||
export const filterOutSupermemories = (content: string) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue