diff --git a/packages/tools/README.md b/packages/tools/README.md index 69856f69..0cdaa999 100644 --- a/packages/tools/README.md +++ b/packages/tools/README.md @@ -669,7 +669,9 @@ interface WithSupermemoryOptions { ## Available Tools -### Search Memories +`supermemoryTools()` returns all of the following. Each is also exported individually — `searchMemoriesTool`, `memoryForgetMatchingTool`, and so on for the AI SDK, and `createSearchMemoriesFunction` / `createMemoryForgetMatchingTool` and friends for OpenAI function calling. + +### Search Memories (`searchMemories`) Searches through stored memories based on a query string. **Parameters:** @@ -677,12 +679,51 @@ Searches through stored memories based on a query string. - `includeFullDocs` (boolean, optional): Whether to include full document content (default: true) - `limit` (number, optional): Maximum number of results (default: 10) -### Add Memory +### Add Memory (`addMemory`) Adds a new memory to the system. **Parameters:** - `memory` (string): The content to remember +### Get Profile (`getProfile`) +Returns the user's static (long-term) and dynamic (recent) memories, optionally with search results. + +**Parameters:** +- `containerTag` (string, optional): Defaults to the configured container tag +- `query` (string, optional): Also run a search and return the results + +### Get Profile Buckets (`getProfileBuckets`) +Returns profile memories grouped into topical buckets (`preferences`, `goals`, `work`, …) instead of the whole profile — useful for keeping prompts small when only one slice matters. + +**Parameters:** +- `containerTag` (string, optional): Defaults to the configured container tag +- `buckets` (string[], optional): Bucket keys to return. Omit for every bucket configured for the user + +### Document List / Add / Delete (`documentList`, `documentAdd`, `documentDelete`) +Browse, ingest, and delete source documents. `documentDelete` removes a document and its extracted memories. + +### Forget Memory (`memoryForget`) +Soft-deletes one memory by ID or exact content match. + +**Parameters:** +- `containerTag` (string, optional): Defaults to the configured container tag +- `memoryId` (string): The memory to forget — or… +- `memoryContent` (string): …its exact content +- `reason` (string, optional): Recorded on the forgotten memory + +### Forget Matching (`memoryForgetMatching`) +Forgets a whole topic in one call. Describe the target and matching memories are soft-deleted; pass explicit IDs to skip the search. + +**Parameters:** +- `containerTag` (string, optional): Defaults to the configured container tag +- `query` (string): What to forget, e.g. `"everything about Project Titan"` — or… +- `memoryIds` (string[]): …the exact memories to forget +- `dryRun` (boolean, optional): **Defaults to `true`** — the tool previews rather than deletes +- `maxForget` (number, optional): Cap on how many memories one call may forget (default: 100) +- `reason` (string, optional): Recorded on each forgotten memory + +The dry-run default is deliberate: a model gets a preview, not a deletion. The intended loop is preview → show the user the returned `memories` → call again with those `memoryIds` and `dryRun: false`. Applying with a `query` re-runs the semantic match, so it can select a different set than the preview showed. + ## Claude Memory Tool diff --git a/packages/tools/src/ai-sdk.ts b/packages/tools/src/ai-sdk.ts index f8d88154..ee64c94e 100644 --- a/packages/tools/src/ai-sdk.ts +++ b/packages/tools/src/ai-sdk.ts @@ -7,7 +7,11 @@ import { TOOL_DESCRIPTIONS, getContainerTags, } from "./tools-shared" -import { forgetMemoryRequest } from "./shared/forget-memory" +import { + forgetMatchingRequest, + forgetMemoryRequest, +} from "./shared/forget-memory" +import { profileBucketsRequest } from "./shared/profile-buckets" import type { SupermemoryToolsConfig } from "./types" // Export individual tool creators @@ -354,6 +358,130 @@ export const memoryForgetTool = ( }) } +export const memoryForgetMatchingTool = ( + apiKey: string, + config?: SupermemoryToolsConfig, +) => { + const containerTags = getContainerTags(config) + + return tool({ + description: TOOL_DESCRIPTIONS.memoryForgetMatching, + inputSchema: z.object({ + containerTag: z + .string() + .optional() + .describe(PARAMETER_DESCRIPTIONS.containerTag), + query: z.string().optional().describe(PARAMETER_DESCRIPTIONS.forgetQuery), + memoryIds: z + .array(z.string()) + .optional() + .describe(PARAMETER_DESCRIPTIONS.forgetMemoryIds), + dryRun: z + .boolean() + .optional() + .default(DEFAULT_VALUES.forgetDryRun) + .describe(PARAMETER_DESCRIPTIONS.forgetDryRun), + maxForget: z.coerce + .number() + .optional() + .default(DEFAULT_VALUES.forgetMaxForget) + .describe(PARAMETER_DESCRIPTIONS.forgetMaxForget), + reason: z.string().optional().describe(PARAMETER_DESCRIPTIONS.reason), + }), + execute: async ({ + containerTag, + query, + memoryIds, + dryRun = DEFAULT_VALUES.forgetDryRun, + maxForget = DEFAULT_VALUES.forgetMaxForget, + reason, + }) => { + try { + if (!query && !memoryIds?.length) { + return { + success: false, + error: "Either query or memoryIds must be provided", + } + } + + const tag = containerTag || containerTags[0] + + const result = await forgetMatchingRequest( + apiKey, + { + containerTag: tag as string, + dryRun, + maxForget, + ...(query && { query }), + ...(memoryIds?.length && { ids: memoryIds }), + ...(reason && { reason }), + }, + config?.baseUrl, + ) + + return { + success: true, + dryRun: result.dryRun, + count: result.count, + summary: result.summary, + memories: result.candidates ?? result.forgotten ?? [], + forgetBatchId: result.forgetBatchId, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } + }, + }) +} + +export const getProfileBucketsTool = ( + apiKey: string, + config?: SupermemoryToolsConfig, +) => { + const containerTags = getContainerTags(config) + + return tool({ + description: TOOL_DESCRIPTIONS.getProfileBuckets, + inputSchema: z.object({ + containerTag: z + .string() + .optional() + .describe(PARAMETER_DESCRIPTIONS.containerTag), + buckets: z + .array(z.string()) + .optional() + .describe(PARAMETER_DESCRIPTIONS.bucketKeys), + }), + execute: async ({ containerTag, buckets }) => { + try { + const tag = containerTag || containerTags[0] + + const result = await profileBucketsRequest( + apiKey, + { + containerTag: tag as string, + ...(buckets?.length && { buckets }), + }, + config?.baseUrl, + ) + + return { + success: true, + buckets: result.buckets, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } + }, + }) +} + /** * Create Supermemory tools for AI SDK */ @@ -365,10 +493,12 @@ export function supermemoryTools( searchMemories: searchMemoriesTool(apiKey, config), addMemory: addMemoryTool(apiKey, config), getProfile: getProfileTool(apiKey, config), + getProfileBuckets: getProfileBucketsTool(apiKey, config), documentList: documentListTool(apiKey, config), documentDelete: documentDeleteTool(apiKey, config), documentAdd: documentAddTool(apiKey, config), memoryForget: memoryForgetTool(apiKey, config), + memoryForgetMatching: memoryForgetMatchingTool(apiKey, config), } } diff --git a/packages/tools/src/openai/tools.ts b/packages/tools/src/openai/tools.ts index 4695c920..2acba372 100644 --- a/packages/tools/src/openai/tools.ts +++ b/packages/tools/src/openai/tools.ts @@ -6,7 +6,12 @@ import { TOOL_DESCRIPTIONS, getContainerTags, } from "../tools-shared" -import { forgetMemoryRequest } from "../shared/forget-memory" +import { + type ForgetMatchingMemory, + forgetMatchingRequest, + forgetMemoryRequest, +} from "../shared/forget-memory" +import { profileBucketsRequest } from "../shared/profile-buckets" import type { SupermemoryToolsConfig } from "../types" /** @@ -62,6 +67,22 @@ export interface MemoryForgetResult { error?: string } +export interface MemoryForgetMatchingResult { + success: boolean + dryRun?: boolean + count?: number + summary?: string + memories?: ForgetMatchingMemory[] + forgetBatchId?: string | null + error?: string +} + +export interface ProfileBucketsResult { + success: boolean + buckets?: Record + error?: string +} + /** * Function schemas for OpenAI function calling */ @@ -213,6 +234,64 @@ export const memoryToolSchemas = { required: [], }, } satisfies OpenAI.FunctionDefinition, + + memoryForgetMatching: { + name: "memoryForgetMatching", + description: TOOL_DESCRIPTIONS.memoryForgetMatching, + parameters: { + type: "object", + properties: { + containerTag: { + type: "string", + description: PARAMETER_DESCRIPTIONS.containerTag, + }, + query: { + type: "string", + description: PARAMETER_DESCRIPTIONS.forgetQuery, + }, + memoryIds: { + type: "array", + items: { type: "string" }, + description: PARAMETER_DESCRIPTIONS.forgetMemoryIds, + }, + dryRun: { + type: "boolean", + description: PARAMETER_DESCRIPTIONS.forgetDryRun, + default: DEFAULT_VALUES.forgetDryRun, + }, + maxForget: { + type: "number", + description: PARAMETER_DESCRIPTIONS.forgetMaxForget, + default: DEFAULT_VALUES.forgetMaxForget, + }, + reason: { + type: "string", + description: PARAMETER_DESCRIPTIONS.reason, + }, + }, + required: [], + }, + } satisfies OpenAI.FunctionDefinition, + + getProfileBuckets: { + name: "getProfileBuckets", + description: TOOL_DESCRIPTIONS.getProfileBuckets, + parameters: { + type: "object", + properties: { + containerTag: { + type: "string", + description: PARAMETER_DESCRIPTIONS.containerTag, + }, + buckets: { + type: "array", + items: { type: "string" }, + description: PARAMETER_DESCRIPTIONS.bucketKeys, + }, + }, + required: [], + }, + } satisfies OpenAI.FunctionDefinition, } as const /** @@ -511,6 +590,111 @@ export function createMemoryForgetFunction( } } +/** + * Mass forget function + */ +export function createMemoryForgetMatchingFunction( + apiKey: string, + config?: SupermemoryToolsConfig, +) { + const containerTags = getContainerTags(config) + + return async function memoryForgetMatching({ + containerTag, + query, + memoryIds, + dryRun = DEFAULT_VALUES.forgetDryRun, + maxForget = DEFAULT_VALUES.forgetMaxForget, + reason, + }: { + containerTag?: string + query?: string + memoryIds?: string[] + dryRun?: boolean + maxForget?: number + reason?: string + }): Promise { + try { + if (!query && !memoryIds?.length) { + return { + success: false, + error: "Either query or memoryIds must be provided", + } + } + + const tag = containerTag || containerTags[0] + + const result = await forgetMatchingRequest( + apiKey, + { + containerTag: tag as string, + dryRun, + maxForget, + ...(query && { query }), + ...(memoryIds?.length && { ids: memoryIds }), + ...(reason && { reason }), + }, + config?.baseUrl, + ) + + return { + success: true, + dryRun: result.dryRun, + count: result.count, + summary: result.summary, + memories: result.candidates ?? result.forgotten ?? [], + forgetBatchId: result.forgetBatchId, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } + } +} + +/** + * Get bucketed profile function + */ +export function createGetProfileBucketsFunction( + apiKey: string, + config?: SupermemoryToolsConfig, +) { + const containerTags = getContainerTags(config) + + return async function getProfileBuckets({ + containerTag, + buckets, + }: { + containerTag?: string + buckets?: string[] + }): Promise { + try { + const tag = containerTag || containerTags[0] + + const result = await profileBucketsRequest( + apiKey, + { + containerTag: tag as string, + ...(buckets?.length && { buckets }), + }, + config?.baseUrl, + ) + + return { + success: true, + buckets: result.buckets, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } + } +} + /** * Create all memory tools functions */ @@ -525,15 +709,22 @@ export function supermemoryTools( const documentDelete = createDocumentDeleteFunction(apiKey, config) const documentAdd = createDocumentAddFunction(apiKey, config) const memoryForget = createMemoryForgetFunction(apiKey, config) + const memoryForgetMatching = createMemoryForgetMatchingFunction( + apiKey, + config, + ) + const getProfileBuckets = createGetProfileBucketsFunction(apiKey, config) return { searchMemories, addMemory, getProfile, + getProfileBuckets, documentList, documentDelete, documentAdd, memoryForget, + memoryForgetMatching, } } @@ -549,6 +740,8 @@ export function getToolDefinitions(): OpenAI.Chat.Completions.ChatCompletionTool { type: "function", function: memoryToolSchemas.documentDelete }, { type: "function", function: memoryToolSchemas.documentAdd }, { type: "function", function: memoryToolSchemas.memoryForget }, + { type: "function", function: memoryToolSchemas.memoryForgetMatching }, + { type: "function", function: memoryToolSchemas.getProfileBuckets }, ] } @@ -582,6 +775,10 @@ export function createToolCallExecutor( return JSON.stringify(await tools.documentAdd(args)) case "memoryForget": return JSON.stringify(await tools.memoryForget(args)) + case "memoryForgetMatching": + return JSON.stringify(await tools.memoryForgetMatching(args)) + case "getProfileBuckets": + return JSON.stringify(await tools.getProfileBuckets(args)) default: return JSON.stringify({ success: false, @@ -725,3 +922,36 @@ export function createMemoryForgetTool( execute: memoryForget, } } + +export function createMemoryForgetMatchingTool( + apiKey: string, + config?: SupermemoryToolsConfig, +) { + const memoryForgetMatching = createMemoryForgetMatchingFunction( + apiKey, + config, + ) + + return { + definition: { + type: "function" as const, + function: memoryToolSchemas.memoryForgetMatching, + }, + execute: memoryForgetMatching, + } +} + +export function createGetProfileBucketsTool( + apiKey: string, + config?: SupermemoryToolsConfig, +) { + const getProfileBuckets = createGetProfileBucketsFunction(apiKey, config) + + return { + definition: { + type: "function" as const, + function: memoryToolSchemas.getProfileBuckets, + }, + execute: getProfileBuckets, + } +} diff --git a/packages/tools/src/shared/forget-memory.ts b/packages/tools/src/shared/forget-memory.ts index 50a3a529..6f7583f8 100644 --- a/packages/tools/src/shared/forget-memory.ts +++ b/packages/tools/src/shared/forget-memory.ts @@ -36,3 +36,68 @@ export async function forgetMemoryRequest( ) } } + +export interface ForgetMatchingParams { + containerTag: string + /** Topic or instruction to forget. Provide either this or `ids`. */ + query?: string + /** Exact memory ids to forget, skipping the semantic search. Max 500. */ + ids?: string[] + /** Preview without mutating. */ + dryRun?: boolean + /** Similarity floor for candidates, 0-1. Lower casts a wider net. */ + threshold?: number + /** Safety cap on how many memories a query-mode call may forget, 1-500. */ + maxForget?: number + reason?: string +} + +export interface ForgetMatchingMemory { + id: string + memory: string + score: number +} + +export interface ForgetMatchingResponse { + dryRun: boolean + count: number + /** Tagged on every memory forgotten in this call. Null on a dry run. */ + forgetBatchId: string | null + summary: string + /** Present on a dry run: what would be forgotten. */ + candidates?: ForgetMatchingMemory[] + /** Present on apply: what was forgotten. */ + forgotten?: ForgetMatchingMemory[] +} + +/** + * Mass-forgets memories via `POST /v4/memories/forget-matching`. + * + * With `query`, the service semantically searches the container and an LLM + * picks the memories genuinely about the target; with `ids`, it forgets exactly + * those. Applying a `query` re-runs the match, so to delete precisely what a + * dry run showed, pass that preview's ids back as `ids`. + */ +export async function forgetMatchingRequest( + apiKey: string, + params: ForgetMatchingParams, + baseUrl: string = DEFAULT_BASE_URL, +): Promise { + const response = await fetch(`${baseUrl}/v4/memories/forget-matching`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(params), + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => "Unknown error") + throw new Error( + `Supermemory forget matching failed: ${response.status} ${response.statusText}. ${errorText}`, + ) + } + + return await response.json() +} diff --git a/packages/tools/src/shared/profile-buckets.ts b/packages/tools/src/shared/profile-buckets.ts new file mode 100644 index 00000000..ee38dcee --- /dev/null +++ b/packages/tools/src/shared/profile-buckets.ts @@ -0,0 +1,52 @@ +const DEFAULT_BASE_URL = "https://api.supermemory.ai" + +export interface ProfileBucketsParams { + containerTag: string + /** Bucket keys to return. Omit for every bucket configured for the tag. */ + buckets?: string[] +} + +export interface ProfileBucketsResponse { + /** Memory lists keyed by bucket key. */ + buckets: Record +} + +/** + * Reads bucket-organized profile memories via `POST /v4/profile` with + * `include: ["buckets"]`, which skips the static and dynamic sections entirely. + * + * The supermemory SDK's `profile()` params don't cover `include`/`buckets`, so + * the endpoint is called directly — the same pattern the middleware already + * uses for `/v4/profile`. + */ +export async function profileBucketsRequest( + apiKey: string, + params: ProfileBucketsParams, + baseUrl: string = DEFAULT_BASE_URL, +): Promise { + const response = await fetch(`${baseUrl}/v4/profile`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + containerTag: params.containerTag, + include: ["buckets"], + ...(params.buckets?.length ? { buckets: params.buckets } : {}), + }), + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => "Unknown error") + throw new Error( + `Supermemory profile buckets failed: ${response.status} ${response.statusText}. ${errorText}`, + ) + } + + const body = (await response.json()) as { + profile?: { buckets?: Record } + } + + return { buckets: body.profile?.buckets ?? {} } +} diff --git a/packages/tools/src/tool-operations.test.ts b/packages/tools/src/tool-operations.test.ts index 69f12594..cec45452 100644 --- a/packages/tools/src/tool-operations.test.ts +++ b/packages/tools/src/tool-operations.test.ts @@ -158,6 +158,149 @@ describe("memoryForget", () => { }) }) +describe("memoryForgetMatching", () => { + function stubFetch( + body: unknown = { + dryRun: true, + count: 1, + forgetBatchId: null, + summary: "Selected 1 memory about Project Titan", + candidates: [{ id: "mem_1", memory: "Titan ships in Q3", score: 0.7 }], + }, + ) { + const fetchMock = vi + .fn() + .mockResolvedValue(new Response(JSON.stringify(body), { status: 200 })) + vi.stubGlobal("fetch", fetchMock) + return fetchMock + } + + it("previews by default rather than deleting", async () => { + const fetchMock = stubFetch() + const tool = aiSdk.memoryForgetMatchingTool(API_KEY, { + containerTags: ["user_1"], + }) + + const result = (await executeTool(tool, { + query: "everything about Project Titan", + })) as { success: boolean; dryRun: boolean; memories: unknown[] } + + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit] + expect(url).toBe("https://api.supermemory.ai/v4/memories/forget-matching") + expect(JSON.parse(init.body as string)).toEqual({ + containerTag: "user_1", + dryRun: true, + maxForget: 100, + query: "everything about Project Titan", + }) + expect(result.success).toBe(true) + expect(result.dryRun).toBe(true) + expect(result.memories).toHaveLength(1) + }) + + it("applies an explicit id list when dryRun is turned off", async () => { + const fetchMock = stubFetch({ + dryRun: false, + count: 2, + forgetBatchId: "batch_1", + summary: "Forgot 2 memories", + forgotten: [ + { id: "mem_1", memory: "a", score: 1 }, + { id: "mem_2", memory: "b", score: 1 }, + ], + }) + const memoryForgetMatching = + openAi.createMemoryForgetMatchingFunction(API_KEY) + + const result = await memoryForgetMatching({ + containerTag: "user_3", + memoryIds: ["mem_1", "mem_2"], + dryRun: false, + reason: "project cancelled", + }) + + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit] + expect(JSON.parse(init.body as string)).toMatchObject({ + containerTag: "user_3", + ids: ["mem_1", "mem_2"], + dryRun: false, + reason: "project cancelled", + }) + expect(result.count).toBe(2) + expect(result.forgetBatchId).toBe("batch_1") + }) + + it("requires a query or ids", async () => { + const fetchMock = stubFetch() + const memoryForgetMatching = + openAi.createMemoryForgetMatchingFunction(API_KEY) + + const result = await memoryForgetMatching({ containerTag: "user_1" }) + + expect(result.success).toBe(false) + expect(fetchMock).not.toHaveBeenCalled() + }) +}) + +describe("getProfileBuckets", () => { + function stubFetch( + body: unknown = { + profile: { buckets: { preferences: ["Likes dark mode"] } }, + }, + ) { + const fetchMock = vi + .fn() + .mockResolvedValue(new Response(JSON.stringify(body), { status: 200 })) + vi.stubGlobal("fetch", fetchMock) + return fetchMock + } + + it("requests only the bucket section of the profile", async () => { + const fetchMock = stubFetch() + const tool = aiSdk.getProfileBucketsTool(API_KEY, { + containerTags: ["user_1"], + }) + + const result = (await executeTool(tool, { + buckets: ["preferences"], + })) as { success: boolean; buckets: Record } + + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit] + expect(url).toBe("https://api.supermemory.ai/v4/profile") + expect(JSON.parse(init.body as string)).toEqual({ + containerTag: "user_1", + include: ["buckets"], + buckets: ["preferences"], + }) + expect(result.buckets).toEqual({ preferences: ["Likes dark mode"] }) + }) + + it("omits the bucket filter to return every configured bucket", async () => { + const fetchMock = stubFetch() + const getProfileBuckets = openAi.createGetProfileBucketsFunction(API_KEY, { + containerTags: ["user_2"], + }) + + await getProfileBuckets({}) + + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit] + expect(JSON.parse(init.body as string)).toEqual({ + containerTag: "user_2", + include: ["buckets"], + }) + }) + + it("returns an empty bucket map when the profile has none", async () => { + stubFetch({ profile: {} }) + const getProfileBuckets = openAi.createGetProfileBucketsFunction(API_KEY) + + const result = await getProfileBuckets({ containerTag: "user_1" }) + + expect(result.success).toBe(true) + expect(result.buckets).toEqual({}) + }) +}) + describe("ClaudeMemoryTool", () => { const FILE_PATH = "/memories/prefs.txt" const CUSTOM_ID = "memories_prefs_txt" diff --git a/packages/tools/src/tools-shared.ts b/packages/tools/src/tools-shared.ts index 80ba33a6..777a8788 100644 --- a/packages/tools/src/tools-shared.ts +++ b/packages/tools/src/tools-shared.ts @@ -20,6 +20,10 @@ export const TOOL_DESCRIPTIONS = { "Add a new document (URL, text, or content) to memory. The content is queued for processing, and memories will be extracted automatically.", memoryForget: "Forget (soft delete) a specific memory by ID or content match. The memory is marked as forgotten but not permanently deleted. Use when user wants to remove specific information from their profile.", + memoryForgetMatching: + "Forget a whole topic at once — describe what to forget ('everything about Project Titan') and matching memories are soft deleted. Runs as a preview by default: review the returned candidates with the user, then call again with their ids and dryRun false to apply. Use when the user wants a subject removed rather than one specific memory.", + getProfileBuckets: + "Get profile memories grouped into topical buckets (e.g. preferences, goals, work) instead of the full profile. Use when only one slice of context is relevant, to keep the prompt small. Omit bucket keys to see every bucket configured for this user.", } as const // Parameter descriptions @@ -41,6 +45,16 @@ export const PARAMETER_DESCRIPTIONS = { memoryContent: "Exact content match of the memory entry to operate on (alternative to ID)", reason: "Optional reason for forgetting this memory", + forgetQuery: + "What to forget, as a topic or instruction (e.g. 'everything about Project Titan'). Leave empty when passing explicit memoryIds.", + forgetMemoryIds: + "Exact memory ids to forget, skipping the search. Use the ids from a preview to apply exactly what was reviewed.", + forgetDryRun: + "Preview without deleting anything. Defaults to true — only set false once the user has confirmed the specific memories to forget.", + forgetMaxForget: + "Maximum number of memories this call may forget (1-500). Lower it when the topic is broad.", + bucketKeys: + "Bucket keys to return (e.g. ['preferences', 'goals']). Omit to return every bucket configured for this user.", } as const // Default values @@ -48,6 +62,9 @@ export const DEFAULT_VALUES = { includeFullDocs: true, limit: 10, chunkThreshold: 0.6, + /** Mass forget previews by default so a model can't delete in one shot. */ + forgetDryRun: true, + forgetMaxForget: 100, } as const // Container tag constants diff --git a/skills/supermemory/README.md b/skills/supermemory/README.md index 03cac7b4..1084850f 100644 --- a/skills/supermemory/README.md +++ b/skills/supermemory/README.md @@ -114,18 +114,23 @@ Ready-to-use code snippets for TypeScript and Python showing the basic workflow: - Store new memories ### 2. Complete SDK Documentation -Full reference for all SDK methods: -- `add()` - Store memories +Full reference for all SDK methods, plus which endpoints have no SDK method yet and need a plain HTTP call: +- `add()` - Ingest content - `profile()` - Retrieve user context -- `search.memories()` - Semantic search -- `documents.list()` - List documents -- `documents.delete()` - Delete documents +- `search()` - Semantic search (`search.memories()` in Python) +- `memories.forget()` / `memories.updateMemory()` - Forget and correct +- `documents.list()` / `.get()` / `.update()` / `.delete()` - Manage documents +- `settings.get()` / `.update()` - Org settings, including profile buckets ### 3. REST API Reference -Complete endpoint documentation with cURL examples: -- `POST /v3/documents` - Add documents -- `POST /v3/search` - Search memories -- `POST /v4/memories` - Create direct memories +Complete endpoint documentation with cURL examples, covering the full public surface: +- `POST /v3/documents` - Ingest documents +- `POST /v4/conversations` - Ingest chat transcripts +- `POST /v4/memories` - Write memories directly +- `POST /v4/search` - Search memories +- `POST /v4/profile` and `/v4/profile/buckets` - Profiles and topical buckets +- `PATCH|DELETE /v4/memories` and `POST /v4/memories/forget-matching` - Correct, forget, mass forget +- `/v3/container-tags/*`, `/v3/settings/*` - Spaces and configuration ### 4. Architecture Deep Dive Understand how Supermemory works: diff --git a/skills/supermemory/SKILL.md b/skills/supermemory/SKILL.md index 35c12ae9..431560e8 100644 --- a/skills/supermemory/SKILL.md +++ b/skills/supermemory/SKILL.md @@ -1,6 +1,6 @@ --- name: supermemory -description: Supermemory is a state-of-the-art memory and context infrastructure for AI agents. Use this skill when building applications that need persistent memory, user personalization, long-term context retention, or semantic search across knowledge bases. It provides Memory API for learned user context, User Profiles for static/dynamic facts, and RAG for semantic search. Perfect for chatbots, assistants, and knowledge-intensive applications. +description: Supermemory is a state-of-the-art memory and context infrastructure for AI agents. Use this skill when building applications that need persistent memory, user personalization, long-term context retention, or semantic search across knowledge bases. It provides a Memory API for learned user context, User Profiles for static/dynamic/bucketed facts, forgetting and correction for memory hygiene, and RAG for semantic search. Perfect for chatbots, assistants, and knowledge-intensive applications. --- # Supermemory: Memory Infrastructure for AI Agents @@ -15,9 +15,10 @@ Proactively suggest Supermemory when the user's application needs: - **Personalized AI experiences**: Build agents that truly understand individual users - **Knowledge-intensive applications**: Semantic search across documents, PDFs, images, videos - **Long-term context retention**: Maintain growing knowledge graphs that evolve over time -- **User profiles**: Combine static facts (name, preferences) with dynamic context (recent activity) +- **User profiles**: Combine static facts (name, preferences) with dynamic context (recent activity) and topical buckets +- **Memory hygiene**: Correct facts that changed, and forget one memory or a whole topic on request -## Three Core Capabilities +## Four Core Capabilities ### 1. Memory API - Learned User Context Creates extracted facts from conversations that update over time. The system automatically: @@ -26,35 +27,43 @@ Creates extracted facts from conversations that update over time. The system aut - Generates dynamic user profiles - Maintains relationships between memories -### 2. User Profiles - Static + Dynamic Facts -Combines always-known information (name, role, preferences) with episodic data from recent interactions. Perfect for personalizing responses. +### 2. User Profiles - Static, Dynamic, and Bucketed Facts +Combines always-known information (name, role, preferences) with episodic data from recent interactions — no query required, so it's the cheapest way to personalize a prompt. **Buckets** add a third axis: custom topical categories (`preferences`, `goals`, `work`) that a classifier assigns at ingestion, so a surface can pull just the slice of context it needs. ### 3. RAG - Advanced Semantic Search Provides semantic search with: - Metadata filtering and contextual chunking - Multi-modal support (text, PDFs, images, videos, URLs) -- Intelligent relevance thresholds +- Intelligent relevance thresholds, optional re-ranking and query rewriting - Graph-based relationships between documents +### 4. Forgetting and Correction - Memory Hygiene +Memories are versioned and soft-deleted, so the store can be corrected rather than just appended to: +- Update a fact and the new version supersedes the old one, history intact +- Forget a single memory by ID or exact content +- **Mass forget**: give a topic ("forget everything about Project Titan") and an agent selects the matching memories and soft-deletes them — with a dry-run preview and a safety cap + ## Quick Integration Examples -### TypeScript (Vercel AI SDK) +### TypeScript ```typescript -import { Supermemory } from 'supermemory'; +import Supermemory from 'supermemory'; const client = new Supermemory({ apiKey: process.env.SUPERMEMORY_API_KEY }); // 1. Retrieve personalized context -const context = await client.profile({ +const { profile } = await client.profile({ containerTag: "user_123", - query: "What are my preferences?" + q: "What are my preferences?" }); // 2. Enrich your prompt with context -const systemMessage = `User Profile: ${context.profile} -Relevant Memories: ${context.memories.join('\n')}`; +const systemMessage = [ + `Long-term facts:\n${(profile.static ?? []).map(f => `- ${f}`).join('\n')}`, + `Recent context:\n${(profile.dynamic ?? []).map(f => `- ${f}`).join('\n')}` +].join('\n\n'); // 3. Store new memories after conversation await client.add({ @@ -71,19 +80,36 @@ from supermemory import Supermemory client = Supermemory(api_key=os.environ["SUPERMEMORY_API_KEY"]) # Retrieve context -context = client.profile( +response = client.profile( container_tag="user_123", - query="What are my preferences?" + q="What are my preferences?", ) +print(response.profile.static, response.profile.dynamic) # Add memories client.add( content=conversation_text, container_tag="user_123", - metadata={"type": "conversation"} + metadata={"type": "conversation"}, ) ``` +### Forgetting + +```typescript +// One memory +await client.memories.forget({ containerTag: "user_123", id: "mem_abc123" }); + +// A whole topic — preview first, then apply exactly what you reviewed +const preview = await fetch("https://api.supermemory.ai/v4/memories/forget-matching", { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify({ containerTag: "user_123", query: "Project Titan", dryRun: true }) +}).then(r => r.json()); +``` + +Mass forget has no SDK method yet — call it over HTTP. See `references/sdk-guide.md` for the full list of HTTP-only endpoints. + ## Key Value Propositions 1. **Zero-boilerplate personalization**: Just a few lines of code to add persistent memory @@ -117,14 +143,18 @@ See `references/quickstart.md` for complete setup instructions. ## Integration Patterns -**For Chatbots**: Use `profile()` before each response to get user context, then `add()` after conversations +**For Chatbots**: Use `profile()` before each response to get user context, then `add()` after conversations — or `POST /v4/conversations` to send the structured transcript and keep roles, tool calls, and images intact -**For Knowledge Bases (RAG)**: Use `add()` for ingestion, then `search.memories({ q, searchMode: "hybrid" })` for retrieval with combined semantic + keyword search +**For Knowledge Bases (RAG)**: Use `add()` for ingestion, then `client.search({ q, searchMode: "hybrid" })` for retrieval with combined memory + chunk results **For Task Assistants**: Combine user profiles with document search for context-aware task completion **For Customer Support**: Index documentation and tickets, retrieve relevant knowledge per customer +**For "forget what I told you"**: Expose forgetting as a tool. Single memory → `memories.forget()`; a whole topic → `forget-matching` with `dryRun: true`, show the user what matched, then apply with the returned `ids` + +**For agent frameworks**: Use `@supermemory/tools` instead of hand-rolling calls — ready-made tools and memory-injecting middleware for Vercel AI SDK, OpenAI, Mastra, and VoltAgent + ## Reference Documentation - **Quickstart Guide**: `references/quickstart.md` - Complete setup walkthrough @@ -135,11 +165,12 @@ See `references/quickstart.md` for complete setup instructions. ## Best Practices -1. **Container Tags**: Use consistent user/project IDs as containerTags for proper isolation -2. **Metadata**: Add custom metadata for advanced filtering (source, type, timestamp) -3. **Thresholds**: Start with `threshold: 0.3` for balanced precision/recall -4. **Static Memories**: Mark permanent facts as `isStatic: true` for better performance +1. **Container Tags**: Use consistent user/project IDs as containerTags for proper isolation. If one entity ends up split across two tags, merge them rather than re-ingesting +2. **Metadata**: Add custom metadata for advanced filtering (source, type, timestamp). Filter conditions must be wrapped in `AND`/`OR` arrays +3. **Thresholds**: 0.3–0.5 favours recall, 0.5–0.7 is balanced, 0.7+ favours precision. Tune against real queries +4. **Static Memories**: Mark genuine identity traits as `isStatic: true` (name, hometown, profession) — not things that should be allowed to age 5. **Batch Operations**: Use bulk endpoints for multiple documents +6. **Destructive Operations**: Always `dryRun` a mass forget first, and apply with the `ids` from the preview so the delete is bound to exactly what was reviewed ## Integration Ecosystem diff --git a/skills/supermemory/references/api-reference.md b/skills/supermemory/references/api-reference.md index 791c303d..689018fa 100644 --- a/skills/supermemory/references/api-reference.md +++ b/skills/supermemory/references/api-reference.md @@ -1,6 +1,6 @@ # Supermemory API Reference -Complete REST API documentation for Supermemory. +Complete reference for the Supermemory REST API. ## Base URL @@ -10,582 +10,548 @@ https://api.supermemory.ai ## Authentication -All requests require authentication via Bearer token in the Authorization header: +All requests require a bearer token: -```http -Authorization: Bearer YOUR_API_KEY -``` - -Get your API key at [console.supermemory.ai](https://console.supermemory.ai). - -## Endpoints - -### POST /v3/documents - -Add a document for processing and memory extraction. - -**Endpoint:** -``` -POST https://api.supermemory.ai/v3/documents -``` - -**Headers:** ```http Authorization: Bearer YOUR_API_KEY Content-Type: application/json ``` -**Request Body:** +Get a key from [console.supermemory.ai](https://console.supermemory.ai). Keys are either **full-access** or **scoped** to specific container tags / projects — scoped keys can read anything in their scope but are rejected (`403`) by org-level write endpoints like `PATCH /v3/settings`. -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `content` | string | Yes | The content to process. Can be a URL, text, PDF path, image, or video | -| `containerTag` | string | No | Identifier for organizing documents (max 100 chars, alphanumeric with hyphens/underscores) | -| `entityContext` | string | No | Context guidance for memory extraction (max 1500 chars) | -| `customId` | string | No | Your custom identifier (max 100 chars, alphanumeric with hyphens/underscores) | -| `metadata` | object | No | Custom key-value pairs (strings, numbers, booleans, or string arrays) | +## Endpoint coverage -**Example Request:** +Every endpoint below is callable over plain HTTP. Some also have a generated SDK method — the rest need `fetch`/`requests`. Coverage verified against `supermemory@4.25.4` (npm) and `supermemory==3.56.0` (PyPI); check for newer releases before assuming a method is still missing. -```bash -curl -X POST https://api.supermemory.ai/v3/documents \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "content": "https://example.com/article", - "containerTag": "user_123", - "entityContext": "Technical blog post about API design", - "metadata": { - "source": "blog", - "category": "technical", - "tags": ["api", "design"] - } - }' +| Endpoint | SDK method | Purpose | +|---|---|---| +| `POST /v3/documents` | `client.documents.add()` | Ingest text, URL, or file reference | +| `POST /v3/documents/batch` | `client.documents.batchAdd()` | Ingest up to many documents in one call | +| `POST /v3/documents/file` | `client.documents.uploadFile()` | Multipart file upload | +| `POST /v4/conversations` | — HTTP only | Ingest structured chat transcripts | +| `POST /v4/memories` | — HTTP only | Write memories directly, skipping extraction | +| `POST /v4/search` | `client.search()` | Memory search (default; low latency) | +| `POST /v3/search` | `client.search.documents()` *(deprecated)* | Legacy document-shaped search | +| `POST /v4/profile` | `client.profile()` | Static + dynamic user profile | +| `POST /v4/profile/buckets` | — HTTP only | List effective bucket definitions | +| `PATCH /v4/memories` | `client.memories.updateMemory()` | Supersede a memory with a new version | +| `DELETE /v4/memories` | `client.memories.forget()` | Forget one memory | +| `POST /v4/memories/forget-matching` | — HTTP only | Agentic mass forget, with dry-run | +| `POST /v4/memories/list` | — HTTP only | List memory entries with version history | +| `POST /v3/documents/list` | `client.documents.list()` | Paginated document listing | +| `GET /v3/documents/{id}` | `client.documents.get()` | Fetch one document | +| `PATCH /v3/documents/{id}` | `client.documents.update()` | Update document content/metadata | +| `DELETE /v3/documents/{id}` | `client.documents.delete()` | Delete document + its memories | +| `DELETE /v3/documents/bulk` | `client.documents.deleteBulk()` | Delete many documents | +| `GET /v3/documents/processing` | `client.documents.listProcessing()` | In-flight ingestion status | +| `GET /v3/documents/{id}/chunks` | — HTTP only | Raw chunks for a document | +| `GET /v3/documents/{id}/file-url` | — HTTP only | Signed URL for an uploaded file | +| `GET /v3/container-tags/list` | — HTTP only | List container tags | +| `GET|PATCH|DELETE /v3/container-tags/{tag}` | — HTTP only | Read / configure / delete a container tag | +| `POST /v3/container-tags/merge` | — HTTP only | Merge one tag into another | +| `GET /v3/container-tags/merge/{mergeId}` | — HTTP only | Poll a merge job | +| `GET /v3/settings` | `client.settings.get()` | Read org settings | +| `PATCH /v3/settings` | `client.settings.update()` | Update org settings (incl. buckets) | +| `POST /v3/settings/reset` | — HTTP only | Reset org settings to defaults | +| `POST /v3/settings/suggest-buckets` | — HTTP only | AI-suggested bucket definitions | +| `/v3/connections/*` | `client.connections.*` | Google Drive, Notion, OneDrive, etc. | + +When no SDK method exists, call the endpoint directly — same base URL, same bearer token. That is a supported path, not a workaround: + +```typescript +const res = await fetch("https://api.supermemory.ai/v4/memories/forget-matching", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ containerTag: "user_123", query: "Project Titan", dryRun: true }), +}); ``` -**Response (200 OK):** - -```json -{ - "id": "doc_abc123xyz", - "status": "queued" -} -``` - -**Response (401 Unauthorized):** - -```json -{ - "error": "Unauthorized", - "details": "Invalid or missing API key" -} -``` - -**Response (500 Internal Server Error):** - -```json -{ - "error": "Internal Server Error", - "details": "Failed to process document" -} -``` - -**Processing Statuses:** -- `queued`: Document awaiting processing -- `extracting`: Content extraction in progress -- `chunking`: Breaking into semantic segments -- `embedding`: Generating vector embeddings -- `indexing`: Building relationships -- `done`: Processing complete, searchable - --- -### POST /v4/search +## Ingestion -Search memories using semantic understanding with advanced filtering. +### POST /v3/documents -**Endpoint:** -``` -POST https://api.supermemory.ai/v4/search -``` +Queue any content for processing. Extraction, chunking, embedding, and memory generation happen asynchronously. -**Headers:** -```http -Authorization: Bearer YOUR_API_KEY -Content-Type: application/json -``` - -**Request Body:** - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `query` | string | Yes | The search query | -| `containerTags` | string[] | No | Filter by container tags | -| `chunkThreshold` | number | No | Threshold for chunk selection (0-1). 0 = least sensitive (more results), 1 = most sensitive (fewer, accurate results). Default: 0 | -| `searchMode` | string | No | Search mode: "semantic" (default) or "hybrid" (semantic + keyword). Use "hybrid" for RAG applications for better accuracy | -| `docId` | string | No | Search within specific document (max 255 chars) | -| `filters` | object | No | Advanced filtering with AND/OR logic (up to 5 nesting levels) | - -**Filter Types:** - -```typescript -{ - "filters": { - // Metadata filtering - "metadata": { - "key": "value" - }, - - // Numeric comparisons - "numeric": { - "field": { "$gte": 4.0 } // >, <, >=, <=, = - }, - - // Array contains - "array_contains": { - "tags": "value" - }, - - // String contains - "string_contains": { - "content": "substring" - }, - - // Logical operators - "$and": [{ /* filters */ }], - "$or": [{ /* filters */ }] - } -} -``` - -**Example Request:** +| Field | Type | Required | Description | +|---|---|---|---| +| `content` | string | yes | Raw text, a URL, or a file reference. URLs, PDFs, images, and videos are fetched and parsed | +| `containerTag` | string | no | Space this document belongs to. Max 100 chars, alphanumeric plus `-`, `_`, `.` | +| `customId` | string | no | Your own idempotency key. Re-adding the same `customId` updates that document | +| `metadata` | object | no | String/number/boolean/string[] values, filterable at search time | +| `entityContext` | string | no | Up to 1,500 chars of context that steers extraction for this container tag | +| `filepath` | string | no | Virtual path, used by supermemory filesystem features | +| `taskType` | `"memory"` \| `"superrag"` | no | `"memory"` (default) for the full context layer, `"superrag"` for managed RAG only | ```bash -curl -X POST https://api.supermemory.ai/v4/search \ - -H "Authorization: Bearer YOUR_API_KEY" \ +curl -X POST https://api.supermemory.ai/v3/documents \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ - "query": "How do I authenticate users?", - "searchMode": "hybrid", - "chunkThreshold": 0.5, - "filters": { - "metadata": { - "type": "documentation", - "category": "security" - }, - "numeric": { - "rating": { "$gte": 4.0 } - } - } + "content": "John prefers dark mode and TypeScript over JavaScript", + "containerTag": "user_123", + "customId": "prefs_v1", + "metadata": { "source": "onboarding", "verified": true } }' ``` -**Response (200 OK):** +```json +{ "id": "doc_abc123", "status": "queued" } +``` + +Ingestion is asynchronous: poll `GET /v3/documents/processing` or `GET /v3/documents/{id}` for status. Expect ~1–2 minutes for a 100-page PDF and 5–10 minutes for video. + +### POST /v4/memories — write memories directly + +Bypasses document ingestion and extraction. Use when you already have clean, entity-centric facts; the memories are embedded and immediately searchable. + +| Field | Type | Required | Description | +|---|---|---|---| +| `memories` | array | yes | 1–100 items | +| `memories[].content` | string | yes | The fact, 1–10,000 chars. Write it entity-centric: "John prefers dark mode" | +| `memories[].isStatic` | boolean | no | `true` for permanent identity traits (name, hometown, profession). Defaults to `false` | +| `memories[].metadata` | object | no | Arbitrary key-value metadata | +| `memories[].forgetAfter` | string \| null | no | ISO 8601 expiry — the memory is auto-forgotten after this time | +| `memories[].forgetReason` | string \| null | no | Why it will expire. Only meaningful with `forgetAfter` | +| `memories[].temporalContext` | object | no | `{ documentDate?, eventDate?[] }` — when the content was authored / what dates it references | +| `containerTag` | string | yes | Space these memories belong to | + +```bash +curl -X POST https://api.supermemory.ai/v4/memories \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "memories": [ + { "content": "John prefers dark mode", "isStatic": false }, + { "content": "John is from Seattle", "isStatic": true } + ], + "containerTag": "user_123" + }' +``` + +```json +{ + "documentId": "doc_xyz", + "memories": [ + { "id": "mem_abc123", "memory": "John prefers dark mode", "isStatic": false, + "createdAt": "2026-08-10T12:00:00Z", "forgetAfter": null, "forgetReason": null, "metadata": null } + ] +} +``` + +### POST /v4/conversations — ingest a transcript + +Send structured messages instead of a flattened string. The backend diffs against the previous state of the same `conversationId`, so re-sending a grown transcript appends rather than duplicating. + +| Field | Type | Required | Description | +|---|---|---|---| +| `conversationId` | string | yes | Stable ID for this thread | +| `messages` | array | yes | `{ role: "user" \| "assistant" \| "system" \| "tool", content, name?, tool_calls?, tool_call_id? }`. `content` is a string or an array of `{ type: "text" \| "image_url", ... }` parts | +| `containerTags` | string[] | no | Spaces this conversation belongs to | +| `metadata` | object | no | Arbitrary key-value metadata | +| `entityContext` | string | no | Context that steers extraction | + +```bash +curl -X POST https://api.supermemory.ai/v4/conversations \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "conversationId": "conv_123", + "messages": [ + { "role": "user", "content": "I switched my editor to Zed" }, + { "role": "assistant", "content": "Noted — how are you finding it?" } + ], + "containerTags": ["user_123"] + }' +``` + +Prefer this over concatenating turns into `POST /v3/documents`: roles, tool calls, and images survive, and repeated sends of the same thread stay one document. + +--- + +## Recall + +### POST /v4/search + +The default search. Returns memories, optionally enriched with document chunks. + +| Field | Type | Required | Description | +|---|---|---|---| +| `q` | string | yes | Natural-language query | +| `containerTag` | string | no | Restrict to one space | +| `limit` | number | no | Max results | +| `threshold` | number | no | Similarity floor, 0–1 | +| `searchMode` | `"memories"` \| `"hybrid"` \| `"documents"` | no | `"memories"` (default) for facts, `"hybrid"` for facts + chunks (best for RAG), `"documents"` for chunks only | +| `filters` | object | no | Metadata filters, see [Filters](#filters) | +| `include` | object | no | `{ chunks?, documents?, summaries?, relatedMemories?, forgottenMemories? }` — extra payload per result | +| `rerank` | boolean | no | Re-rank results with a cross-encoder. Higher precision, slower | +| `rewriteQuery` | boolean | no | Let the service rewrite `q` before searching. Helps with terse or pronoun-heavy queries | +| `aggregate` | boolean | no | Collapse near-duplicate memories into one aggregated result | +| `filepath` | string | no | Restrict to a virtual path | + +```bash +curl -X POST https://api.supermemory.ai/v4/search \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "q": "authentication methods", + "containerTag": "docs", + "searchMode": "hybrid", + "threshold": 0.3, + "limit": 10 + }' +``` ```json { "results": [ { - "content": "Authentication can be done using JWT tokens...", - "score": 0.89, - "docId": "doc_123", - "metadata": { - "type": "documentation", - "category": "security", - "rating": 4.5 - }, - "chunkId": "chunk_456" - }, - { - "content": "OAuth 2.0 is a standard protocol for authorization...", - "score": 0.82, - "docId": "doc_789", - "metadata": { - "type": "documentation", - "category": "security", - "rating": 5.0 - }, - "chunkId": "chunk_789" + "id": "mem_abc123", + "memory": "The API authenticates with a bearer token", + "similarity": 0.82, + "updatedAt": "2026-08-01T10:00:00Z", + "metadata": { "source": "docs" }, + "version": 2 } ], - "total": 2 + "total": 1, + "timing": 142 } ``` -**Response (401 Unauthorized):** +Result fields worth knowing: `memory` is set on memory results, `chunk` on chunk results (hybrid/documents mode), `similarity` is the score (not `score`), and `chunks` / `documents` / `context` appear only when requested via `include`. -```json -{ - "error": "Unauthorized", - "details": "Invalid or missing API key" -} -``` +`POST /v3/search` still exists for the legacy document-shaped response and is exposed as the deprecated `client.search.documents()`. Use `/v4/search` for anything new. ---- +### POST /v4/profile -### POST /v4/memories +The fastest way to personalize a prompt: pre-computed facts about a container tag, no query required. -Create memories directly, bypassing document ingestion. Generates embeddings and makes them immediately searchable. - -**Endpoint:** -``` -POST https://api.supermemory.ai/v4/memories -``` - -**Headers:** -```http -Authorization: Bearer YOUR_API_KEY -Content-Type: application/json -``` - -**Request Body:** - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `memories` | array | Yes | Array of 1-100 memory objects | -| `memories[].content` | string | Yes | Memory text (1-10,000 chars). Preferably entity-centric (e.g., "John prefers dark mode") | -| `memories[].isStatic` | boolean | No | Marks permanent traits like name or profession. Default: false | -| `memories[].metadata` | object | No | Custom key-value pairs (strings, numbers, booleans, or string arrays) | -| `containerTag` | string | Yes | Identifier for the space/container these memories belong to | - -**Example Request:** +| Field | Type | Required | Description | +|---|---|---|---| +| `containerTag` | string | yes | User / project / space identifier | +| `q` | string | no | Also run a search and return `searchResults` | +| `threshold` | number | no | Similarity floor for `searchResults`, 0–1 | +| `filters` | object | no | Metadata filters applied to profile *and* search results | +| `include` | array | no | Sections to return: `"static"`, `"dynamic"`, `"buckets"`. Omit for all. **HTTP only** — not in the generated SDKs | +| `buckets` | string[] | no | Limit to specific bucket keys. Only meaningful with `"buckets"` included. **HTTP only** | ```bash -curl -X POST https://api.supermemory.ai/v4/memories \ - -H "Authorization: Bearer YOUR_API_KEY" \ +curl -X POST https://api.supermemory.ai/v4/profile \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ -H "Content-Type: application/json" \ - -d '{ - "containerTag": "user_123", - "memories": [ - { - "content": "User prefers dark mode", - "isStatic": true, - "metadata": { - "category": "preferences", - "source": "settings" - } - }, - { - "content": "User mentioned working on a React project yesterday", - "isStatic": false, - "metadata": { - "category": "activity", - "timestamp": "2026-02-20T15:30:00Z" - } - } - ] - }' + -d '{ "containerTag": "user_123", "q": "what tools does the user like?" }' ``` -**Response (201 Created):** - ```json { - "documentId": "doc_abc123", - "memories": [ - { - "id": "mem_xyz789", - "memory": "User prefers dark mode", - "isStatic": true, - "createdAt": "2026-02-21T10:00:00Z" - }, - { - "id": "mem_def456", - "memory": "User mentioned working on a React project yesterday", - "isStatic": false, - "createdAt": "2026-02-21T10:00:00Z" - } - ] -} -``` - -**Response (400 Bad Request):** - -```json -{ - "error": "Bad Request", - "details": "Invalid request parameters: memories array must contain 1-100 items" -} -``` - -**Response (404 Not Found):** - -```json -{ - "error": "Not Found", - "details": "Space not found for given containerTag" -} -``` - ---- - -## Error Handling - -### HTTP Status Codes - -| Code | Meaning | Description | -|------|---------|-------------| -| 200 | OK | Request successful | -| 201 | Created | Resource created successfully | -| 400 | Bad Request | Invalid request parameters | -| 401 | Unauthorized | Missing or invalid API key | -| 404 | Not Found | Resource not found | -| 429 | Too Many Requests | Rate limit exceeded | -| 500 | Internal Server Error | Server error occurred | - -### Error Response Format - -All errors follow this format: - -```json -{ - "error": "Error Type", - "details": "Detailed error message" -} -``` - -### Common Errors - -**Invalid API Key:** -```json -{ - "error": "Unauthorized", - "details": "Invalid or missing API key" -} -``` - -**Rate Limit Exceeded:** -```json -{ - "error": "Too Many Requests", - "details": "Rate limit exceeded. Please try again later." -} -``` - -**Invalid Parameters:** -```json -{ - "error": "Bad Request", - "details": "content field is required" -} -``` - -## Rate Limits - -Rate limits are enforced to ensure system stability. When rate limited, the response includes: - -```http -HTTP/1.1 429 Too Many Requests -Retry-After: 3600 -``` - -Check your plan details in the [console](https://console.supermemory.ai) for specific rate limit information. - -## Best Practices - -### 1. Use Idempotent IDs - -Use `customId` for idempotency to prevent duplicate processing: - -```bash -curl -X POST https://api.supermemory.ai/v3/documents \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "content": "Important document", - "customId": "doc_2026_02_21_001", - "containerTag": "user_123" - }' -``` - -### 2. Proper Error Handling - -Always check status codes and handle errors gracefully: - -```javascript -const response = await fetch('https://api.supermemory.ai/v3/documents', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${API_KEY}`, - 'Content-Type': 'application/json' + "profile": { + "static": ["John Doe, staff engineer in Seattle"], + "dynamic": ["[Recent] [2026-08-09] Switched their editor to Zed"], + "buckets": { "preferences": ["[Summary] Prefers concise, technical answers"] } }, - body: JSON.stringify({ content: "...", containerTag: "user_123" }) + "searchResults": { "results": [], "total": 0, "timing": 88 } +} +``` + +`searchResults` is present only when `q` was provided. `buckets` is present only when requested via `include`. + +**`[Recent]` and `[Summary]` prefixes.** Older memories for an entity are periodically aggregated into a synthesis, prefixed `[Summary]`; anything ingested since is prefixed `[Recent]` (with a `[YYYY-MM-DD]` date in `dynamic`). Strip the prefixes for raw text, or keep them to signal recency to your model. + +Because `include`/`buckets` are not in the generated SDK types, request bucketed profiles over HTTP: + +```typescript +const res = await fetch("https://api.supermemory.ai/v4/profile", { + method: "POST", + headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + containerTag: "user_123", + include: ["buckets"], // omits static + dynamic entirely + buckets: ["preferences", "goals"], // omit for all configured buckets + }), }); - -if (!response.ok) { - const error = await response.json(); - console.error(`Error ${response.status}:`, error.details); - throw new Error(error.details); -} - -const data = await response.json(); +const { profile } = await res.json(); ``` -### 3. Use Container Tags Consistently +### POST /v4/profile/buckets -Maintain consistent naming for container tags: +Lists the **effective** bucket definitions for a container tag — org buckets merged with that tag's own additions. Use it to discover valid keys before requesting a bucketed profile. ```bash -# Good -containerTag: "user_123" -containerTag: "user_456" - -# Avoid inconsistency -containerTag: "user_123" -containerTag: "123" # Different format +curl -X POST https://api.supermemory.ai/v4/profile/buckets \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "containerTag": "user_123" }' ``` -### 4. Rich Metadata +```json +{ "buckets": [{ "key": "preferences", "description": "Explicit first-person preferences..." }] } +``` -Add comprehensive metadata for better filtering: +Reading buckets works with any key and any role. Writing them does not — see [Buckets](#buckets). + +--- + +## Memory lifecycle + +Memories are versioned and soft-deleted. Updating creates a new version that supersedes the old one; forgetting marks a memory forgotten rather than destroying the row. + +### DELETE /v4/memories — forget one memory + +| Field | Type | Required | Description | +|---|---|---|---| +| `containerTag` | string | yes | Scopes the operation | +| `id` | string | one of* | Memory ID | +| `content` | string | one of* | Exact content match, when you don't have the ID | +| `reason` | string | no | Recorded as `forgetReason` | + +\* Provide either `id` or `content`. + +```typescript +const res = await client.memories.forget({ id: "mem_abc123", containerTag: "user_123" }); +// { id: "mem_abc123", forgotten: true } +``` + +### PATCH /v4/memories — update (new version) + +Creates a new version rather than editing in place, so history is preserved. + +| Field | Type | Required | Description | +|---|---|---|---| +| `containerTag` | string | yes | Scopes the operation | +| `newContent` | string | yes | Replacement content | +| `id` \| `content` | string | one of | Which memory to supersede | +| `metadata` | object | no | Metadata for the new version. Inherits the previous version's if omitted | +| `forgetAfter` | string \| null | no | ISO expiry. `null` clears an existing expiry; omit to inherit | +| `forgetReason` | string \| null | no | Cleared automatically when `forgetAfter` is set to `null` | +| `temporalContext` | object | no | `{ documentDate?, eventDate?[] }`. Existing value preserved if omitted | + +```typescript +const res = await client.memories.updateMemory({ + id: "mem_abc123", + containerTag: "user_123", + newContent: "John now prefers light mode", +}); +// { id: "mem_xyz789", memory: "...", version: 2, parentMemoryId: "mem_abc123", rootMemoryId: "mem_abc123", ... } +``` + +Prefer this over forget-then-add when a fact changed: the version chain keeps the correction traceable. + +### POST /v4/memories/forget-matching — mass forget + +Bulk forget in one call, two ways. Give a **`query`** and the service semantically searches the container, an LLM decides which memories are genuinely about your target, and those are soft-deleted — this is the "forget everything about X" path. Or give an explicit **`ids`** list to forget exactly those, with no search. + +| Field | Type | Required | Description | +|---|---|---|---| +| `containerTag` | string | yes | Scopes the operation | +| `query` | string | one of* | What to forget — an instruction ("forget everything about Project Titan") or a bare topic ("Project Titan"). Max 2,000 chars | +| `ids` | string[] | one of* | Exact memory IDs, 1–500. Validated against `containerTag`, so unknown or out-of-scope IDs are ignored | +| `dryRun` | boolean | no | Preview without mutating. Defaults to `false` — **pass `true` first** | +| `threshold` | number | no | Similarity floor for candidates, 0–1. Lower casts a wider net. Defaults to `0.5` | +| `maxForget` | number | no | Safety cap for query mode, 1–500. Defaults to `100`. Ignored in ID mode | +| `reason` | string | no | Recorded as `forgetReason` on each memory | + +\* Provide either `query` or a non-empty `ids`. + +```bash +# 1) Preview +curl -X POST https://api.supermemory.ai/v4/memories/forget-matching \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "containerTag": "user_123", "query": "forget everything about Project Titan", "dryRun": true }' + +# 2) Apply the exact set you reviewed +curl -X POST https://api.supermemory.ai/v4/memories/forget-matching \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "containerTag": "user_123", "ids": ["mem_abc123", "mem_def456"], "reason": "project cancelled" }' +``` ```json { - "content": "Product review", - "containerTag": "reviews", - "metadata": { - "product": "iPhone 15", - "rating": 4.5, - "verified": true, - "date": "2026-02-21", - "tags": ["smartphone", "apple"] - } + "dryRun": true, + "count": 2, + "forgetBatchId": null, + "summary": "Selected 2 memories about Project Titan", + "candidates": [{ "id": "mem_abc123", "memory": "Project Titan ships in Q3", "score": 0.71 }] } ``` -### 5. Optimize Search Thresholds +| Field | Description | +|---|---| +| `dryRun` | Whether this was a preview or a real forget | +| `count` | How many memories were selected / forgotten | +| `forgetBatchId` | Tagged on every memory forgotten in this call for traceability; `null` on dry runs | +| `summary` | The agent's one-line account of what it did | +| `candidates` / `forgotten` | The affected memories — `candidates` on dry run, `forgotten` on apply | -Start with default (0) and adjust based on results: +**Always dry-run first.** This is bulk and destructive, and the match is semantic, so a broad query can select more than you intend. Applying with a `query` re-runs the match, which can drift from the preview if the container changed in between — to forget *precisely* what you reviewed, take the `id`s from the preview and send them back as `ids`. Identity is server-owned: the LLM only ever sees opaque handles for memories a search returned, so it cannot reach outside those results or outside `containerTag`. -```json -{ - "query": "authentication methods", - "chunkThreshold": 0.5 // Balanced precision/recall -} -``` +### POST /v4/memories/list -### 6. Monitor Processing Status +Latest memory entries for one or more container tags, with version history and source documents. -For large documents, check processing status: +| Field | Type | Required | Description | +|---|---|---|---| +| `containerTags` | string[] | yes | At least one tag | +| `filters` | object | no | Metadata filters | +| `limit` | number | no | Page size, 1–1100. Defaults to `10` | +| `page` | number | no | 1-based. Defaults to `1` | +| `sort` | `"createdAt"` \| `"updatedAt"` | no | Defaults to `"createdAt"` | +| `order` | `"asc"` \| `"desc"` | no | Defaults to `"desc"` | + +Use this for audit and review UIs — it shows what the system actually believes, including superseded versions. `POST /v3/documents/list` is the document-level equivalent. + +--- + +## Container tags + +A container tag is a space: the isolation and grouping unit for memories. Tags are created implicitly on first write, so these endpoints are for inspection and configuration. + +| Endpoint | Purpose | +|---|---| +| `GET /v3/container-tags/list` | Every tag in the org | +| `GET /v3/container-tags/{tag}` | One tag's configuration | +| `PATCH /v3/container-tags/{tag}` | Set `entityContext`, `profileBuckets`, `memoryFilesystemPaths`, `name` | +| `DELETE /v3/container-tags/{tag}` | Delete the tag and its content | +| `POST /v3/container-tags/merge` | Merge one tag into another (`{ from, to }`); returns a merge job | +| `GET /v3/container-tags/merge/{mergeId}` | Poll merge status | ```bash -# Add document -curl -X POST https://api.supermemory.ai/v3/documents \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -d '{ "content": "large-document.pdf", "containerTag": "docs" }' - -# Returns: { "id": "doc_123", "status": "queued" } - -# Later, list documents to check status -curl -X GET https://api.supermemory.ai/v3/documents?containerTag=docs \ - -H "Authorization: Bearer YOUR_API_KEY" +curl -X PATCH https://api.supermemory.ai/v3/container-tags/user_alex \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ "entityContext": "This tag belongs to a solo founder juggling sales, hiring, and product." }' ``` -## SDK vs Direct API +`entityContext` is per-tag (up to 1,500 chars, `null` to clear) and is folded into the same prompt as the org-level `filterPrompt` during extraction. Use it for who-this-space-is context; use `filterPrompt` for guidance that should apply everywhere. -**Use SDK when:** -- Building applications in TypeScript/Python -- Want automatic error handling and retries -- Need type safety and autocomplete -- Prefer higher-level abstractions +Merging is the fix for the common "same person, two IDs" problem (anonymous session promoted to a signed-in user). -**Use Direct API when:** -- Working in other languages -- Need fine-grained control -- Building serverless functions -- Integrating with existing HTTP clients +--- -## Complete cURL Examples +## Buckets -### Add Text Content +Buckets are custom topical categories for a profile — an axis alongside `static`/`dynamic`. Static vs dynamic splits facts by how long-lived they are; buckets group them by subject. A classifier assigns each memory to matching buckets at ingestion. + +Reading is covered above (`POST /v4/profile` with `include`, `POST /v4/profile/buckets`). Writing goes through the settings and container-tag endpoints: + +| Level | Endpoint | Semantics | +|---|---|---| +| Organization | `PATCH /v3/settings` with `profileBuckets` | The default set every tag inherits. **Replaces** the stored list — always send the full set | +| Space | `PATCH /v3/container-tags/{tag}` with `profileBuckets` | Add-only on top of org buckets. A tag keeps every org bucket; on key collision the org definition wins | +| Suggestions | `POST /v3/settings/suggest-buckets` | 3–6 AI-generated suggestions derived from your org's `filterPrompt`. Saves nothing — feed the results into `PATCH /v3/settings` | ```bash -curl -X POST https://api.supermemory.ai/v3/documents \ - -H "Authorization: Bearer YOUR_API_KEY" \ +curl -X PATCH https://api.supermemory.ai/v3/settings \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ - "content": "User mentioned they prefer TypeScript over JavaScript for type safety", - "containerTag": "user_123", - "metadata": { - "source": "chat", - "timestamp": "2026-02-21T10:00:00Z" - } - }' -``` - -### Add URL - -```bash -curl -X POST https://api.supermemory.ai/v3/documents \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "content": "https://blog.example.com/best-practices", - "containerTag": "knowledge_base", - "entityContext": "Software development best practices article", - "metadata": { - "type": "article", - "category": "best-practices" - } - }' -``` - -### Search with Filters (Hybrid Mode for RAG) - -```bash -curl -X POST https://api.supermemory.ai/v4/search \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "React performance optimization", - "searchMode": "hybrid", - "chunkThreshold": 0.6, - "filters": { - "$and": [ - { - "metadata": { - "type": "tutorial" - } - }, - { - "numeric": { - "rating": { "$gte": 4.0 } - } - } - ] - } - }' -``` - -### Create Direct Memories - -```bash -curl -X POST https://api.supermemory.ai/v4/memories \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "containerTag": "user_789", - "memories": [ - { - "content": "User name is Alice Johnson", - "isStatic": true, - "metadata": { "type": "profile" } - }, - { - "content": "Alice completed the React tutorial today", - "isStatic": false, - "metadata": { "type": "activity", "date": "2026-02-21" } - } + "profileBuckets": [ + { "key": "work", "description": "Professional role, employer, projects, and work-related decisions." }, + { "key": "health", "description": "Physical and mental wellbeing, habits, and health-related goals." } ] }' ``` -## Webhook Support +Writing buckets requires an **admin or owner** role and a **full-access** API key — scoped keys get `403`. `suggest-buckets` needs a `filterPrompt` already set on the org, or it returns `400`. -Coming soon: Webhooks for document processing status updates. +| Rule | Detail | +|---|---| +| Key format | Lowercase alphanumeric, starts with a letter/digit, may contain `-`/`_`, 1–64 chars | +| Reserved keys | `static` and `dynamic` | +| Max buckets | 50 per array, counted separately for the org list and each space list | +| Duplicates | Rejected within a single request | +| Description | Optional, up to 2,000 chars | + +If neither the org nor the space defines buckets, ingestion falls back to a single built-in `preferences` bucket, scoped tightly to explicit first-person statements. Bucket descriptions steer the classifier, so write them precisely: "Explicit first-person preferences only — exclude inferred traits" yields much cleaner buckets than "stuff the user likes". + +--- + +## Filters + +Metadata filters are accepted by `/v4/search`, `/v3/search`, `/v4/profile`, `/v4/memories/list`, and `/v3/documents/list`. Conditions must be wrapped in an `AND` or `OR` array — a bare `{ metadata: {...} }` object is not valid. + +```typescript +const results = await client.search({ + q: "design document", + containerTag: "user_123", + filters: { + AND: [ + { key: "category", value: "engineering" }, + { key: "priority", value: "high" }, + ], + }, +}); +``` + +| Type | Example | Description | +|---|---|---| +| String equality | `{ key: "status", value: "published" }` | Exact match (default) | +| String contains | `{ filterType: "string_contains", key: "title", value: "react" }` | Substring match | +| Numeric | `{ filterType: "numeric", key: "priority", value: "5", numericOperator: ">=" }` | `>`, `<`, `>=`, `<=`, `=` | +| Array contains | `{ filterType: "array_contains", key: "tags", value: "important" }` | Membership in a string array | + +`AND`/`OR` nest, and any condition accepts `negate: true` and `ignoreCase: true`. Numeric values are passed as strings. + +--- + +## Error handling + +| Status | Meaning | +|---|---| +| `200` / `201` | Success | +| `400` | Invalid request — bad body, missing required field, or unmet precondition (e.g. `suggest-buckets` with no `filterPrompt`) | +| `401` | Missing or invalid API key | +| `402` | Search quota or credits exhausted | +| `403` | Key or role lacks permission — usually a scoped key hitting an org-level write | +| `404` | Resource not found, or `containerTag` has no matching space | +| `429` | Rate limited. Honour `Retry-After` | +| `500` | Server error — safe to retry with backoff | + +Errors return `{ "error": "message" }`. The SDKs raise typed errors instead: + +```typescript +import { APIError, RateLimitError, AuthenticationError } from "supermemory"; + +try { + await client.documents.add({ content: "...", containerTag: "user_123" }); +} catch (error) { + if (error instanceof AuthenticationError) console.error("Invalid API key"); + else if (error instanceof RateLimitError) console.error("Rate limited"); + else if (error instanceof APIError) console.error(error.status, error.message); + else throw error; +} +``` + +Rate limits depend on your plan — check the [console](https://console.supermemory.ai). Both SDKs retry idempotent failures with backoff by default. + +--- + +## Best practices + +**Use one container tag format.** `user_${userId}` everywhere beats `user_123` in some places and `123` in others; there is no fuzzy matching across tags. If you do end up with two tags for one entity, merge them rather than re-ingesting. + +**Use `customId` for idempotency.** Re-posting the same `customId` updates that document instead of creating a duplicate — the simplest defence against retries and replayed webhooks. + +**Pick the right write path.** Extracted facts from prose or documents → `POST /v3/documents`. A chat thread → `POST /v4/conversations`. Facts you already have clean → `POST /v4/memories`. + +**Pick the right read path.** Personalizing a prompt → `POST /v4/profile` (no query needed, pre-computed). Answering a specific question → `POST /v4/search`. RAG over documents → `/v4/search` with `searchMode: "hybrid"`. + +**Correct, don't churn.** `PATCH /v4/memories` when a fact changed; `DELETE /v4/memories` when it should never have been stored; `forget-matching` with `dryRun` when a whole topic must go. + +**Tune thresholds from real queries.** 0.3–0.5 favours recall, 0.5–0.7 is balanced, 0.7+ favours precision. Reach for `rerank` when precision matters more than latency, and `rewriteQuery` when queries are terse or pronoun-heavy. + +**Mark real identity traits `isStatic`.** Name, hometown, profession. Not "currently working on the billing revamp" — that is dynamic and should be allowed to age. + +--- ## Support -- **API Issues**: Check [status.supermemory.ai](https://status.supermemory.ai) - **Documentation**: [supermemory.ai/docs](https://supermemory.ai/docs) +- **Status**: [status.supermemory.ai](https://status.supermemory.ai) - **Console**: [console.supermemory.ai](https://console.supermemory.ai) diff --git a/skills/supermemory/references/architecture.md b/skills/supermemory/references/architecture.md index 61c35e6c..c74295b0 100644 --- a/skills/supermemory/references/architecture.md +++ b/skills/supermemory/references/architecture.md @@ -200,6 +200,18 @@ When querying, you can choose: - Full version history - Specific version +`PATCH /v4/memories` is what creates a version: it writes a new memory carrying `parentMemoryId` and `rootMemoryId`, and the old version stops being the latest. `POST /v4/memories/list` returns entries with their history, which is what an audit or review UI should read. + +### Forgetting + +Forgetting is a soft delete — the row survives, marked forgotten, so a correction is auditable and reversible rather than a hole in the graph: + +- **Single memory**: `DELETE /v4/memories` by `id` or exact `content`, with an optional `reason` stored as `forgetReason` +- **Scheduled**: set `forgetAfter` (ISO datetime) at write or update time and the memory is auto-forgotten when it expires +- **Mass forget**: `POST /v4/memories/forget-matching` takes a topic or instruction, semantically searches the container, and an LLM decides which candidates are genuinely about the target. Every memory forgotten in one call shares a `forgetBatchId` for traceability + +The mass path is bounded by design: `threshold` sets the similarity floor, `maxForget` caps the blast radius, `dryRun` previews without mutating, and the LLM only ever sees opaque handles for memories a search already returned — so it cannot reach outside those results or outside the `containerTag`. + ## Retrieval Mechanism ### Semantic Search Process @@ -309,25 +321,36 @@ Recent Activity: - Discussed performance optimization (last week) ``` -**Combined Profile:** +**Buckets** (custom topical categories, assigned by a classifier at ingestion): +``` +preferences: Dark mode, TypeScript, Vim keybindings +work: Senior Software Engineer, currently on the auth revamp +goals: Wants to ship SSO this quarter +``` + +Buckets are a second axis over the same memories: static/dynamic splits by how long-lived a fact is, buckets split by subject. They're defined per-org (and extended per container tag), so a surface can request just the slice it needs. + +**Combined Profile** (what `POST /v4/profile` actually returns): ```javascript { - "profile": "John Doe, Senior Software Engineer who prefers TypeScript and dark mode", - "memories": [ - { - "content": "Currently working on React authentication", - "score": 0.95, - "timestamp": "2 hours ago" - }, - { - "content": "Completed advanced TypeScript course", - "score": 0.87, - "timestamp": "yesterday" - } - ] + "profile": { + "static": ["John Doe, Senior Software Engineer in Seattle"], + "dynamic": ["[Recent] [2026-08-09] Working on React authentication"], + "buckets": { "preferences": ["[Summary] Prefers dark mode and TypeScript"] } + }, + "searchResults": { // only when a query `q` was provided + "results": [ + { "id": "mem_abc", "memory": "Currently working on React authentication", + "similarity": 0.95, "updatedAt": "2026-08-10T10:00:00Z", "metadata": null } + ], + "total": 1, + "timing": 88 + } } ``` +Entries prefixed `[Summary]` are aggregated older context; `[Recent]` entries arrived since the last aggregation. `buckets` appears only when requested. + ## Graph Evolution The knowledge graph continuously evolves: diff --git a/skills/supermemory/references/quickstart.md b/skills/supermemory/references/quickstart.md index 4be0a762..a56caab9 100644 --- a/skills/supermemory/references/quickstart.md +++ b/skills/supermemory/references/quickstart.md @@ -51,7 +51,7 @@ SUPERMEMORY_API_KEY=your_api_key_here ### TypeScript Example ```typescript -import { Supermemory } from 'supermemory'; +import Supermemory from 'supermemory'; const client = new Supermemory({ apiKey: process.env.SUPERMEMORY_API_KEY @@ -73,10 +73,10 @@ async function main() { // 2. Enrich your LLM prompt const systemMessage = ` Static Profile: - ${response.profile.static.map(f => `- ${f}`).join('\n')} + ${(response.profile.static ?? []).map(f => `- ${f}`).join('\n')} Recent Context: - ${response.profile.dynamic.map(f => `- ${f}`).join('\n')} + ${(response.profile.dynamic ?? []).map(f => `- ${f}`).join('\n')} `; // Send systemMessage to your LLM... @@ -112,14 +112,14 @@ def main(): q="What does the user prefer?" ) - print("Static Profile:", response["profile"]["static"]) - print("Dynamic Profile:", response["profile"]["dynamic"]) - if "searchResults" in response: - print("Search Results:", response["searchResults"]["results"]) + print("Static Profile:", response.profile.static) + print("Dynamic Profile:", response.profile.dynamic) + if response.search_results: + print("Search Results:", response.search_results.results) # 2. Enrich your LLM prompt - static_facts = "\n".join(f"- {fact}" for fact in response["profile"]["static"]) - dynamic_facts = "\n".join(f"- {fact}" for fact in response["profile"]["dynamic"]) + static_facts = "\n".join(f"- {fact}" for fact in response.profile.static or []) + dynamic_facts = "\n".join(f"- {fact}" for fact in response.profile.dynamic or []) system_message = f""" Static Profile: @@ -163,7 +163,7 @@ async def main(): q="What does the user prefer?" ) - print("User facts:", response["profile"]["static"]) + print("User facts:", response.profile.static) # 2. Store new memories await client.add( @@ -206,7 +206,7 @@ Control relevance strictness with the `threshold` parameter: ```typescript const context = await client.profile({ containerTag: "user_123", - query: "user preferences", + q: "user preferences", threshold: 0.7 // 0-1: higher = stricter matching }); ``` @@ -217,10 +217,11 @@ const context = await client.profile({ ## Next Steps -- **User Profiles**: Learn about static vs. dynamic facts -- **Search API**: Explore advanced filtering and metadata queries -- **Document Ingestion**: Add PDFs, images, videos, and URLs -- **Integration Guides**: Connect with Vercel AI SDK, LangChain, CrewAI +- **User Profiles**: Learn about static vs. dynamic facts, and topical buckets +- **Search API**: Explore advanced filtering, re-ranking, and query rewriting +- **Document Ingestion**: Add PDFs, images, videos, URLs, and chat transcripts +- **Forgetting**: Correct facts that changed, and forget a memory or a whole topic +- **Integration Guides**: Connect with Vercel AI SDK, LangChain, CrewAI — or use `@supermemory/tools` ## Common Patterns @@ -229,7 +230,7 @@ const context = await client.profile({ // Before generating response const context = await client.profile({ containerTag: userId, - query: userMessage + q: userMessage }); // After receiving LLM response @@ -262,13 +263,32 @@ const response = await client.search({ // Get user profile const profile = await client.profile({ containerTag: userId, - query: "user interests and preferences" + q: "user interests and preferences" }); // Use profile to personalize recommendations const recommendations = generateRecommendations(profile); ``` +### Correcting and Forgetting +```typescript +// A fact changed → new version supersedes the old one, history preserved +await client.memories.updateMemory({ + containerTag: userId, + id: "mem_abc123", + newContent: "Now prefers light mode" +}); + +// Should never have been stored → forget it +await client.memories.forget({ + containerTag: userId, + id: "mem_abc123", + reason: "user asked" +}); +``` + +To forget a whole topic at once, use `POST /v4/memories/forget-matching` with `dryRun: true`, review what matched, then apply with the returned `ids`. It has no SDK method yet — see `sdk-guide.md`. + ## Troubleshooting **API Key Not Working** @@ -284,7 +304,7 @@ const recommendations = generateRecommendations(profile); **Slow Processing** - Large PDFs (100 pages) take 1-2 minutes - Videos take 5-10 minutes -- Check document status with `documents.list()` +- Check document status with `documents.listProcessing()` ## Support diff --git a/skills/supermemory/references/sdk-guide.md b/skills/supermemory/references/sdk-guide.md index b2386031..716c94ef 100644 --- a/skills/supermemory/references/sdk-guide.md +++ b/skills/supermemory/references/sdk-guide.md @@ -2,9 +2,9 @@ Complete reference for the Supermemory SDK in TypeScript and Python. -## Installation +Method availability below was verified against `supermemory@4.25.4` (npm) and `supermemory==3.56.0` (PyPI). The SDKs are generated from the OpenAPI spec and trail the API slightly — anything marked "no SDK method" is still callable over HTTP, and may have gained a method in a newer release. -Supermemory works with the following SDKs natively: +## Installation ### TypeScript/JavaScript ```bash @@ -34,11 +34,12 @@ Discover all available SDKs, community integrations, and framework-specific guid ### TypeScript ```typescript -import { Supermemory } from 'supermemory'; +import Supermemory from 'supermemory'; // default export +// import { Supermemory } from 'supermemory'; // named export also works const client = new Supermemory({ apiKey: process.env.SUPERMEMORY_API_KEY, // Optional if env var is set - baseURL: 'https://api.supermemory.ai' // Optional, defaults to this + baseURL: 'https://api.supermemory.ai' // Optional, defaults to this }); ``` @@ -60,31 +61,55 @@ async_client = AsyncSupermemory( ) ``` +## Method map + +| Task | TypeScript | Python | +|---|---|---| +| Ingest content | `client.add()` / `client.documents.add()` | `client.add()` / `client.documents.add()` | +| Batch ingest | `client.documents.batchAdd()` | `client.documents.batch_add()` | +| Upload a file | `client.documents.uploadFile()` | `client.documents.upload_file()` | +| Search memories | `client.search()` | `client.search.memories()` | +| Get a profile | `client.profile()` | `client.profile()` | +| Forget a memory | `client.memories.forget()` | `client.memories.forget()` | +| Update a memory | `client.memories.updateMemory()` | `client.memories.update_memory()` | +| List documents | `client.documents.list()` | `client.documents.list()` | +| Get / update / delete a document | `client.documents.get()` / `.update()` / `.delete()` | `client.documents.get()` / `.update()` / `.delete()` | +| Bulk delete documents | `client.documents.deleteBulk()` | `client.documents.delete_bulk()` | +| Ingestion status | `client.documents.listProcessing()` | `client.documents.list_processing()` | +| Org settings | `client.settings.get()` / `.update()` | `client.settings.get()` / `.update()` | +| Connections | `client.connections.*` | `client.connections.*` | + +No SDK method yet — [call these over HTTP](#endpoints-without-sdk-methods): direct memory writes, mass forget, memory listing, profile buckets, conversation ingestion, container tags, settings reset, bucket suggestions. + ## Core Methods -### `add()` - Store Memories +### `add()` - Store Content -Add content to Supermemory for processing and memory extraction. +Queue content for processing and memory extraction. `client.add()` and `client.documents.add()` are the same endpoint (`POST /v3/documents`). #### TypeScript ```typescript await client.add({ - content: string | URL, // Required: text, URL, or file path - containerTag?: string, // Optional: isolation identifier - entityContext?: string, // Optional: context for memory extraction - customId?: string, // Optional: your custom identifier - metadata?: Record // Optional: custom key-value pairs + content: string, // Required: text, URL, or file reference + containerTag?: string, // Optional: isolation identifier + customId?: string, // Optional: your idempotency key + metadata?: Record, + entityContext?: string, // Optional: context that steers extraction (max 1500 chars) + filepath?: string, // Optional: virtual path + taskType?: "memory" | "superrag" // Optional: defaults to "memory" }); +// Returns: { id: string, status: string } ``` #### Python ```python client.add( - content=str | url, # Required: text, URL, or file path - container_tag=str, # Optional: isolation identifier - entity_context=str, # Optional: context for memory extraction - custom_id=str, # Optional: your custom identifier - metadata=dict # Optional: custom key-value pairs + content=str, # Required: text, URL, or file reference + container_tag=str, # Optional: isolation identifier + custom_id=str, # Optional: your idempotency key + metadata=dict, # Optional: custom key-value pairs + entity_context=str, # Optional: context that steers extraction + task_type=str, # Optional: "memory" (default) or "superrag" ) ``` @@ -112,41 +137,39 @@ await client.add({ }); ``` -**Add with custom ID:** +**Add with custom ID (idempotent):** ```typescript await client.add({ content: "Project requirements document...", containerTag: "project_abc", - customId: "requirements_v1", + customId: "requirements_v1", // re-posting this ID updates the same document metadata: { version: "1.0", author: "john@example.com" } }); ``` +Ingestion is asynchronous. `status` comes back as `"queued"`; poll `client.documents.listProcessing()` or `client.documents.get(id)` to see when memories are available. + ### `profile()` - Retrieve User Context -Get personalized context including static profile data and relevant dynamic memories. +Pre-computed facts for a container tag. The cheapest way to personalize a prompt — no query required. #### TypeScript ```typescript const response = await client.profile({ containerTag: string, // Required: user/project identifier - q?: string, // Optional: search query to include search results - threshold?: number // Optional: relevance threshold (0-1, default 0.5) + q?: string, // Optional: also run a search, returned as searchResults + threshold?: number, // Optional: similarity floor for searchResults (0-1) + filters?: FilterObject // Optional: metadata filters, see "Metadata Filtering" }); // Returns: // { // profile: { -// static: string[], // Array of static memories (permanent facts) -// dynamic: string[] // Array of dynamic memories (recent context) +// static?: string[], // Long-term facts (name, profession, stable preferences) +// dynamic?: string[] // Recent context, prefixed [Recent] [YYYY-MM-DD] // }, -// searchResults?: { // Only included if q parameter was provided -// results: Array<{ // Search results -// id: string, -// memory?: string, -// similarity: number, -// metadata: object | null -// }>, +// searchResults?: { // Only when q was provided +// results: Array<{ id, memory?, similarity, updatedAt, metadata }>, // total: number, // timing: number // } @@ -157,78 +180,99 @@ const response = await client.profile({ ```python response = client.profile( container_tag=str, # Required: user/project identifier - q=str, # Optional: search query to include search results - threshold=float # Optional: relevance threshold (0-1, default 0.5) + q=str, # Optional: also run a search + threshold=float, # Optional: similarity floor for search results (0-1) + filters=dict, # Optional: metadata filters ) -# Returns dict: -# { -# "profile": { -# "static": List[str], # Array of static memories (permanent facts) -# "dynamic": List[str] # Array of dynamic memories (recent context) -# }, -# "searchResults": { # Only included if q parameter was provided -# "results": List[dict], # Search results -# "total": int, -# "timing": int -# } -# } +# Returns a model, accessed by attribute: +# response.profile.static -> list[str] +# response.profile.dynamic -> list[str] +# response.search_results -> present only when q was provided ``` #### Examples -**Get user profile:** +**Profile with a query:** ```typescript const response = await client.profile({ containerTag: "user_123", q: "What are the user's preferences and settings?" }); -console.log(response.profile.static); // ["User John Doe", "Prefers dark mode", ...] -console.log(response.profile.dynamic); // ["Recently mentioned...", "Last conversation..."] -console.log(response.searchResults); // Search results for the query (if provided) +console.log(response.profile.static); // ["John Doe, staff engineer in Seattle", ...] +console.log(response.profile.dynamic); // ["[Recent] [2026-08-09] Switched their editor to Zed"] +console.log(response.searchResults); // Query-relevant memories ``` -**Profile without search (just get stored memories):** +**Profile without a query:** ```typescript -const response = await client.profile({ - containerTag: "user_456" - // No q parameter = only returns profile.static and profile.dynamic -}); +const response = await client.profile({ containerTag: "user_456" }); -console.log(response.profile.static); // All static facts -console.log(response.profile.dynamic); // Recent dynamic memories -// response.searchResults will be undefined +console.log(response.profile.static); // All long-term facts +console.log(response.profile.dynamic); // Recent context +// response.searchResults is undefined ``` +**Bucketed profile (HTTP — `include`/`buckets` are not in the SDK types yet):** +```typescript +const res = await fetch("https://api.supermemory.ai/v4/profile", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + containerTag: "user_123", + include: ["buckets"], // omits static + dynamic entirely + buckets: ["preferences", "goals"], // omit for all configured buckets + }), +}); +const { profile } = await res.json(); +console.log(profile.buckets.preferences); +``` + +Entries prefixed `[Summary]` are aggregated older context; `[Recent]` entries arrived since the last aggregation. Strip the prefixes for raw text, or keep them to signal recency. + ### `search()` - Semantic Search -Search across memories using semantic understanding, not just keywords. `client.search.memories()` and `client.search.documents()` still work (deprecated) but `client.search()` is the current, recommended call — Python keeps `client.search.memories()`. +Semantic search over memories, not keyword matching. In TypeScript the client's `search` is callable — `client.search({...})` hits `/v4/search`, the current endpoint. `client.search.memories()` is an alias for it; `client.search.documents()` and `client.search.execute()` are deprecated and hit the legacy `/v3/search`. In Python, `client.search` is not callable — use `client.search.memories()`. #### TypeScript ```typescript const response = await client.search({ q: string, // Required: search query containerTag?: string, // Optional: filter by container tag - limit?: number, // Optional: max results (default 10) - threshold?: number, // Optional: similarity threshold (0-1, default 0.5) - searchMode?: "memories" | "hybrid" | "documents", // Optional: "memories" (default), "hybrid" (memories + document chunks), or "documents" (chunks only) - filters?: FilterObject // Optional: advanced filtering + limit?: number, // Optional: max results + threshold?: number, // Optional: similarity floor (0-1) + searchMode?: "memories" | "hybrid" | "documents", // "memories" (default), "hybrid" (memories + chunks), "documents" (chunks only) + filters?: FilterObject, // Optional: metadata filters + include?: { // Optional: extra payload per result + chunks?: boolean, + documents?: boolean, + summaries?: boolean, + relatedMemories?: boolean, + forgottenMemories?: boolean + }, + rerank?: boolean, // Optional: cross-encoder re-ranking (higher precision, slower) + rewriteQuery?: boolean, // Optional: let the service rewrite q before searching + aggregate?: boolean // Optional: collapse near-duplicate memories }); // Returns: // { // results: Array<{ // id: string, -// memory?: string, // Memory content (for memory results) -// chunk?: string, // Chunk content (for chunk results in hybrid mode) -// metadata: object | null, +// memory?: string, // set on memory results +// chunk?: string, // set on chunk results (hybrid / documents mode) +// similarity: number, // the score — not `score` // updatedAt: string, -// similarity: number, -// version?: number | null +// metadata: object | null, +// version?: number | null, +// chunks?, documents?, context? // only when requested via `include` // }>, // total: number, -// timing: number // Search time in milliseconds +// timing: number // milliseconds // } ``` @@ -237,18 +281,15 @@ const response = await client.search({ response = client.search.memories( q=str, # Required: search query container_tag=str, # Optional: filter by container tag - threshold=float, # Optional: similarity threshold (0-1, default 0.5) - limit=int, # Optional: max results (default 50) + threshold=float, # Optional: similarity floor (0-1) + limit=int, # Optional: max results search_mode=str, # Optional: "memories" (default), "hybrid", or "documents" - filters=dict # Optional: advanced filtering + filters=dict, # Optional: metadata filters + rerank=bool, # Optional: cross-encoder re-ranking + rewrite_query=bool, # Optional: server-side query rewriting ) -# Returns dict: -# { -# "results": List[dict], # Array of search results -# "total": int, -# "timing": int # Search time in milliseconds -# } +# response.results, response.total, response.timing ``` #### Examples @@ -261,141 +302,209 @@ const response = await client.search({ limit: 10 }); -response.results.forEach(result => { - console.log(`Score: ${result.score}`); - console.log(`Content: ${result.content}`); -}); +for (const result of response.results) { + console.log(result.similarity, result.memory ?? result.chunk); +} ``` -**Hybrid search for RAG (semantic + keyword):** +**Hybrid search for RAG:** ```typescript const response = await client.search({ q: "authentication methods", containerTag: "docs", - searchMode: "hybrid", // Combines semantic and keyword search for better RAG accuracy + searchMode: "hybrid", // memories + document chunks threshold: 0.3, limit: 10 }); ``` -**Search with metadata filters:** +**High-precision search:** ```typescript const response = await client.search({ - q: "authentication methods", - containerTag: "docs", - threshold: 0.3, - filters: { - metadata: { - type: "tutorial", - category: "security" - } - } + q: "what did we decide about rate limiting?", + containerTag: "eng_notes", + rerank: true, // re-rank for precision + rewriteQuery: true // helps with terse or pronoun-heavy queries }); ``` -**Search within specific document:** +### `memories.forget()` - Forget a Memory + +Soft-deletes one memory. Identify it by `id`, or by exact `content` when you don't have the ID. + ```typescript -const response = await client.search({ - q: "rate limiting configuration", - containerTag: "specific_project" +const res = await client.memories.forget({ + containerTag: "user_123", // Required + id: "mem_abc123", // Either id... + // content: "John prefers dark mode", // ...or exact content + reason: "outdated information" // Optional, recorded as forgetReason }); +// { id: "mem_abc123", forgotten: true } +``` + +```python +res = client.memories.forget( + container_tag="user_123", + id="mem_abc123", + reason="outdated information", +) +``` + +### `memories.updateMemory()` - Correct a Memory + +Creates a new version that supersedes the old one, preserving history. Prefer this over forget-then-add when a fact merely changed. + +```typescript +const res = await client.memories.updateMemory({ + containerTag: "user_123", // Required + newContent: "John now prefers light mode", // Required + id: "mem_abc123", // Either id or exact content + metadata: { source: "chat" }, // Optional: inherits previous version if omitted + forgetAfter: "2026-12-01T00:00:00Z", // Optional: ISO expiry, null clears it + forgetReason: "temporary preference" // Optional +}); +// { id: "mem_xyz789", memory: "...", version: 2, parentMemoryId: "mem_abc123", rootMemoryId: "mem_abc123", ... } +``` + +```python +res = client.memories.update_memory( + container_tag="user_123", + new_content="John now prefers light mode", + id="mem_abc123", +) ``` ### `documents.list()` - List Documents -Retrieve stored documents with optional filtering and pagination. - -#### TypeScript ```typescript const docs = await client.documents.list({ - containerTag?: string, // Optional: filter by container - limit?: number, // Optional: number of results (default 20) - offset?: number, // Optional: pagination offset - status?: string // Optional: filter by processing status + containerTags?: string[], // Note: array, not a single tag + limit?: number | string, // Page size + page?: number | string, // 1-based + sort?: "createdAt" | "updatedAt", + order?: "asc" | "desc", + filters?: FilterObject, + includeContent?: boolean, + filepath?: string }); // Returns: // { -// documents: Array<{ -// id: string, -// content: string, -// status: string, -// metadata: object, -// createdAt: string -// }>, -// total: number +// memories: Array<{ id, title?, status, metadata, createdAt, updatedAt, ... }>, +// pagination: { currentPage, limit, totalItems, totalPages } // } ``` -#### Python ```python docs = client.documents.list( - container_tag=str, # Optional: filter by container - limit=int, # Optional: number of results (default 20) - offset=int, # Optional: pagination offset - status=str # Optional: filter by processing status + container_tags=["user_123"], + limit=50, + page=1, ) +# docs.memories, docs.pagination ``` -#### Examples - -**List all documents for a user:** -```typescript -const docs = await client.documents.list({ - containerTag: "user_123", - limit: 50 -}); - -docs.documents.forEach(doc => { - console.log(`${doc.id}: ${doc.status}`); -}); -``` +The response field is `memories` (documents with their extracted memories), not `documents`, and pagination is page-based — there is no `offset`. **Paginated listing:** ```typescript -const page1 = await client.documents.list({ limit: 20, offset: 0 }); -const page2 = await client.documents.list({ limit: 20, offset: 20 }); +const page1 = await client.documents.list({ containerTags: ["user_123"], limit: 20, page: 1 }); +const page2 = await client.documents.list({ containerTags: ["user_123"], limit: 20, page: 2 }); ``` -**Filter by status:** +**Check what is still processing:** ```typescript -const processing = await client.documents.list({ - containerTag: "project_abc", - status: "processing" -}); +const processing = await client.documents.listProcessing(); ``` -### `documents.delete()` - Delete Document +### `documents.get()` / `update()` / `delete()` -Remove a document and its associated memories. +IDs are positional arguments, not body fields. -#### TypeScript ```typescript -await client.documents.delete({ - docId: string // Required: document ID -}); +const doc = await client.documents.get("doc_abc123"); +await client.documents.update("doc_abc123", { metadata: { reviewed: true } }); +await client.documents.delete("doc_abc123"); // also deletes its memories +await client.documents.deleteBulk({ ids: ["doc_1", "doc_2"] }); ``` -#### Python ```python -client.documents.delete( - doc_id=str # Required: document ID -) +doc = client.documents.get("doc_abc123") +client.documents.delete("doc_abc123") ``` -#### Example +## Endpoints without SDK methods + +These are documented, supported endpoints that the generated SDKs don't cover yet. Call them with `fetch` (or `requests`) against the same base URL with the same bearer token. Full request/response shapes are in `api-reference.md`. + +| Endpoint | What it does | +|---|---| +| `POST /v4/memories` | Write memories directly, skipping document ingestion and extraction | +| `POST /v4/memories/forget-matching` | Agentic mass forget by query or ID list, with `dryRun` | +| `POST /v4/memories/list` | List memory entries with version history | +| `POST /v4/profile` with `include` / `buckets` | Bucketed profile reads (the SDK's `profile()` params omit these) | +| `POST /v4/profile/buckets` | List effective bucket definitions for a container tag | +| `POST /v4/conversations` | Ingest structured chat transcripts with append detection | +| `/v3/container-tags/*` | List, configure, delete, and merge container tags | +| `POST /v3/settings/suggest-buckets` | AI-suggested bucket definitions | +| `POST /v3/settings/reset` | Reset org settings to defaults | +| `GET /v3/documents/{id}/chunks`, `/file-url` | Raw chunks, signed file URL | + +A small helper keeps call sites clean: ```typescript -await client.documents.delete({ - docId: "doc_abc123" +const sm = async (path: string, body: unknown) => { + const res = await fetch(`https://api.supermemory.ai${path}`, { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(`supermemory ${path} failed: ${res.status} ${await res.text()}`); + return res.json(); +}; + +// Write memories directly +await sm("/v4/memories", { + containerTag: "user_123", + memories: [ + { content: "John prefers dark mode" }, + { content: "John is from Seattle", isStatic: true }, + ], +}); + +// Mass forget — preview, then apply exactly what you reviewed +const preview = await sm("/v4/memories/forget-matching", { + containerTag: "user_123", + query: "forget everything about Project Titan", + dryRun: true, +}); +const applied = await sm("/v4/memories/forget-matching", { + containerTag: "user_123", + ids: preview.candidates.map((c: { id: string }) => c.id), + reason: "project cancelled", +}); + +// Ingest a conversation +await sm("/v4/conversations", { + conversationId: "conv_123", + containerTags: ["user_123"], + messages: [ + { role: "user", content: "I switched my editor to Zed" }, + { role: "assistant", content: "Noted — how are you finding it?" }, + ], }); ``` +Always `dryRun` a `forget-matching` call before applying it, and prefer applying with the `ids` from the preview: applying with a `query` re-runs the semantic match, which can drift if the container changed in between. + ## Advanced Features ### Metadata Filtering -Add rich metadata to enable advanced filtering: +Add rich metadata at write time, then filter at read time. Filter conditions must be wrapped in `AND` or `OR` — a bare `{ metadata: {...} }` object is rejected. ```typescript await client.add({ @@ -409,23 +518,22 @@ await client.add({ } }); -// Search with metadata filters const results = await client.search({ q: "phone reviews", containerTag: "reviews", filters: { - metadata: { - rating: { $gte: 4.0 }, // Rating >= 4.0 - verified: true, - tags: { $contains: "apple" } - } + AND: [ + { filterType: "numeric", key: "rating", value: "4", numericOperator: ">=" }, + { key: "verified", value: "true" }, + { filterType: "array_contains", key: "tags", value: "apple" } + ] } }); ``` -### Entity Context for Better Extraction +Condition types: string equality (default), `string_contains`, `numeric` (with `numericOperator`), and `array_contains`. `AND`/`OR` nest freely, and any condition accepts `negate` and `ignoreCase`. Numeric values are passed as strings. -Provide context to guide memory extraction: +### Entity Context for Better Extraction ```typescript await client.add({ @@ -435,7 +543,7 @@ await client.add({ }); ``` -The `entityContext` helps Supermemory understand what type of information to extract and prioritize. +`entityContext` (max 1,500 chars) guides what gets extracted and how it's classified. Set it per-call as above, or once per container tag via `PATCH /v3/container-tags/{tag}`. For guidance that should apply org-wide, use `filterPrompt` in `PATCH /v3/settings` instead. ### Container Tag Patterns @@ -468,12 +576,16 @@ await client.add({ }); ``` +If one entity ends up split across two tags (an anonymous session that later signs in), merge them with `POST /v3/container-tags/merge` rather than re-ingesting. + ## Integration with AI Frameworks +For Vercel AI SDK, OpenAI, Mastra, and VoltAgent there is a purpose-built package — `@supermemory/tools` — with ready-made tools and middleware that inject memory automatically. Reach for it before hand-rolling the calls below. + ### Vercel AI SDK ```typescript -import { Supermemory } from 'supermemory'; +import Supermemory from 'supermemory'; import { openai } from '@ai-sdk/openai'; import { generateText } from 'ai'; @@ -481,19 +593,19 @@ const memory = new Supermemory(); async function chat(userId: string, message: string) { // 1. Get context - const context = await memory.profile({ - containerTag: userId, - q: message - }); + const { profile } = await memory.profile({ containerTag: userId, q: message }); // 2. Generate response with context const { text } = await generateText({ - model: openai('gpt-4'), - system: `User Profile: ${context.profile}\n\nRelevant Context:\n${context.memories.map(m => m.content).join('\n')}`, + model: openai('gpt-5'), + system: [ + `Long-term facts:\n${(profile.static ?? []).map(f => `- ${f}`).join('\n')}`, + `Recent context:\n${(profile.dynamic ?? []).map(f => `- ${f}`).join('\n')}` + ].join('\n\n'), prompt: message }); - // 3. Store conversation + // 3. Store the exchange await memory.add({ content: `User: ${message}\nAssistant: ${text}`, containerTag: userId @@ -506,29 +618,27 @@ async function chat(userId: string, message: string) { ### LangChain ```typescript -import { Supermemory } from 'supermemory'; +import Supermemory from 'supermemory'; import { ChatOpenAI } from '@langchain/openai'; import { HumanMessage, SystemMessage } from '@langchain/core/messages'; const memory = new Supermemory(); -const llm = new ChatOpenAI({ model: 'gpt-4' }); +const llm = new ChatOpenAI({ model: 'gpt-5' }); async function chatWithMemory(userId: string, userMessage: string) { - // Retrieve context - const context = await memory.profile({ - containerTag: userId, - q: userMessage - }); + const { profile } = await memory.profile({ containerTag: userId, q: userMessage }); - // Create messages with context const messages = [ - new SystemMessage(`Context: ${JSON.stringify(context)}`), + new SystemMessage( + `What you know about this user:\n${[...(profile.static ?? []), ...(profile.dynamic ?? [])] + .map(f => `- ${f}`) + .join('\n')}` + ), new HumanMessage(userMessage) ]; const response = await llm.invoke(messages); - // Store interaction await memory.add({ content: `${userMessage}\n${response.content}`, containerTag: userId @@ -542,25 +652,21 @@ async function chatWithMemory(userId: string, userMessage: string) { ```python from supermemory import Supermemory -from crewai import Agent, Task, Crew +from crewai import Agent memory = Supermemory() def create_memory_enhanced_agent(user_id: str): - # Get user context - context = memory.profile( - container_tag=user_id, - query="user preferences and history" - ) + response = memory.profile(container_tag=user_id) + facts = "\n".join(f"- {fact}" for fact in response.profile.static or []) + recent = "\n".join(f"- {fact}" for fact in response.profile.dynamic or []) - agent = Agent( + return Agent( role="Personal Assistant", goal="Help the user with personalized assistance", - backstory=f"User Context: {context['profile']}\n\nRecent interactions:\n{context['memories']}", - verbose=True + backstory=f"What you know about this user:\n{facts}\n\nRecent context:\n{recent}", + verbose=True, ) - - return agent ``` ## Best Practices @@ -602,23 +708,40 @@ await client.add({ ``` ### 4. Appropriate Thresholds -Start with default (0.5) and adjust based on results: +Start with the default and adjust based on real queries: - **0.3-0.5**: Broader recall, good for discovery - **0.5-0.7**: Balanced precision and recall - **0.7-1.0**: High precision, fewer but more relevant results -### 5. Error Handling -Always handle errors gracefully: +### 5. Correct Memories Instead of Rewriting Them ```typescript +// Fact changed → new version, history preserved +await client.memories.updateMemory({ + containerTag: userId, + id: memoryId, + newContent: "Now prefers light mode" +}); + +// Should never have been stored → forget it +await client.memories.forget({ containerTag: userId, id: memoryId, reason: "user asked" }); +``` + +### 6. Error Handling +The SDKs raise typed errors: +```typescript +import { APIError, RateLimitError, AuthenticationError } from 'supermemory'; + try { await client.add({ content: "...", containerTag: "user_123" }); } catch (error) { - if (error.status === 401) { + if (error instanceof AuthenticationError) { console.error("Invalid API key"); - } else if (error.status === 429) { + } else if (error instanceof RateLimitError) { console.error("Rate limit exceeded"); + } else if (error instanceof APIError) { + console.error(error.status, error.message); } else { - console.error("Failed to add memory:", error.message); + throw error; } } ``` @@ -626,27 +749,33 @@ try { ## Naming Conventions ### TypeScript (camelCase) -- `containerTag` +- `containerTag` / `containerTags` - `entityContext` - `customId` +- `newContent` +- `searchMode` - `threshold` -- `docId` - `q` ### Python (snake_case) -- `container_tag` +- `container_tag` / `container_tags` - `entity_context` - `custom_id` +- `new_content` +- `search_mode` - `threshold` -- `doc_id` +- `q` + +Python responses are models, so read them by attribute (`response.profile.static`), not by key. ## Performance Tips -1. **Batch Operations**: Add multiple documents in quick succession if needed -2. **Async/Await**: Always use async operations to avoid blocking -3. **Pagination**: Use `limit` and `offset` for large result sets -4. **Caching**: Cache profile() results for short periods if making multiple calls -5. **Processing Time**: Allow 1-2 minutes for PDFs, 5-10 minutes for videos +1. **Batch Operations**: Use `documents.batchAdd()` rather than a loop of `add()` calls +2. **Async/Await**: Always use async operations to avoid blocking; Python has `AsyncSupermemory` +3. **Pagination**: `documents.list()` and `/v4/memories/list` are page-based (`limit` + `page`) +4. **Caching**: Cache `profile()` results for short periods if making multiple calls per turn +5. **Cost of quality knobs**: `rerank` and `rewriteQuery` improve results but add latency — enable them per-query, not globally +6. **Processing Time**: Allow 1-2 minutes for PDFs, 5-10 minutes for videos ## Support diff --git a/skills/supermemory/references/use-cases.md b/skills/supermemory/references/use-cases.md index db80e0d6..602a2929 100644 --- a/skills/supermemory/references/use-cases.md +++ b/skills/supermemory/references/use-cases.md @@ -97,8 +97,8 @@ def chat(user_id: str, message: str) -> str: ) # 2. Build system prompt - static_facts = "\n".join(f"- {fact}" for fact in response['profile']['static']) - dynamic_facts = "\n".join(f"- {fact}" for fact in response['profile']['dynamic']) + static_facts = "\n".join(f"- {fact}" for fact in response.profile.static or []) + dynamic_facts = "\n".join(f"- {fact}" for fact in response.profile.dynamic or []) system_prompt = f""" You are a helpful assistant with perfect memory. @@ -277,21 +277,21 @@ async function indexDocumentation() { // 2. Search documentation async function searchDocs(query: string, category?: string) { const filters = category ? { - metadata: { category } + AND: [{ key: 'category', value: category }] } : undefined; - const results = await memory.search.memories({ + const response = await memory.search.memories({ q: query, containerTag: 'documentation', - searchMode: 'hybrid', // Use hybrid search for better RAG accuracy + searchMode: 'hybrid', // Memories + document chunks, best for RAG threshold: 0.3, limit: 10, filters }); - return results.map(r => ({ - content: r.content, - relevance: r.score, + return response.results.map(r => ({ + content: r.memory ?? r.chunk, + relevance: r.similarity, metadata: r.metadata })); } @@ -545,10 +545,10 @@ async function reviewCode(projectId: string, code: string, fileName: string) { You are a code review assistant familiar with this codebase. Similar Code Patterns: -${similarCode.map(c => c.content).slice(0, 3).join('\n\n---\n\n')} +${similarCode.results.slice(0, 3).map(c => c.memory ?? c.chunk).join('\n\n---\n\n')} Past Review Patterns: -${pastReviews.map(p => p.content).slice(0, 3).join('\n\n---\n\n')} +${pastReviews.results.slice(0, 3).map(p => p.memory ?? p.chunk).join('\n\n---\n\n')} Provide a thoughtful code review, considering existing patterns and past feedback. `, @@ -769,14 +769,14 @@ async function search(orgId: string, userId: string, query: string, includeShare ? [tags.user, tags.shared] // User + org shared : [tags.user]; // User only - const results = await memory.search.memories({ + const response = await memory.search.memories({ q: query, - containerTag: containerTags[0], // Use first tag + containerTag: containerTags[0], // One tag per search — run one call per tag to span several threshold: 0.3, limit: 10 }); - return results; + return response.results; } // Usage @@ -865,7 +865,7 @@ async function addResearchNote(userId: string, note: string, relatedPapers: stri } async function findRelatedResearch(userId: string, topic: string) { - const results = await memory.search.memories({ + const response = await memory.search.memories({ q: topic, containerTag: `${userId}_research`, threshold: 0.3, @@ -873,8 +873,8 @@ async function findRelatedResearch(userId: string, topic: string) { }); // Group by type - const papers = results.filter(r => r.metadata?.type === 'paper'); - const notes = results.filter(r => r.metadata?.type === 'note'); + const papers = response.results.filter(r => r.metadata?.type === 'paper'); + const notes = response.results.filter(r => r.metadata?.type === 'note'); return { papers, notes }; } @@ -884,9 +884,9 @@ async function synthesizeInsights(userId: string, research_question: string) { const context = [ '=== Related Papers ===', - ...related.papers.map(p => p.content), + ...related.papers.map(p => p.memory ?? p.chunk), '\n=== Your Notes ===', - ...related.notes.map(n => n.content) + ...related.notes.map(n => n.memory ?? n.chunk) ].join('\n\n'); const { text } = await generateText({ @@ -997,7 +997,10 @@ const results = await memory.search.memories({ q: 'billing issues', containerTag: 'user_123', filters: { - metadata: { priority: 'high', type: 'conversation' } + AND: [ + { key: 'priority', value: 'high' }, + { key: 'type', value: 'conversation' } + ] } }); ```