mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-10 22:41:17 +00:00
updated ai sdk
This commit is contained in:
parent
1030375094
commit
7b99822ae3
6 changed files with 427 additions and 727 deletions
|
|
@ -32,10 +32,14 @@ Automatically inject user profiles into every LLM call for instant personalizati
|
|||
|
||||
```typescript
|
||||
import { generateText } from "ai"
|
||||
import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
||||
import { withSupermemory } from "@supermemory/tools/vercel"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
|
||||
const modelWithMemory = withSupermemory(openai("gpt-5"), "user-123")
|
||||
const modelWithMemory = withSupermemory({
|
||||
model: openai("gpt-4"),
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456"
|
||||
})
|
||||
|
||||
const result = await generateText({
|
||||
model: modelWithMemory,
|
||||
|
|
@ -44,11 +48,14 @@ const result = await generateText({
|
|||
```
|
||||
|
||||
<Note>
|
||||
**Memory saving is disabled by default.** The middleware only retrieves existing memories. To automatically save new memories:
|
||||
**Memory saving is enabled by default.** The middleware automatically saves conversations to memory. To disable memory saving:
|
||||
|
||||
```typescript
|
||||
const modelWithMemory = withSupermemory(openai("gpt-5"), "user-123", {
|
||||
addMemory: "always"
|
||||
const modelWithMemory = withSupermemory({
|
||||
model: openai("gpt-4"),
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
addMemory: "never"
|
||||
})
|
||||
```
|
||||
</Note>
|
||||
|
|
@ -58,19 +65,34 @@ const result = await generateText({
|
|||
**Profile Mode (Default)** - Retrieves the user's complete profile:
|
||||
|
||||
```typescript
|
||||
const model = withSupermemory(openai("gpt-4"), "user-123", { mode: "profile" })
|
||||
const model = withSupermemory({
|
||||
model: openai("gpt-4"),
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "profile"
|
||||
})
|
||||
```
|
||||
|
||||
**Query Mode** - Searches memories based on the user's message:
|
||||
|
||||
```typescript
|
||||
const model = withSupermemory(openai("gpt-4"), "user-123", { mode: "query" })
|
||||
const model = withSupermemory({
|
||||
model: openai("gpt-4"),
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "query"
|
||||
})
|
||||
```
|
||||
|
||||
**Full Mode** - Combines profile AND query-based search:
|
||||
|
||||
```typescript
|
||||
const model = withSupermemory(openai("gpt-4"), "user-123", { mode: "full" })
|
||||
const model = withSupermemory({
|
||||
model: openai("gpt-4"),
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "full"
|
||||
})
|
||||
```
|
||||
|
||||
### Custom Prompt Templates
|
||||
|
|
@ -78,7 +100,7 @@ const model = withSupermemory(openai("gpt-4"), "user-123", { mode: "full" })
|
|||
Customize how memories are formatted. The template receives `userMemories`, `generalSearchMemories`, and `searchResults` (raw array for filtering by metadata):
|
||||
|
||||
```typescript
|
||||
import { withSupermemory, type MemoryPromptData } from "@supermemory/tools/ai-sdk"
|
||||
import { withSupermemory, type MemoryPromptData } from "@supermemory/tools/vercel"
|
||||
|
||||
const claudePrompt = (data: MemoryPromptData) => `
|
||||
<context>
|
||||
|
|
@ -91,7 +113,10 @@ const claudePrompt = (data: MemoryPromptData) => `
|
|||
</context>
|
||||
`.trim()
|
||||
|
||||
const model = withSupermemory(anthropic("claude-3-sonnet"), "user-123", {
|
||||
const model = withSupermemory({
|
||||
model: anthropic("claude-3-sonnet"),
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
mode: "full",
|
||||
promptTemplate: claudePrompt
|
||||
})
|
||||
|
|
@ -100,7 +125,10 @@ const model = withSupermemory(anthropic("claude-3-sonnet"), "user-123", {
|
|||
### Verbose Logging
|
||||
|
||||
```typescript
|
||||
const model = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
const model = withSupermemory({
|
||||
model: openai("gpt-4"),
|
||||
containerTag: "user-123",
|
||||
customId: "conv-456",
|
||||
verbose: true
|
||||
})
|
||||
// Console output shows memory retrieval details
|
||||
|
|
|
|||
|
|
@ -10,14 +10,59 @@ import {
|
|||
extractAssistantResponseText,
|
||||
saveMemoryAfterResponse,
|
||||
} from "./middleware"
|
||||
import type { PromptTemplate, MemoryPromptData } from "./memory-prompt"
|
||||
|
||||
interface WrapVercelLanguageModelOptions {
|
||||
conversationId?: string
|
||||
interface WrapVercelLanguageModelOptions<T extends LanguageModel> {
|
||||
/** The language model to wrap with supermemory capabilities */
|
||||
model: T
|
||||
/** The container tag/identifier for memory search (e.g., user ID, project ID) */
|
||||
containerTag: string
|
||||
/** Custom ID to group messages into a single document. Required. */
|
||||
customId: 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"
|
||||
/**
|
||||
* Search mode for memory retrieval:
|
||||
* - "memories": Search only memory entries (default)
|
||||
* - "hybrid": Search both memories AND document chunks (recommended for RAG)
|
||||
* - "documents": Search only document chunks
|
||||
*/
|
||||
searchMode?: "memories" | "hybrid" | "documents"
|
||||
/** Maximum number of search results to return when using hybrid/documents mode (default: 10) */
|
||||
searchLimit?: number
|
||||
/**
|
||||
* Memory persistence mode:
|
||||
* - "always": Automatically save conversations as memories (default)
|
||||
* - "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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -31,13 +76,15 @@ interface WrapVercelLanguageModelOptions {
|
|||
* Supports both Vercel AI SDK 5 (LanguageModelV2) and SDK 6 (LanguageModelV3) via runtime
|
||||
* detection of `model.specificationVersion`.
|
||||
*
|
||||
* @param model - The language model to wrap with supermemory capabilities (V2 or V3)
|
||||
* @param containerTag - The container tag/identifier for memory search (e.g., user ID, project ID)
|
||||
* @param options - Optional configuration options for the middleware
|
||||
* @param options.conversationId - Optional conversation ID to group messages into a single document for contextual memory generation
|
||||
* @param options - Configuration object containing model and Supermemory options
|
||||
* @param options.model - The language model to wrap with supermemory capabilities (V2 or V3)
|
||||
* @param options.containerTag - Required. The container tag/identifier for memory search (e.g., user ID, project ID)
|
||||
* @param options.customId - Required. Custom ID to group messages into a single document
|
||||
* @param options.verbose - Optional flag to enable detailed logging of memory search and injection process (default: false)
|
||||
* @param options.mode - Optional mode for memory search: "profile", "query", or "full" (default: "profile")
|
||||
* @param options.addMemory - Optional mode for memory search: "always", "never" (default: "never")
|
||||
* @param options.searchMode - Optional search mode: "memories" (default), "hybrid" (memories + chunks), or "documents" (chunks only)
|
||||
* @param options.searchLimit - Optional maximum number of search results when using hybrid/documents mode (default: 10)
|
||||
* @param options.addMemory - Optional mode for memory persistence: "always" (default - saves conversations), "never" (read-only mode)
|
||||
* @param options.apiKey - Optional Supermemory API key to use instead of the environment variable
|
||||
* @param options.baseUrl - Optional base URL for the Supermemory API (default: "https://api.supermemory.ai")
|
||||
*
|
||||
|
|
@ -45,18 +92,32 @@ interface WrapVercelLanguageModelOptions {
|
|||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { withSupermemory } from "@supermemory/tools/ai-sdk"
|
||||
* import { withSupermemory } from "@supermemory/tools/vercel"
|
||||
* import { openai } from "@ai-sdk/openai"
|
||||
* import { generateText } from "ai"
|
||||
*
|
||||
* const modelWithMemory = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
* conversationId: "conversation-456",
|
||||
* // Basic usage with profile memories
|
||||
* const modelWithMemory = withSupermemory({
|
||||
* model: openai("gpt-4"),
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-456",
|
||||
* mode: "full",
|
||||
* addMemory: "always"
|
||||
* })
|
||||
*
|
||||
* // RAG usage with hybrid search (memories + document chunks)
|
||||
* const ragModel = withSupermemory({
|
||||
* model: openai("gpt-4"),
|
||||
* containerTag: "user-123",
|
||||
* customId: "conv-789",
|
||||
* mode: "full",
|
||||
* searchMode: "hybrid", // Search both memories and document chunks
|
||||
* searchLimit: 15,
|
||||
* })
|
||||
*
|
||||
* const result = await generateText({
|
||||
* model: modelWithMemory,
|
||||
* messages: [{ role: "user", content: "What's my favorite programming language?" }]
|
||||
* model: ragModel,
|
||||
* messages: [{ role: "user", content: "What's in my documents about quarterly goals?" }]
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
|
|
@ -64,11 +125,10 @@ interface WrapVercelLanguageModelOptions {
|
|||
* @throws {Error} When supermemory API request fails
|
||||
*/
|
||||
const wrapVercelLanguageModel = <T extends LanguageModel>(
|
||||
model: T,
|
||||
containerTag: string,
|
||||
options?: WrapVercelLanguageModelOptions,
|
||||
options: WrapVercelLanguageModelOptions<T>,
|
||||
): T => {
|
||||
const providedApiKey = options?.apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
const { model, containerTag, customId, ...restOptions } = options
|
||||
const providedApiKey = restOptions.apiKey ?? process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
if (!providedApiKey) {
|
||||
throw new Error(
|
||||
|
|
@ -79,11 +139,14 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag,
|
||||
apiKey: providedApiKey,
|
||||
conversationId: options?.conversationId,
|
||||
verbose: options?.verbose ?? false,
|
||||
mode: options?.mode ?? "profile",
|
||||
addMemory: options?.addMemory ?? "never",
|
||||
baseUrl: options?.baseUrl,
|
||||
customId,
|
||||
verbose: restOptions.verbose ?? false,
|
||||
mode: restOptions.mode ?? "profile",
|
||||
searchMode: restOptions.searchMode ?? "memories",
|
||||
searchLimit: restOptions.searchLimit ?? 10,
|
||||
addMemory: restOptions.addMemory ?? "always",
|
||||
baseUrl: restOptions.baseUrl,
|
||||
promptTemplate: restOptions.promptTemplate,
|
||||
})
|
||||
|
||||
const wrappedModel = {
|
||||
|
|
@ -97,14 +160,19 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
const result = await model.doGenerate(transformedParams as any)
|
||||
|
||||
const userMessage = getLastUserMessage(params)
|
||||
if (ctx.addMemory === "always" && userMessage && userMessage.trim()) {
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
ctx.customId &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
const assistantResponseText = extractAssistantResponseText(
|
||||
result.content as unknown[],
|
||||
)
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
ctx.customId,
|
||||
assistantResponseText,
|
||||
params,
|
||||
ctx.logger,
|
||||
|
|
@ -147,13 +215,14 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
const userMessage = getLastUserMessage(params)
|
||||
if (
|
||||
ctx.addMemory === "always" &&
|
||||
ctx.customId &&
|
||||
userMessage &&
|
||||
userMessage.trim()
|
||||
) {
|
||||
saveMemoryAfterResponse(
|
||||
ctx.client,
|
||||
ctx.containerTag,
|
||||
ctx.conversationId,
|
||||
ctx.customId,
|
||||
generatedText,
|
||||
params,
|
||||
ctx.logger,
|
||||
|
|
@ -183,4 +252,6 @@ const wrapVercelLanguageModel = <T extends LanguageModel>(
|
|||
export {
|
||||
wrapVercelLanguageModel as withSupermemory,
|
||||
type WrapVercelLanguageModelOptions as WithSupermemoryOptions,
|
||||
type PromptTemplate,
|
||||
type MemoryPromptData,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,17 +3,24 @@ import {
|
|||
addConversation,
|
||||
type ConversationMessage,
|
||||
} from "../conversations-client"
|
||||
import { createLogger, type Logger } from "./logger"
|
||||
import {
|
||||
createLogger,
|
||||
normalizeBaseUrl,
|
||||
MemoryCache,
|
||||
buildMemoriesText,
|
||||
type Logger,
|
||||
type PromptTemplate,
|
||||
type MemoryMode,
|
||||
type SearchMode,
|
||||
} from "../shared"
|
||||
import {
|
||||
type LanguageModelCallOptions,
|
||||
type LanguageModelStreamPart,
|
||||
type OutputContentItem,
|
||||
getLastUserMessage,
|
||||
filterOutSupermemories,
|
||||
} from "./util"
|
||||
import { addSystemPrompt, normalizeBaseUrl } from "./memory-prompt"
|
||||
import { extractQueryText, injectMemoriesIntoParams } from "./memory-prompt"
|
||||
|
||||
export const getConversationContent = (params: LanguageModelCallOptions) => {
|
||||
const _getConversationContent = (params: LanguageModelCallOptions) => {
|
||||
return params.prompt
|
||||
.filter((msg) => msg.role !== "system" && msg.role !== "tool")
|
||||
.map((msg) => {
|
||||
|
|
@ -32,31 +39,31 @@ export const getConversationContent = (params: LanguageModelCallOptions) => {
|
|||
.join("\n\n")
|
||||
}
|
||||
|
||||
export const convertToConversationMessages = (
|
||||
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") {
|
||||
const filteredContent = filterOutSupermemories(msg.content)
|
||||
if (filteredContent) {
|
||||
if (msg.content) {
|
||||
messages.push({
|
||||
role: msg.role as "user" | "assistant" | "system" | "tool",
|
||||
content: filteredContent,
|
||||
role: msg.role as "user" | "assistant" | "tool",
|
||||
content: msg.content,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const contentParts = msg.content
|
||||
.map((c) => {
|
||||
if (c.type === "text") {
|
||||
const filteredText = filterOutSupermemories(c.text)
|
||||
if (filteredText) {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: filteredText,
|
||||
}
|
||||
if (c.type === "text" && c.text) {
|
||||
return {
|
||||
type: "text" as const,
|
||||
text: c.text,
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
|
@ -75,7 +82,7 @@ export const convertToConversationMessages = (
|
|||
|
||||
if (contentParts.length > 0) {
|
||||
messages.push({
|
||||
role: msg.role as "user" | "assistant" | "system" | "tool",
|
||||
role: msg.role as "user" | "assistant" | "tool",
|
||||
content: contentParts,
|
||||
})
|
||||
}
|
||||
|
|
@ -93,58 +100,34 @@ export const convertToConversationMessages = (
|
|||
}
|
||||
|
||||
export const saveMemoryAfterResponse = async (
|
||||
client: Supermemory,
|
||||
_client: Supermemory,
|
||||
containerTag: string,
|
||||
conversationId: string | undefined,
|
||||
customId: string,
|
||||
assistantResponseText: string,
|
||||
params: LanguageModelCallOptions,
|
||||
logger: Logger,
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
): Promise<void> => {
|
||||
const customId = conversationId ? `conversation:${conversationId}` : undefined
|
||||
|
||||
try {
|
||||
if (customId && conversationId) {
|
||||
const conversationMessages = convertToConversationMessages(
|
||||
params,
|
||||
assistantResponseText,
|
||||
)
|
||||
const conversationMessages = convertToConversationMessages(
|
||||
params,
|
||||
assistantResponseText,
|
||||
)
|
||||
|
||||
const response = await addConversation({
|
||||
conversationId,
|
||||
messages: conversationMessages,
|
||||
containerTags: [containerTag],
|
||||
apiKey,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
logger.info("Conversation saved successfully via /v4/conversations", {
|
||||
containerTag,
|
||||
conversationId,
|
||||
messageCount: conversationMessages.length,
|
||||
responseId: response.id,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const userMessage = getLastUserMessage(params)
|
||||
const content = conversationId
|
||||
? `${getConversationContent(params)} \n\n Assistant: ${assistantResponseText}`
|
||||
: `User: ${userMessage} \n\n Assistant: ${assistantResponseText}`
|
||||
|
||||
const response = await client.memories.add({
|
||||
content,
|
||||
const response = await addConversation({
|
||||
conversationId: customId,
|
||||
messages: conversationMessages,
|
||||
containerTags: [containerTag],
|
||||
customId,
|
||||
apiKey,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
logger.info("Memory saved successfully via /v3/documents", {
|
||||
logger.info("Conversation saved successfully via /v4/conversations", {
|
||||
containerTag,
|
||||
customId,
|
||||
content,
|
||||
contentLength: content.length,
|
||||
memoryId: response.id,
|
||||
messageCount: conversationMessages.length,
|
||||
responseId: response.id,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error("Error saving memory", {
|
||||
|
|
@ -153,25 +136,63 @@ export const saveMemoryAfterResponse = async (
|
|||
}
|
||||
}
|
||||
|
||||
export interface SupermemoryMiddlewareOptions {
|
||||
/**
|
||||
* Configuration options for the Supermemory middleware.
|
||||
*/
|
||||
interface SupermemoryMiddlewareOptions {
|
||||
/** Container tag/identifier for memory search (e.g., user ID, project ID) */
|
||||
containerTag: string
|
||||
/** Supermemory API key */
|
||||
apiKey: string
|
||||
conversationId?: string
|
||||
/** Custom ID to group messages into a single document. Required. */
|
||||
customId: string
|
||||
/** Enable detailed logging of memory search and injection */
|
||||
verbose?: boolean
|
||||
mode?: "profile" | "query" | "full"
|
||||
/**
|
||||
* 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
|
||||
/**
|
||||
* Search mode for memory retrieval:
|
||||
* - "memories": Search only memory entries (default)
|
||||
* - "hybrid": Search both memories AND document chunks (recommended for RAG)
|
||||
* - "documents": Search only document chunks
|
||||
*/
|
||||
searchMode?: SearchMode
|
||||
/** Maximum number of search results to return (default: 10) */
|
||||
searchLimit?: number
|
||||
/**
|
||||
* Memory persistence mode:
|
||||
* - "always": Automatically save conversations as memories
|
||||
* - "never": Only retrieve memories, don't store new ones
|
||||
*/
|
||||
addMemory?: "always" | "never"
|
||||
/** Custom Supermemory API base URL */
|
||||
baseUrl?: string
|
||||
/** Custom function to format memory data into the system prompt */
|
||||
promptTemplate?: PromptTemplate
|
||||
}
|
||||
|
||||
export interface SupermemoryMiddlewareContext {
|
||||
interface SupermemoryMiddlewareContext {
|
||||
client: Supermemory
|
||||
logger: Logger
|
||||
containerTag: string
|
||||
conversationId?: string
|
||||
mode: "profile" | "query" | "full"
|
||||
customId: string
|
||||
mode: MemoryMode
|
||||
searchMode: SearchMode
|
||||
searchLimit: number
|
||||
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 = (
|
||||
|
|
@ -180,11 +201,14 @@ export const createSupermemoryContext = (
|
|||
const {
|
||||
containerTag,
|
||||
apiKey,
|
||||
conversationId,
|
||||
customId,
|
||||
verbose = false,
|
||||
mode = "profile",
|
||||
searchMode = "memories",
|
||||
searchLimit = 10,
|
||||
addMemory = "never",
|
||||
baseUrl,
|
||||
promptTemplate,
|
||||
} = options
|
||||
|
||||
const logger = createLogger(verbose)
|
||||
|
|
@ -201,14 +225,42 @@ export const createSupermemoryContext = (
|
|||
client,
|
||||
logger,
|
||||
containerTag,
|
||||
conversationId,
|
||||
customId,
|
||||
mode,
|
||||
searchMode,
|
||||
searchLimit,
|
||||
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.customId,
|
||||
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,
|
||||
|
|
@ -222,20 +274,45 @@ 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,
|
||||
customId: ctx.customId,
|
||||
mode: ctx.mode,
|
||||
searchMode: ctx.searchMode,
|
||||
isNewTurn,
|
||||
cacheHit: false,
|
||||
})
|
||||
|
||||
const transformedParams = await addSystemPrompt(
|
||||
params,
|
||||
ctx.containerTag,
|
||||
ctx.logger,
|
||||
ctx.mode,
|
||||
ctx.normalizedBaseUrl,
|
||||
)
|
||||
return transformedParams
|
||||
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,
|
||||
searchMode: ctx.searchMode,
|
||||
searchLimit: ctx.searchLimit,
|
||||
})
|
||||
|
||||
ctx.memoryCache.set(turnKey, memories)
|
||||
ctx.logger.debug("Cached memories for turn", { turnKey })
|
||||
|
||||
return injectMemoriesIntoParams(params, memories, ctx.logger)
|
||||
}
|
||||
|
||||
export const extractAssistantResponseText = (content: unknown[]): string => {
|
||||
|
|
@ -243,47 +320,3 @@ 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 }
|
||||
|
|
|
|||
|
|
@ -96,14 +96,13 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model, getCapturedGenerateParams } =
|
||||
createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -125,18 +124,16 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const conversationId = `test-generate-${Date.now()}`
|
||||
const customId = `test-generate-${Date.now()}`
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
conversationId,
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -166,21 +163,19 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
fetchSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should work with conversationId for grouped memories", async () => {
|
||||
it("should work with customId for grouped memories", async () => {
|
||||
const { model, getCapturedGenerateParams } =
|
||||
createIntegrationMockModel()
|
||||
|
||||
const conversationId = `test-conversation-${Date.now()}`
|
||||
const customId = `test-conversation-${Date.now()}`
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
conversationId,
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -203,14 +198,13 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
it("should fetch memories and stream response", async () => {
|
||||
const { model, getCapturedStreamParams } = createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-stream-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
const { stream } = await wrapped.doStream({
|
||||
prompt: [
|
||||
|
|
@ -240,18 +234,16 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const conversationId = `test-stream-${Date.now()}`
|
||||
const customId = `test-stream-${Date.now()}`
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
conversationId,
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
addMemory: "always",
|
||||
})
|
||||
|
||||
const { stream } = await wrapped.doStream({
|
||||
prompt: [
|
||||
|
|
@ -286,14 +278,13 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
it("should handle text-delta chunks correctly", async () => {
|
||||
const { model } = createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-chunks-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
const { stream } = await wrapped.doStream({
|
||||
prompt: [
|
||||
|
|
@ -327,14 +318,13 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-profile-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -368,14 +358,13 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "query",
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-query-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "query",
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -409,14 +398,13 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model } = createIntegrationMockModel()
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "full",
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-full-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "full",
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -456,15 +444,14 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
generalSearchMemories: string
|
||||
}) => `<custom-memories>${data.userMemories}</custom-memories>`
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
promptTemplate: customTemplate,
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-template-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
promptTemplate: customTemplate,
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -485,15 +472,14 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const { model, getCapturedGenerateParams } =
|
||||
createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
verbose: true,
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-verbose-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
verbose: true,
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -514,15 +500,14 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
const fetchSpy = vi.spyOn(globalThis, "fetch")
|
||||
|
||||
// Use the configured base URL (or default)
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-baseurl-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
baseUrl: INTEGRATION_CONFIG.baseUrl,
|
||||
})
|
||||
|
||||
await wrapped.doGenerate({
|
||||
prompt: [
|
||||
|
|
@ -556,14 +541,13 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
new Error("Model error"),
|
||||
)
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-error-${Date.now()}`,
|
||||
apiKey: INTEGRATION_CONFIG.apiKey,
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
await expect(
|
||||
wrapped.doGenerate({
|
||||
|
|
@ -580,14 +564,13 @@ describe.skipIf(!shouldRunIntegration)(
|
|||
it("should handle invalid API key gracefully", async () => {
|
||||
const { model } = createIntegrationMockModel()
|
||||
|
||||
const wrapped = withSupermemory(
|
||||
const wrapped = withSupermemory({
|
||||
model,
|
||||
INTEGRATION_CONFIG.containerTag,
|
||||
{
|
||||
apiKey: "invalid-api-key-12345",
|
||||
mode: "profile",
|
||||
},
|
||||
)
|
||||
containerTag: INTEGRATION_CONFIG.containerTag,
|
||||
customId: `test-invalid-key-${Date.now()}`,
|
||||
apiKey: "invalid-api-key-12345",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
await expect(
|
||||
wrapped.doGenerate({
|
||||
|
|
|
|||
|
|
@ -73,7 +73,11 @@ describe("Unit: withSupermemory", () => {
|
|||
const mockModel = createMockLanguageModel()
|
||||
|
||||
expect(() => {
|
||||
withSupermemory(mockModel, TEST_CONFIG.containerTag)
|
||||
withSupermemory({
|
||||
model: mockModel,
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "test-conv-123",
|
||||
})
|
||||
}).toThrow("SUPERMEMORY_API_KEY is not set")
|
||||
})
|
||||
|
||||
|
|
@ -81,7 +85,11 @@ describe("Unit: withSupermemory", () => {
|
|||
process.env.SUPERMEMORY_API_KEY = "test-key"
|
||||
|
||||
const mockModel = createMockLanguageModel()
|
||||
const wrappedModel = withSupermemory(mockModel, TEST_CONFIG.containerTag)
|
||||
const wrappedModel = withSupermemory({
|
||||
model: mockModel,
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
customId: "test-conv-456",
|
||||
})
|
||||
|
||||
expect(wrappedModel).toBeDefined()
|
||||
expect(wrappedModel.specificationVersion).toBe("v2")
|
||||
|
|
@ -125,6 +133,7 @@ describe("Unit: withSupermemory", () => {
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-cache-123",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -157,6 +166,7 @@ describe("Unit: withSupermemory", () => {
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-continuation-456",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -229,6 +239,7 @@ describe("Unit: withSupermemory", () => {
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-refetch-789",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -289,6 +300,7 @@ describe("Unit: withSupermemory", () => {
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-error-101",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
@ -310,6 +322,7 @@ describe("Unit: withSupermemory", () => {
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-empty-102",
|
||||
mode: "query",
|
||||
})
|
||||
|
||||
|
|
@ -327,6 +340,7 @@ describe("Unit: withSupermemory", () => {
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-empty-content-103",
|
||||
mode: "query",
|
||||
})
|
||||
|
||||
|
|
@ -354,6 +368,7 @@ describe("Unit: withSupermemory", () => {
|
|||
const ctx = createSupermemoryContext({
|
||||
containerTag: TEST_CONFIG.containerTag,
|
||||
apiKey: TEST_CONFIG.apiKey,
|
||||
customId: "test-mutate-104",
|
||||
mode: "profile",
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue