fix(middleware): update deduplication logic for profile memories in query mode (#1243)

This commit is contained in:
Rajarshi Datta 2026-07-21 19:25:32 +05:30 committed by GitHub
parent 566be20898
commit 86c3ad69d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 168 additions and 6 deletions

View file

@ -1,7 +1,7 @@
import type OpenAI from "openai"
import Supermemory from "supermemory"
import { addConversation } from "../conversations-client"
import { deduplicateMemories } from "../tools-shared"
import { deduplicateMemoriesForMode } from "../tools-shared"
import { createLogger, type Logger } from "../vercel/logger"
import { convertProfileToMarkdown } from "../vercel/util"
@ -184,7 +184,7 @@ const addSystemPrompt = async (
mode,
})
const deduplicated = deduplicateMemories({
const deduplicated = deduplicateMemoriesForMode(mode, {
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults?.results,
@ -471,7 +471,7 @@ export function createOpenAIMiddleware(
mode,
})
const deduplicated = deduplicateMemories({
const deduplicated = deduplicateMemoriesForMode(mode, {
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults?.results,

View file

@ -0,0 +1,78 @@
import { afterEach, describe, expect, it, vi } from "vitest"
import { buildMemoriesText } from "./memory-client"
import { createLogger } from "./logger"
const API_KEY = "sm_test_key"
const BASE_URL = "https://api.supermemory.ai"
const CONTAINER_TAG = "user-123"
const logger = createLogger(false)
/** Stubs `/v4/profile` so the injected prompt can be asserted without network access. */
function mockProfileResponse(body: unknown) {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => body,
})
vi.stubGlobal("fetch", fetchMock)
return fetchMock
}
afterEach(() => {
vi.unstubAllGlobals()
})
describe("buildMemoriesText", () => {
// The profile is not injected in "query" mode. Deduplicating the search
// results against it would drop a fact present in both, leaving the model
// with nothing.
it("injects a search result that also exists in the profile in query mode", async () => {
mockProfileResponse({
profile: {
static: [{ memory: "User is allergic to peanuts" }],
dynamic: [],
},
searchResults: { results: [{ memory: "User is allergic to peanuts" }] },
})
const memories = await buildMemoriesText({
containerTag: CONTAINER_TAG,
queryText: "what should I avoid eating?",
mode: "query",
baseUrl: BASE_URL,
apiKey: API_KEY,
logger,
})
expect(memories).toContain("User is allergic to peanuts")
})
it("does not repeat a profile memory in the search results in full mode", async () => {
mockProfileResponse({
profile: {
static: [{ memory: "User is allergic to peanuts" }],
dynamic: [],
},
searchResults: {
results: [
{ memory: "User is allergic to peanuts" },
{ memory: "User prefers async/await" },
],
},
})
const memories = await buildMemoriesText({
containerTag: CONTAINER_TAG,
queryText: "what should I avoid eating?",
mode: "full",
baseUrl: BASE_URL,
apiKey: API_KEY,
logger,
})
expect(memories).toContain("## Static Profile")
expect(memories).toContain("User prefers async/await")
// Present once, under the profile — not duplicated into the search results.
expect(memories.match(/User is allergic to peanuts/g)).toHaveLength(1)
})
})

View file

@ -1,4 +1,4 @@
import { deduplicateMemories } from "../tools-shared"
import { deduplicateMemoriesForMode } from "../tools-shared"
import type {
Logger,
MemoryMode,
@ -119,7 +119,7 @@ export const buildMemoriesText = async (
mode,
})
const deduplicated = deduplicateMemories({
const deduplicated = deduplicateMemoriesForMode(mode, {
static: memoriesResponse.profile.static,
dynamic: memoriesResponse.profile.dynamic,
searchResults: memoriesResponse.searchResults?.results,

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"
import { getContainerTags } from "./tools-shared"
import { deduplicateMemoriesForMode, getContainerTags } from "./tools-shared"
describe("getContainerTags", () => {
it("uses the default project when no config is provided", () => {
@ -26,3 +26,59 @@ describe("getContainerTags", () => {
).toThrow("either projectId or containerTags")
})
})
describe("deduplicateMemoriesForMode", () => {
// The profile is not injected in "query" mode, so a memory that is both a
// profile fact and a search hit must survive in the search results —
// otherwise it is dropped from the prompt entirely.
it("keeps a search result that duplicates a profile memory in query mode", () => {
const deduplicated = deduplicateMemoriesForMode("query", {
static: [{ memory: "User is allergic to peanuts" }],
dynamic: [],
searchResults: [{ memory: "User is allergic to peanuts" }],
})
expect(deduplicated.searchResults).toEqual(["User is allergic to peanuts"])
expect(deduplicated.static).toEqual([])
expect(deduplicated.dynamic).toEqual([])
})
it("still deduplicates within the search results in query mode", () => {
const deduplicated = deduplicateMemoriesForMode("query", {
static: [],
dynamic: [],
searchResults: [
{ memory: "User likes TypeScript" },
"User likes TypeScript",
],
})
expect(deduplicated.searchResults).toEqual(["User likes TypeScript"])
})
it("deduplicates search results against the profile in full mode", () => {
const deduplicated = deduplicateMemoriesForMode("full", {
static: [{ memory: "User is allergic to peanuts" }],
dynamic: [{ memory: "User is shipping a release today" }],
searchResults: [
{ memory: "User is allergic to peanuts" },
{ memory: "User prefers async/await" },
],
})
expect(deduplicated.static).toEqual(["User is allergic to peanuts"])
expect(deduplicated.dynamic).toEqual(["User is shipping a release today"])
expect(deduplicated.searchResults).toEqual(["User prefers async/await"])
})
it("deduplicates search results against the profile in profile mode", () => {
const deduplicated = deduplicateMemoriesForMode("profile", {
static: [{ memory: "User is allergic to peanuts" }],
dynamic: [],
searchResults: [{ memory: "User is allergic to peanuts" }],
})
expect(deduplicated.static).toEqual(["User is allergic to peanuts"])
expect(deduplicated.searchResults).toEqual([])
})
})

View file

@ -2,6 +2,8 @@
* Shared constants and descriptions for Supermemory tools
*/
import type { MemoryMode } from "./shared/types"
// Tool descriptions
export const TOOL_DESCRIPTIONS = {
searchMemories:
@ -177,3 +179,29 @@ export function deduplicateMemories(
searchResults: searchMemories,
}
}
/**
* Deduplicates memory items against only the sources the given mode actually
* injects into the prompt.
*
* `"query"` mode injects the search results but not the profile, so search
* results must not be deduplicated against the profile: a memory present in
* both would be dropped as a duplicate of something the model never sees, and
* would disappear from the prompt entirely.
*
* @param mode - The memory retrieval mode
* @param data - Profile data with memory items from different sources
* @returns Deduplicated memory strings for each source
*/
export function deduplicateMemoriesForMode(
mode: MemoryMode,
data: ProfileWithMemories,
): DeduplicatedMemories {
const injectsProfile = mode !== "query"
return deduplicateMemories({
static: injectsProfile ? data.static : [],
dynamic: injectsProfile ? data.dynamic : [],
searchResults: data.searchResults,
})
}