fix: deduplicate memories after returned to save tokens

This commit is contained in:
Dhravya Shah 2025-12-22 11:09:50 -08:00
parent fae0afa7da
commit 821d3049cd
5 changed files with 201 additions and 38 deletions

View file

@ -1,7 +1,7 @@
{
"name": "@supermemory/tools",
"type": "module",
"version": "1.3.52",
"version": "1.3.60",
"description": "Memory tools for AI SDK and OpenAI function calling with supermemory",
"scripts": {
"build": "tsdown",

View file

@ -1,6 +1,7 @@
import type OpenAI from "openai"
import Supermemory from "supermemory"
import { addConversation } from "../conversations-client"
import { deduplicateMemories } from "../shared"
import { createLogger, type Logger } from "../vercel/logger"
import { convertProfileToMarkdown } from "../vercel/util"
@ -180,26 +181,41 @@ const addSystemPrompt = async (
mode,
})
const deduplicated = deduplicateMemories({
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults.results,
})
logger.debug("Memory deduplication completed for chat API", {
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: memoriesResponse.profile.static?.map((item) => item.memory),
dynamic: memoriesResponse.profile.dynamic?.map(
(item) => item.memory,
),
},
searchResults: {
results: memoriesResponse.searchResults.results.map((item) => ({
memory: item.memory,
})) as [{ memory: string }],
static: deduplicated.static,
dynamic: deduplicated.dynamic,
},
searchResults: { results: [] },
})
: ""
const searchResultsMemories =
mode !== "profile"
? `Search results for user's recent message: \n${memoriesResponse.searchResults.results
.map((result) => `- ${result.memory}`)
? `Search results for user's recent message: \n${deduplicated.searchResults
.map((memory) => `- ${memory}`)
.join("\n")}`
: ""
@ -448,28 +464,41 @@ export function createOpenAIMiddleware(
mode,
})
const deduplicated = deduplicateMemories({
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults.results,
})
logger.debug(`Memory deduplication completed for ${context} API`, {
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: memoriesResponse.profile.static?.map(
(item) => item.memory,
),
dynamic: memoriesResponse.profile.dynamic?.map(
(item) => item.memory,
),
},
searchResults: {
results: memoriesResponse.searchResults.results.map((item) => ({
memory: item.memory,
})) as [{ memory: string }],
static: deduplicated.static,
dynamic: deduplicated.dynamic,
},
searchResults: { results: [] },
})
: ""
const searchResultsMemories =
mode !== "profile"
? `Search results for user's ${context === "chat" ? "recent message" : "input"}: \n${memoriesResponse.searchResults.results
.map((result) => `- ${result.memory}`)
? `Search results for user's ${context === "chat" ? "recent message" : "input"}: \n${deduplicated.searchResults
.map((memory) => `- ${memory}`)
.join("\n")}`
: ""

View file

@ -45,3 +45,102 @@ export function getContainerTags(config?: {
}
return config?.containerTags ?? CONTAINER_TAG_CONSTANTS.defaultTags
}
/**
* Memory item interface representing a single memory with optional metadata
*/
export interface MemoryItem {
memory: string
metadata?: Record<string, unknown>
}
/**
* Profile data structure containing memory items from different sources
*/
export interface ProfileWithMemories {
static?: Array<MemoryItem>
dynamic?: Array<MemoryItem>
searchResults?: Array<MemoryItem>
}
/**
* Deduplicated memory strings organized by source
*/
export interface DeduplicatedMemories {
static: string[]
dynamic: string[]
searchResults: string[]
}
/**
* Deduplicates memory items across static, dynamic, and search result sources.
* Priority: Static > Dynamic > Search Results
*
* @param data - Profile data with memory items from different sources
* @returns Deduplicated memory strings for each source
*
* @example
* ```typescript
* const deduplicated = deduplicateMemories({
* static: [{ memory: "User likes TypeScript" }],
* dynamic: [{ memory: "User likes TypeScript" }, { memory: "User works remotely" }],
* searchResults: [{ memory: "User prefers async/await" }]
* });
* // Returns:
* // {
* // static: ["User likes TypeScript"],
* // dynamic: ["User works remotely"],
* // searchResults: ["User prefers async/await"]
* // }
* ```
*/
export function deduplicateMemories(
data: ProfileWithMemories,
): DeduplicatedMemories {
const staticItems = data.static ?? []
const dynamicItems = data.dynamic ?? []
const searchItems = data.searchResults ?? []
const getMemoryString = (item: MemoryItem): string | null => {
if (!item || typeof item.memory !== "string") return null
const trimmed = item.memory.trim()
return trimmed.length > 0 ? trimmed : null
}
const staticMemories: string[] = []
const seenMemories = new Set<string>()
for (const item of staticItems) {
const memory = getMemoryString(item)
if (memory !== null) {
staticMemories.push(memory)
seenMemories.add(memory)
}
}
const dynamicMemories: string[] = []
for (const item of dynamicItems) {
const memory = getMemoryString(item)
if (memory !== null && !seenMemories.has(memory)) {
dynamicMemories.push(memory)
seenMemories.add(memory)
}
}
const searchMemories: string[] = []
for (const item of searchItems) {
const memory = getMemoryString(item)
if (memory !== null && !seenMemories.has(memory)) {
searchMemories.push(memory)
seenMemories.add(memory)
}
}
return {
static: staticMemories,
dynamic: dynamicMemories,
searchResults: searchMemories,
}
}

View file

@ -1,4 +1,5 @@
import type { LanguageModelV2CallOptions } from "@ai-sdk/provider"
import { deduplicateMemories } from "../shared"
import type { Logger } from "./logger"
import { convertProfileToMarkdown, type ProfileStructure } from "./util"
@ -88,12 +89,41 @@ export const addSystemPrompt = async (
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(memoriesResponse) : ""
mode !== "query"
? convertProfileToMarkdown({
profile: {
static: deduplicated.static,
dynamic: deduplicated.dynamic,
},
searchResults: { results: [] },
})
: ""
const searchResultsMemories =
mode !== "profile"
? `Search results for user's recent message: \n${memoriesResponse.searchResults.results
.map((result) => `- ${result.memory}`)
? `Search results for user's recent message: \n${deduplicated.searchResults
.map((memory) => `- ${memory}`)
.join("\n")}`
: ""

View file

@ -1,16 +1,22 @@
import type { LanguageModelV2CallOptions, LanguageModelV2Message } from "@ai-sdk/provider"
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: [
{
memory: string
},
]
results: Array<{ memory: string }>
}
}
@ -32,12 +38,11 @@ export type OutputContentItem =
}
/**
* Convert ProfileStructure to markdown
* based on profile.static and profile.dynamic properties
* @param data ProfileStructure
* @returns Markdown string
* 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: ProfileStructure): string {
export function convertProfileToMarkdown(data: ProfileMarkdownData): string {
const sections: string[] = []
if (data.profile.static && data.profile.static.length > 0) {