mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-16 20:14:02 +00:00
feat(tools): mass forget + bucket tools, and refresh the supermemory skill
@supermemory/tools had no tool for POST /v4/memories/forget-matching or bucketed profile reads, so agents could forget one memory at a time and read the whole profile or nothing. Adds both, mirrored across the AI SDK and OpenAI surfaces, over fetch — the generated SDK has no forget-matching method and profile()'s params can't express include/buckets. memoryForgetMatching defaults dryRun to true even though the API defaults to false: a model shouldn't be able to bulk-delete in one turn. The Claude skill had drifted further. profile() was called with instead of , the profile response shape was invented, search responses were treated as arrays, Python used dict access on Pydantic models, and filters used a form the API rejects. The reference documented 3 endpoints and never mentioned forgetting at all. Rewrote it against the route schemas in mono, with a table marking which endpoints have SDK methods and which need HTTP.
This commit is contained in:
parent
59b148e5b2
commit
dfddc2ad06
14 changed files with 1637 additions and 782 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string[]>
|
||||
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<MemoryForgetMatchingResult> {
|
||||
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<ProfileBucketsResult> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ForgetMatchingResponse> {
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
52
packages/tools/src/shared/profile-buckets.ts
Normal file
52
packages/tools/src/shared/profile-buckets.ts
Normal file
|
|
@ -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<string, string[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ProfileBucketsResponse> {
|
||||
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<string, string[]> }
|
||||
}
|
||||
|
||||
return { buckets: body.profile?.buckets ?? {} }
|
||||
}
|
||||
|
|
@ -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<string, string[]> }
|
||||
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, any> // 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<string, string | number | boolean | string[]>,
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue