mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-01 02:01:40 +00:00
fix: fallback memory graph to POST /v3/documents/list on 404
Fixes #1672 - self-hosted supermemory-server 0.0.8 returns 404 for POST /v3/documents/documents. All graph/context callers now try the rich endpoint first, then fallback to documented POST /v3/documents/list and normalize { memories } to { documents }. - apps/memory-graph-playground/src/app/api/graph/route.ts: fallback + normalize - apps/mcp/src/server/client/index.ts: getDocuments fallback + map to DocumentsApiResponse - apps/sdk-playground/src/lib/context-api.ts + python/server.py: same fallback for container context - apps/memory-graph-playground/src/app/page.tsx: persist API key in localStorage so Bearer auth is sent on non-localhost hosts - apps/docs/integrations/memory-graph.mdx: update examples to /list with normalization
This commit is contained in:
parent
4d8a4ebfdd
commit
91d7f07c0f
6 changed files with 278 additions and 82 deletions
|
|
@ -70,21 +70,42 @@ Create an API route to fetch documents from Supermemory:
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
const response = await fetch('https://api.supermemory.ai/v3/documents/documents', {
|
||||
const body = JSON.stringify({
|
||||
page: 1,
|
||||
limit: 500,
|
||||
sort: 'createdAt',
|
||||
order: 'desc',
|
||||
});
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
|
||||
};
|
||||
|
||||
let response = await fetch('https://api.supermemory.ai/v3/documents/documents', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page: 1,
|
||||
limit: 500,
|
||||
sort: 'createdAt',
|
||||
order: 'desc',
|
||||
}),
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
// Fallback for servers without the rich graph endpoint (e.g. self-hosted
|
||||
// supermemory-server returns 404 for /documents/documents).
|
||||
// POST /v3/documents/list exists on both cloud and self-hosted.
|
||||
if (response.status === 404) {
|
||||
response = await fetch('https://api.supermemory.ai/v3/documents/list', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Normalize /list ({ memories }) to graph shape ({ documents }).
|
||||
if (!data.documents && Array.isArray(data.memories)) {
|
||||
return NextResponse.json({
|
||||
documents: data.memories.map((m) => ({ ...m, memories: [], memoryEntries: [] })),
|
||||
pagination: data.pagination,
|
||||
});
|
||||
}
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
```
|
||||
|
|
@ -94,32 +115,50 @@ export async function GET() {
|
|||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const response = await fetch('https://api.supermemory.ai/v3/documents/documents', {
|
||||
const body = JSON.stringify({ page: 1, limit: 500, sort: 'createdAt', order: 'desc' });
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
|
||||
};
|
||||
|
||||
let response = await fetch('https://api.supermemory.ai/v3/documents/list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({ page: 1, limit: 500, sort: 'createdAt', order: 'desc' }),
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.documents && Array.isArray(data.memories)) {
|
||||
return res.json({
|
||||
documents: data.memories.map((m) => ({ ...m, memories: [], memoryEntries: [] })),
|
||||
pagination: data.pagination,
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
}
|
||||
```
|
||||
|
||||
```javascript Express
|
||||
app.get('/api/graph', async (req, res) => {
|
||||
const response = await fetch('https://api.supermemory.ai/v3/documents/documents', {
|
||||
const body = JSON.stringify({ page: 1, limit: 500, sort: 'createdAt', order: 'desc' });
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
|
||||
};
|
||||
|
||||
let response = await fetch('https://api.supermemory.ai/v3/documents/list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({ page: 1, limit: 500, sort: 'createdAt', order: 'desc' }),
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.documents && Array.isArray(data.memories)) {
|
||||
return res.json({
|
||||
documents: data.memories.map((m) => ({ ...m, memories: [], memoryEntries: [] })),
|
||||
pagination: data.pagination,
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -337,22 +337,72 @@ export class SupermemoryClient {
|
|||
): Promise<DocumentsApiResponse> {
|
||||
try {
|
||||
const signal = options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
||||
const response = await fetch(`${this.apiUrl}/v3/documents/documents`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.bearerToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-sm-source": MCP_SOURCE,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page,
|
||||
limit,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
containerTags,
|
||||
}),
|
||||
signal,
|
||||
const headers = {
|
||||
Authorization: `Bearer ${this.bearerToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-sm-source": MCP_SOURCE,
|
||||
}
|
||||
const body = JSON.stringify({
|
||||
page,
|
||||
limit,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
containerTags,
|
||||
})
|
||||
|
||||
const postDocuments = async (path: string) =>
|
||||
fetch(`${this.apiUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
signal,
|
||||
})
|
||||
|
||||
let response = await postDocuments("/v3/documents/documents")
|
||||
|
||||
// Fallback for servers without the rich graph endpoint (see #1672):
|
||||
// POST /v3/documents/list exists on both cloud and self-hosted
|
||||
// (supermemory-server 0.0.8 returns 404 for /documents/documents).
|
||||
if (response.status === 404) {
|
||||
response = await postDocuments("/v3/documents/list")
|
||||
if (!response.ok) {
|
||||
const message = extractApiErrorMessage(await response.text())
|
||||
throw Object.assign(new Error(message ?? ""), {
|
||||
status: response.status,
|
||||
})
|
||||
}
|
||||
const listData = (await response.json()) as {
|
||||
memories?: Array<Record<string, unknown>>
|
||||
pagination?: DocumentsApiResponse["pagination"]
|
||||
}
|
||||
const memories = Array.isArray(listData.memories)
|
||||
? listData.memories
|
||||
: []
|
||||
return documentsApiResponseSchema.parse({
|
||||
documents: memories.map((mem) => ({
|
||||
id: String(mem.id ?? ""),
|
||||
title: (mem.title as string | null | undefined) ?? null,
|
||||
summary: (mem.summary as string | null | undefined) ?? null,
|
||||
type: (mem.type as string | undefined) ?? "unknown",
|
||||
createdAt: String(
|
||||
(mem.createdAt as string | undefined) ??
|
||||
new Date(0).toISOString(),
|
||||
),
|
||||
updatedAt: String(
|
||||
(mem.updatedAt as string | undefined) ??
|
||||
new Date(0).toISOString(),
|
||||
),
|
||||
memoryEntries: [],
|
||||
})),
|
||||
pagination: listData.pagination ?? {
|
||||
currentPage: page,
|
||||
limit,
|
||||
totalItems: memories.length,
|
||||
totalPages: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = extractApiErrorMessage(await response.text())
|
||||
throw Object.assign(new Error(message ?? ""), {
|
||||
|
|
|
|||
|
|
@ -21,27 +21,39 @@ export async function POST(request: Request) {
|
|||
)
|
||||
}
|
||||
|
||||
const graphUrl = new URL(
|
||||
"/v3/documents/documents",
|
||||
SUPERMEMORY_API_BASE_URL,
|
||||
)
|
||||
const requestBody = {
|
||||
page,
|
||||
limit,
|
||||
sort,
|
||||
order,
|
||||
...(Array.isArray(containerTags) && containerTags.length > 0
|
||||
? { containerTags }
|
||||
: {}),
|
||||
}
|
||||
|
||||
const response = await fetch(graphUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
page,
|
||||
limit,
|
||||
sort,
|
||||
order,
|
||||
...(Array.isArray(containerTags) && containerTags.length > 0
|
||||
? { containerTags }
|
||||
: {}),
|
||||
}),
|
||||
})
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
|
||||
// Primary: documents with memories for rich graph rendering.
|
||||
// Fallback: POST /v3/documents/list (see #1672) — self-hosted
|
||||
// servers (e.g. supermemory-server 0.0.8) return 404 for
|
||||
// /v3/documents/documents, so normalize { memories } to { documents }.
|
||||
async function postDocuments(path: string) {
|
||||
const url = new URL(path, SUPERMEMORY_API_BASE_URL)
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
}
|
||||
|
||||
let response = await postDocuments("/v3/documents/documents")
|
||||
|
||||
if (response.status === 404) {
|
||||
response = await postDocuments("/v3/documents/list")
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
|
|
@ -52,6 +64,39 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Normalize POST /v3/documents/list ({ memories }) to the graph
|
||||
// shape ({ documents }) so the UI works against both cloud and
|
||||
// self-hosted servers.
|
||||
if (Array.isArray((data as { documents?: unknown })?.documents)) {
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
|
||||
const memories = (data as { memories?: Array<Record<string, unknown>> })
|
||||
?.memories
|
||||
if (Array.isArray(memories)) {
|
||||
return NextResponse.json({
|
||||
documents: memories.map((mem) => ({
|
||||
id: mem.id,
|
||||
title: mem.title ?? null,
|
||||
summary: mem.summary ?? null,
|
||||
documentType: (mem.type as string | undefined) ?? "unknown",
|
||||
type: (mem.type as string | undefined) ?? "unknown",
|
||||
containerTags: mem.containerTags ?? [],
|
||||
createdAt: mem.createdAt,
|
||||
updatedAt: mem.updatedAt,
|
||||
memories: [],
|
||||
memoryEntries: [],
|
||||
})),
|
||||
pagination: (data as { pagination?: unknown }).pagination ?? {
|
||||
currentPage: page,
|
||||
limit,
|
||||
totalItems: memories.length,
|
||||
totalPages: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error("Graph API error:", error)
|
||||
|
|
|
|||
|
|
@ -113,7 +113,14 @@ function toGraphDocuments(docs: PlaygroundApiDocument[]): GraphApiDocument[] {
|
|||
}
|
||||
|
||||
export default function Home() {
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
const [apiKey, setApiKey] = useState(() => {
|
||||
if (typeof window === "undefined") return ""
|
||||
try {
|
||||
return window.localStorage.getItem("supermemory-api-key") ?? ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
})
|
||||
const [containerTag, setContainerTag] = useState("")
|
||||
const [containerTags, setContainerTags] = useState<ContainerTagOption[]>([])
|
||||
const [isLoadingContainerTags, setIsLoadingContainerTags] = useState(false)
|
||||
|
|
@ -140,6 +147,20 @@ export default function Home() {
|
|||
documents: GraphApiDocument[]
|
||||
} | null>(null)
|
||||
|
||||
// Persist API key so all graph calls send `Authorization: Bearer <key>`
|
||||
// (see #1672 — unauthenticated calls 401 on non-localhost hosts).
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (apiKey) {
|
||||
window.localStorage.setItem("supermemory-api-key", apiKey)
|
||||
} else {
|
||||
window.localStorage.removeItem("supermemory-api-key")
|
||||
}
|
||||
} catch {
|
||||
// ignore storage errors (private mode, etc.)
|
||||
}
|
||||
}, [apiKey])
|
||||
|
||||
const selectedContainerTags = useMemo(() => {
|
||||
const trimmed = containerTag.trim()
|
||||
return trimmed ? [trimmed] : undefined
|
||||
|
|
|
|||
|
|
@ -484,23 +484,35 @@ async def fetch_container_context(
|
|||
timeout=HTTP_TIMEOUT_SECONDS,
|
||||
follow_redirects=False,
|
||||
) as http:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {sm_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"containerTags": [container_tag],
|
||||
"limit": 25,
|
||||
"sort": "createdAt",
|
||||
"order": "desc",
|
||||
}
|
||||
docs_response = await http.post(
|
||||
f"{base_url}/v3/documents/documents",
|
||||
headers={
|
||||
"Authorization": f"Bearer {sm_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={
|
||||
"containerTags": [container_tag],
|
||||
"limit": 25,
|
||||
"sort": "createdAt",
|
||||
"order": "desc",
|
||||
},
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
# Fallback for servers without the rich graph endpoint (see #1672):
|
||||
# supermemory-server 0.0.8 returns 404 for /documents/documents.
|
||||
# POST /v3/documents/list exists on both cloud and self-hosted.
|
||||
if docs_response.status_code == 404:
|
||||
docs_response = await http.post(
|
||||
f"{base_url}/v3/documents/list",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
docs_response.raise_for_status()
|
||||
docs = docs_response.json()
|
||||
|
||||
raw_documents = docs.get("documents", []) if isinstance(docs, dict) else []
|
||||
if isinstance(docs, dict):
|
||||
raw_documents = docs.get("documents", []) or docs.get("memories", [])
|
||||
|
||||
documents = []
|
||||
for doc in raw_documents:
|
||||
|
|
|
|||
|
|
@ -152,19 +152,48 @@ export async function fetchContainerContext(
|
|||
const client = getSupermemoryClient(apiKey)
|
||||
const profile = await fetchProfileContext(client, containerTag, query)
|
||||
|
||||
const docsResponse = await client.post<{
|
||||
documents?: unknown[]
|
||||
pagination?: unknown
|
||||
}>("/v3/documents/documents", {
|
||||
body: {
|
||||
containerTags: [containerTag],
|
||||
limit: 25,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
},
|
||||
})
|
||||
async function fetchDocs() {
|
||||
try {
|
||||
return await client.post<{
|
||||
documents?: unknown[]
|
||||
memories?: unknown[]
|
||||
pagination?: unknown
|
||||
}>("/v3/documents/documents", {
|
||||
body: {
|
||||
containerTags: [containerTag],
|
||||
limit: 25,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
// Fallback for servers without the rich graph endpoint (see #1672):
|
||||
// supermemory-server 0.0.8 returns 404 for /documents/documents.
|
||||
// POST /v3/documents/list exists on both cloud and self-hosted.
|
||||
const status =
|
||||
typeof error === "object" && error !== null && "status" in error
|
||||
? (error as { status?: unknown }).status
|
||||
: undefined
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
if (status !== 404 && !message.includes("404")) throw error
|
||||
return await client.post<{
|
||||
documents?: unknown[]
|
||||
memories?: unknown[]
|
||||
pagination?: unknown
|
||||
}>("/v3/documents/list", {
|
||||
body: {
|
||||
containerTags: [containerTag],
|
||||
limit: 25,
|
||||
sort: "createdAt",
|
||||
order: "desc",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const rawDocuments = docsResponse.documents ?? []
|
||||
const docsResponse = await fetchDocs()
|
||||
|
||||
const rawDocuments = docsResponse.documents ?? docsResponse.memories ?? []
|
||||
const documents = rawDocuments.map((doc) => {
|
||||
const record = doc as Record<string, unknown>
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue