diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 6be467ff7..a50b38200 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -104,7 +104,11 @@ { "group": "Manage Content", "icon": "folder-cog", - "pages": ["document-operations", "memory-operations"] + "pages": [ + "document-operations", + "memory-operations", + "memory-review" + ] }, "overview/use-cases" ] diff --git a/apps/docs/memory-operations.mdx b/apps/docs/memory-operations.mdx index baf3b36ea..6f3906d51 100644 --- a/apps/docs/memory-operations.mdx +++ b/apps/docs/memory-operations.mdx @@ -201,6 +201,7 @@ Update a memory by creating a new version. The original is preserved with `isLat ## Next Steps +- [Review Inferred Memories](/memory-review) — Approve or decline low-confidence memories - [Document Operations](/document-operations) — Manage documents (SDK supported) - [Search](/search) — Query your memories - [Ingesting Content](/add-memories) — Add new content diff --git a/apps/docs/memory-review.mdx b/apps/docs/memory-review.mdx new file mode 100644 index 000000000..84fda2fa2 --- /dev/null +++ b/apps/docs/memory-review.mdx @@ -0,0 +1,288 @@ +--- +title: "Review Inferred Memories" +sidebarTitle: "Memory Review" +description: "List and act on low-confidence inferred memories — approve, decline, or undo" +icon: "list-checks" +--- + +Supermemory's graph automatically **derives** new facts from patterns across your +existing memories (see [Graph Memory](/concepts/graph-memory)). These derived facts +are guesses — the engine wasn't told them directly — so they are flagged as +**inferred** (`isInference: true`) and **down-weighted in search** until confirmed. + +These two endpoints let you build a review experience on top of that queue: list the +inferred memories awaiting review, then **approve**, **decline**, or **undo** a +decision on each one. + + +These endpoints are scoped to a single [container tag](/concepts/container-tags) +(space), under `/v3/container-tags/{containerTag}`. + + +## How review affects ranking + +While a memory is unreviewed and inferred it is down-weighted in search, so the +engine's guesses rank below facts you stated explicitly. Reviewing it resolves that +either way: + +| Action | Result | Effect on search | +|--------|--------|------------------| +| **Approve** | `isInference` cleared | Ranks like a stated fact — no longer down-weighted | +| **Decline** | `isForgotten` set | Removed from search entirely — a rejected guess is forgotten | +| **Undo** | back to unreviewed | Returns to the queue; inferred and down-weighted again | + +A reviewed memory is stamped with `reviewStatus` in its metadata so it drops out of the +review queue (declined memories also leave search, since they're forgotten). **Undo** +clears that stamp — and un-forgets a declined memory — bringing it back. + +--- + +## List Inferred Memories + +Return the inferred memories for a container tag that are still awaiting review (the +review queue). Reviewed memories are excluded. + +``` +GET /v3/container-tags/{containerTag}/inferred +``` + + + + ```typescript + const res = await fetch( + "https://api.supermemory.ai/v3/container-tags/user_123/inferred", + { headers: { "Authorization": `Bearer ${API_KEY}` } } + ); + + const { memories, total } = await res.json(); + ``` + + + ```bash + curl "https://api.supermemory.ai/v3/container-tags/user_123/inferred" \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" + ``` + + + +### Path parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `containerTag` | string | The container tag / space to read the review queue for | + +### Response + +```json +{ + "memories": [ + { + "id": "mem_abc123", + "memory": "Alex likely works on Stripe's core payments product", + "parentCount": 3, + "createdAt": "2025-01-15T10:30:00.000Z", + "updatedAt": "2025-01-15T10:30:00.000Z", + "metadata": { "source": "derive" } + } + ], + "total": 1 +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `memories[].id` | string | Memory entry ID — pass to the review endpoint | +| `memories[].memory` | string | The inferred memory text | +| `memories[].parentCount` | number | How many source memories this was derived from. Higher = stronger signal | +| `memories[].createdAt` | string | ISO 8601 timestamp | +| `memories[].updatedAt` | string | ISO 8601 timestamp | +| `memories[].metadata` | object \| null | Arbitrary metadata stored on the memory | +| `total` | number | Count of unreviewed inferred memories returned | + + +The queue returns up to **50** memories, ordered by `parentCount` descending (most +strongly supported first), then by `createdAt` descending. It excludes anything that +is forgotten, expired, or already reviewed. An unknown or empty container tag returns +`{ "memories": [], "total": 0 }`. + + +--- + +## Review an Inferred Memory + +Record a decision on a single inferred memory. + +``` +POST /v3/container-tags/{containerTag}/inferred/{memoryId}/review +``` + + + + ```typescript + const res = await fetch( + "https://api.supermemory.ai/v3/container-tags/user_123/inferred/mem_abc123/review", + { + method: "POST", + headers: { + "Authorization": `Bearer ${API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ action: "approve" }) + } + ); + + const result = await res.json(); + // { id: "mem_abc123", isInference: false, isForgotten: false, reviewStatus: "approved" } + ``` + + + ```bash + curl -X POST \ + "https://api.supermemory.ai/v3/container-tags/user_123/inferred/mem_abc123/review" \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"action": "approve"}' + ``` + + + +### Path parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `containerTag` | string | The container tag / space the memory belongs to | +| `memoryId` | string | The memory entry ID from the list endpoint | + +### Body parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `action` | string | yes | One of `approve`, `decline`, or `undo` | + + +The reject action is named **`decline`**. There is no `reject` value. + + +**Action semantics:** + +- **`approve`** — Promote the memory: clears `isInference`, so it ranks like a stated + fact instead of a down-weighted guess. Stamps `reviewStatus: "approved"`. +- **`decline`** — Reject the suggestion: the memory is **forgotten** + (`isForgotten: true`) and stamped `reviewStatus: "declined"`, so it leaves both + search and the review queue. +- **`undo`** — Revert a prior `approve`/`decline` back to the unreviewed inferred + state: restores `isInference: true`, un-forgets the memory (`isForgotten: false`), + and clears the review stamp, so it returns to the queue. + +### Response + +```json +{ + "id": "mem_abc123", + "isInference": false, + "isForgotten": false, + "reviewStatus": "approved" +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | The reviewed memory ID | +| `isInference` | boolean | `false` after approve; `true` after decline or undo | +| `isForgotten` | boolean | `true` after decline (the memory is forgotten); `false` otherwise | +| `reviewStatus` | `"approved"` \| `"declined"` \| `null` | The new status; `null` after an undo | + +### Errors + +| Status | When | +|--------|------| +| `401` | Missing or invalid authentication | +| `404` | The container tag or memory was not found in your organization | +| `409` | The memory isn't reviewable for this action — it's not an inferred memory, or there's no prior review to undo | + +--- + +## Building a review experience + +The endpoints are designed for an optimistic, one-at-a-time review UI (swipe to keep / +decline, with undo). A typical client fetches the queue once, then pops each card off +locally as the user decides — `undo` re-adds it. + + +```typescript +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +const BASE = "https://api.supermemory.ai/v3"; +const key = (tag: string) => ["inferred-memories", tag] as const; + +export type InferredMemory = { + id: string; + memory: string; + parentCount: number; + createdAt: string; + updatedAt: string; + metadata: Record | null; +}; + +export type ReviewAction = "approve" | "decline" | "undo"; + +export function useInferredMemories(containerTag: string) { + return useQuery({ + queryKey: key(containerTag), + queryFn: async (): Promise => { + const res = await fetch(`${BASE}/container-tags/${containerTag}/inferred`, { + headers: { Authorization: `Bearer ${API_KEY}` }, + }); + if (!res.ok) throw new Error("Failed to load review queue"); + const data = await res.json(); + return data.memories ?? []; + }, + staleTime: 60_000, + }); +} + +export function useReviewInferredMemory(containerTag: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (vars: { memoryId: string; action: ReviewAction }) => { + const res = await fetch( + `${BASE}/container-tags/${containerTag}/inferred/${vars.memoryId}/review`, + { + method: "POST", + headers: { + Authorization: `Bearer ${API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ action: vars.action }), + }, + ); + if (!res.ok) throw new Error("Review failed"); + return res.json(); + }, + onSuccess: (_data, { memoryId, action }) => { + // approve/decline remove the card; undo brings it back, so refetch. + if (action === "undo") { + queryClient.invalidateQueries({ queryKey: key(containerTag) }); + return; + } + queryClient.setQueryData(key(containerTag), (prev) => + prev?.filter((m) => m.id !== memoryId), + ); + }, + }); +} +``` + + + +There is no separate "skip" action. A swipe-to-skip is purely client-side — don't send +a request and the memory simply stays in the queue for a later session. + + +--- + +## Next Steps + +- [Graph Memory](/concepts/graph-memory) — How inferred (`derive`) memories are created +- [Memory Operations](/memory-operations) — Create, forget, and update memories +- [Search](/search) — How inferred memories are ranked in results diff --git a/apps/docs/user-profiles.mdx b/apps/docs/user-profiles.mdx index e2b3ae461..cb5c7cd18 100644 --- a/apps/docs/user-profiles.mdx +++ b/apps/docs/user-profiles.mdx @@ -121,6 +121,9 @@ Get profile and search results in one call by adding the `q` parameter: | `containerTag` | string | Yes | User/project identifier | | `q` | string | No | Search query (includes search results in response) | | `threshold` | 0-1 | No | Filter search results by relevance score | +| `filters` | object | No | Metadata filters applied to profile and search results | +| `include` | string[] | No | Sections to return — any of `"static"`, `"dynamic"`, `"buckets"`. Omit to return all | +| `buckets` | string[] | No | Restrict the `buckets` section to specific keys. Omit for all configured buckets | --- @@ -180,6 +183,142 @@ ${result.searchResults?.results.map(m => m.memory).join('\n') || 'None'} --- +## Profile Buckets + +Buckets are **custom topical categories** for a profile — an axis that sits alongside +`static` and `dynamic`. Where static/dynamic split facts by how long-lived they are, +buckets group them by subject (e.g. `preferences`, `goals`, `work`). As content is +ingested, a classifier assigns each memory to the buckets it matches, so you can pull +just the slice of context a given surface needs. + +Every org starts with a built-in `preferences` bucket. You can define your own at the +organization or space level in your console settings; space-level buckets are +**add-only** — a container tag inherits all org buckets and may add more, but cannot +disable them. + +### Requesting buckets + +Pass `include: ["buckets"]` to return bucket-organized memories, and optionally +`buckets` to limit the response to specific keys. `include` also lets you skip +sections you don't need — `["buckets"]` alone omits `static` and `dynamic`. + + + + ```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"], + buckets: ["preferences", "goals"] // optional — omit for all buckets + }) + }); + + const { profile } = await res.json(); + console.log(profile.buckets.preferences); + console.log(profile.buckets.goals); + ``` + + + ```bash + curl -X POST "https://api.supermemory.ai/v4/profile" \ + -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "containerTag": "user_123", + "include": ["buckets"], + "buckets": ["preferences", "goals"] + }' + ``` + + + +**Response:** +```json +{ + "profile": { + "buckets": { + "preferences": [ + "[Summary] Prefers concise, technical answers and dark-mode tooling", + "[Recent] Switched their editor to Zed" + ], + "goals": [ + "[Recent] Wants to ship the billing revamp this quarter" + ] + } + } +} +``` + + +**`[Recent]` and `[Summary]` labels.** To keep profiles dense, an entity's older +memories are periodically aggregated into a short synthesis. Entries prefixed +`[Summary]` are that aggregated context; entries prefixed `[Recent]` were ingested +since the last aggregation and aren't summarized yet. The `dynamic` section uses the +same `[Recent]` prefix (plus a `[YYYY-MM-DD]` date). Strip the prefixes if you only +want raw text, or keep them to signal recency to your model. + + +### List bucket definitions + +To see which buckets are configured for a container tag (org buckets merged with any +space-level additions), call `/v4/profile/buckets`: + + + + ```typescript + const res = await fetch("https://api.supermemory.ai/v4/profile/buckets", { + method: "POST", + headers: { + "Authorization": `Bearer ${API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ containerTag: "user_123" }) + }); + + const { buckets } = await res.json(); + // [{ key: "preferences", description: "..." }, ...] + ``` + + + ```bash + 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"}' + ``` + + + +**Response:** +```json +{ + "buckets": [ + { + "key": "preferences", + "description": "Explicit first-person preferences the person directly stated." + } + ] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `buckets[].key` | string | Stable slug, also stored on each memory. Lowercase alphanumeric with `-`/`_`, 1–64 chars | +| `buckets[].description` | string | What belongs in the bucket — guides the ingestion classifier | + + +Bucket descriptions steer classification. A precise description ("Explicit +first-person preferences only — exclude inferred traits") yields cleaner buckets than +a vague one. `static` and `dynamic` are reserved and can't be used as bucket keys. + + +--- + ## Framework Examples @@ -249,8 +388,9 @@ ${result.searchResults?.results.map(m => m.memory).join('\n') || 'None'} ```typescript interface ProfileResponse { profile: { - static: string[]; // Long-term facts - dynamic: string[]; // Recent context + static?: string[]; // Long-term facts + dynamic?: string[]; // Recent context + buckets?: Record; // Topical buckets, keyed by bucket key }; searchResults?: { // Only if q parameter provided results: SearchResult[];