fix: adopt new v3 memory endpoints in Python + TS clients (#4856)

This commit is contained in:
Saket Aryan
2026-04-16 05:28:49 +05:30
committed by GitHub
parent c8e20a9bb5
commit 8827553576
7 changed files with 49 additions and 50 deletions
+4 -4
View File
@@ -1,6 +1,7 @@
import axios from "axios"; import axios from "axios";
import { import {
AllUsers, AllUsers,
PaginatedMemories,
ProjectOptions, ProjectOptions,
Memory, Memory,
MemoryHistory, MemoryHistory,
@@ -221,7 +222,7 @@ export default class MemoryClient {
this._captureEvent("add", [payloadKeys]); this._captureEvent("add", [payloadKeys]);
const response = await this._fetchWithErrorHandling( const response = await this._fetchWithErrorHandling(
`${this.host}/v3/memories/`, `${this.host}/v3/memories/add/`,
{ {
method: "POST", method: "POST",
headers: this.headers, headers: this.headers,
@@ -284,7 +285,7 @@ export default class MemoryClient {
); );
} }
async getAll(options?: GetAllMemoryOptions): Promise<Array<Memory>> { async getAll(options?: GetAllMemoryOptions): Promise<PaginatedMemories> {
// Reject top-level entity params - must use filters instead // Reject top-level entity params - must use filters instead
rejectTopLevelEntityParams(options as Record<string, any>, "getAll"); rejectTopLevelEntityParams(options as Record<string, any>, "getAll");
@@ -293,12 +294,11 @@ export default class MemoryClient {
this._captureEvent("get_all", [payloadKeys]); this._captureEvent("get_all", [payloadKeys]);
const { page, pageSize, filters, ...rest } = options ?? {}; const { page, pageSize, filters, ...rest } = options ?? {};
const body: Record<string, any> = { const body: Record<string, any> = {
output_format: "v1.1",
...camelToSnakeKeys(rest), ...camelToSnakeKeys(rest),
...(filters && { filters }), ...(filters && { filters }),
}; };
let url = `${this.host}/v2/memories/`; let url = `${this.host}/v3/memories/`;
if (page && pageSize) { if (page && pageSize) {
url += `?page=${page}&page_size=${pageSize}`; url += `?page=${page}&page_size=${pageSize}`;
} }
+7
View File
@@ -142,6 +142,13 @@ export interface AllUsers {
previous: any; previous: any;
} }
export interface PaginatedMemories {
count: number;
next: string | null;
previous: string | null;
results: Array<Memory>;
}
export interface ProjectResponse { export interface ProjectResponse {
customInstructions?: string; customInstructions?: string;
customCategories?: string[]; customCategories?: string[];
@@ -124,13 +124,16 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
filters: { user_id: TEST_USER_ID }, filters: { user_id: TEST_USER_ID },
}); });
// v1.1 output_format returns { results: [...] } // Paginated shape: { count, next, previous, results: [...] }
expect(response).toHaveProperty("count");
expect(response).toHaveProperty("next");
expect(response).toHaveProperty("previous");
expect(response).toHaveProperty("results"); expect(response).toHaveProperty("results");
const memories = (response as any).results; expect(typeof response.count).toBe("number");
expect(Array.isArray(memories)).toBe(true); expect(Array.isArray(response.results)).toBe(true);
expect(memories.length).toBeGreaterThanOrEqual(memoryIds.length); expect(response.results.length).toBeGreaterThanOrEqual(memoryIds.length);
for (const mem of memories) { for (const mem of response.results) {
expect(typeof mem.id).toBe("string"); expect(typeof mem.id).toBe("string");
expect(typeof mem.memory).toBe("string"); expect(typeof mem.memory).toBe("string");
} }
@@ -143,9 +146,12 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
pageSize: 1, pageSize: 1,
}); });
// Paginated response is an object with results array
expect(page1).toBeDefined(); expect(page1).toBeDefined();
expect(page1).toHaveProperty("results"); expect(page1).toHaveProperty("count");
expect(page1).toHaveProperty("next");
expect(page1).toHaveProperty("previous");
expect(Array.isArray(page1.results)).toBe(true);
expect(page1.results.length).toBeLessThanOrEqual(1);
}); });
}); });
@@ -206,11 +212,10 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
filters: { user_id: `nonexistent-user-${randomUUID()}` }, filters: { user_id: `nonexistent-user-${randomUUID()}` },
}); });
// v1.1 output_format returns { results: [...] }
expect(response).toHaveProperty("results"); expect(response).toHaveProperty("results");
const memories = (response as any).results; expect(Array.isArray(response.results)).toBe(true);
expect(Array.isArray(memories)).toBe(true); expect(response.results.length).toBe(0);
expect(memories.length).toBe(0); expect(response.count).toBe(0);
}); });
test("deleteAll for non-existent user does not throw", async () => { test("deleteAll for non-existent user does not throw", async () => {
@@ -70,10 +70,7 @@ export async function waitForMemories(
const response = await withRetry(() => const response = await withRetry(() =>
client.getAll({ filters: { user_id: userId } }), client.getAll({ filters: { user_id: userId } }),
); );
// v1.1 output_format returns { results: [...] } const memories = response.results ?? [];
const memories = Array.isArray(response)
? response
: ((response as any)?.results ?? []);
if (memories.length >= minCount) { if (memories.length >= minCount) {
return memories; return memories;
} }
@@ -21,33 +21,33 @@ installConsoleSuppression();
// ─── add() ─────────────────────────────────────────────── // ─── add() ───────────────────────────────────────────────
describe("MemoryClient - add()", () => { describe("MemoryClient - add()", () => {
test("sends POST to /v3/memories/", async () => { test("sends POST to /v3/memories/add/", async () => {
const extra = new Map<string, { status: number; body: unknown }>(); const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", { status: 200, body: [createMockMemory()] }); extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] });
const mock = setupMockFetch(extra); const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY }); const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.add([{ role: "user", content: "Hello" }], { userId: "u1" }); await client.add([{ role: "user", content: "Hello" }], { userId: "u1" });
expect(findFetchCall(mock, "/v3/memories/", "POST")).toBeDefined(); expect(findFetchCall(mock, "/v3/memories/add/", "POST")).toBeDefined();
}); });
test("includes messages in request body", async () => { test("includes messages in request body", async () => {
const messages = [{ role: "user" as const, content: "Hello, I am Alex" }]; const messages = [{ role: "user" as const, content: "Hello, I am Alex" }];
const extra = new Map<string, { status: number; body: unknown }>(); const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", { status: 200, body: [createMockMemory()] }); extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] });
const mock = setupMockFetch(extra); const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY }); const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.add(messages, { userId: "u1" }); await client.add(messages, { userId: "u1" });
const call = findFetchCall(mock, "/v3/memories/", "POST"); const call = findFetchCall(mock, "/v3/memories/add/", "POST");
expect(getFetchBody(call!).messages).toEqual(messages); expect(getFetchBody(call!).messages).toEqual(messages);
}); });
test("includes user_id in request body", async () => { test("includes user_id in request body", async () => {
const extra = new Map<string, { status: number; body: unknown }>(); const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", { status: 200, body: [createMockMemory()] }); extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] });
const mock = setupMockFetch(extra); const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY }); const client = new MemoryClient({ apiKey: TEST_API_KEY });
@@ -55,19 +55,19 @@ describe("MemoryClient - add()", () => {
user_id: "user_1", user_id: "user_1",
}); });
const call = findFetchCall(mock, "/v3/memories/", "POST"); const call = findFetchCall(mock, "/v3/memories/add/", "POST");
expect(getFetchBody(call!).user_id).toBe("user_1"); expect(getFetchBody(call!).user_id).toBe("user_1");
}); });
test("sends empty messages array without crashing", async () => { test("sends empty messages array without crashing", async () => {
const extra = new Map<string, { status: number; body: unknown }>(); const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", { status: 200, body: [] }); extra.set("/v3/memories/add/", { status: 200, body: [] });
const mock = setupMockFetch(extra); const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY }); const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.add([], { userId: "u1" }); await client.add([], { userId: "u1" });
const call = findFetchCall(mock, "/v3/memories/", "POST"); const call = findFetchCall(mock, "/v3/memories/add/", "POST");
expect(getFetchBody(call!).messages).toEqual([]); expect(getFetchBody(call!).messages).toEqual([]);
}); });
}); });
@@ -254,7 +254,7 @@ describe("MemoryClient - getAll() entity param rejection", () => {
test("accepts filters with user_id", async () => { test("accepts filters with user_id", async () => {
const extra = new Map<string, { status: number; body: unknown }>(); const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v2/memories/", { extra.set("/v3/memories/", {
status: 200, status: 200,
body: { results: [] }, body: { results: [] },
}); });
@@ -262,6 +262,6 @@ describe("MemoryClient - getAll() entity param rejection", () => {
const client = new MemoryClient({ apiKey: TEST_API_KEY }); const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.getAll({ filters: { user_id: "u1" } }); await client.getAll({ filters: { user_id: "u1" } });
expect(findFetchCall(mock, "/v2/memories/", "POST")).toBeDefined(); expect(findFetchCall(mock, "/v3/memories/", "POST")).toBeDefined();
}); });
}); });
+10 -20
View File
@@ -167,7 +167,7 @@ class MemoryClient:
kwargs = self._prepare_params(kwargs) kwargs = self._prepare_params(kwargs)
payload = self._prepare_payload(messages, kwargs) payload = self._prepare_payload(messages, kwargs)
response = self.client.post("/v3/memories/", json=payload) response = self.client.post("/v3/memories/add/", json=payload)
response.raise_for_status() response.raise_for_status()
if "metadata" in kwargs: if "metadata" in kwargs:
del kwargs["metadata"] del kwargs["metadata"]
@@ -207,7 +207,7 @@ class MemoryClient:
**kwargs: Optional parameters for filtering (filters, page, page_size). **kwargs: Optional parameters for filtering (filters, page, page_size).
Returns: Returns:
A dictionary containing memories in v1.1 format: {"results": [...]} A paginated dict: {"count": int, "next": str | None, "previous": str | None, "results": [...]}
Raises: Raises:
ValidationError: If the input data is invalid. ValidationError: If the input data is invalid.
@@ -233,9 +233,9 @@ class MemoryClient:
"page": params.pop("page"), "page": params.pop("page"),
"page_size": params.pop("page_size"), "page_size": params.pop("page_size"),
} }
response = self.client.post("/v2/memories/", json=params, params=query_params) response = self.client.post("/v3/memories/", json=params, params=query_params)
else: else:
response = self.client.post("/v2/memories/", json=params) response = self.client.post("/v3/memories/", json=params)
response.raise_for_status() response.raise_for_status()
if "metadata" in kwargs: if "metadata" in kwargs:
del kwargs["metadata"] del kwargs["metadata"]
@@ -247,12 +247,7 @@ class MemoryClient:
"sync_type": "sync", "sync_type": "sync",
}, },
) )
result = response.json() return response.json()
# Ensure v1.1 format (wrap raw list if needed)
if isinstance(result, list):
return {"results": result}
return result
@api_error_handler @api_error_handler
def search(self, query: str, options: Optional[SearchMemoryOptions] = None, **kwargs) -> Dict[str, Any]: def search(self, query: str, options: Optional[SearchMemoryOptions] = None, **kwargs) -> Dict[str, Any]:
@@ -1095,7 +1090,7 @@ class AsyncMemoryClient:
kwargs = self._prepare_params(kwargs) kwargs = self._prepare_params(kwargs)
payload = self._prepare_payload(messages, kwargs) payload = self._prepare_payload(messages, kwargs)
response = await self.async_client.post("/v3/memories/", json=payload) response = await self.async_client.post("/v3/memories/add/", json=payload)
response.raise_for_status() response.raise_for_status()
if "metadata" in kwargs: if "metadata" in kwargs:
del kwargs["metadata"] del kwargs["metadata"]
@@ -1119,7 +1114,7 @@ class AsyncMemoryClient:
**kwargs: Optional parameters for filtering (filters, page, page_size). **kwargs: Optional parameters for filtering (filters, page, page_size).
Returns: Returns:
A dictionary containing memories in v1.1 format: {"results": [...]} A paginated dict: {"count": int, "next": str | None, "previous": str | None, "results": [...]}
Raises: Raises:
ValidationError: If the input data is invalid. ValidationError: If the input data is invalid.
@@ -1145,9 +1140,9 @@ class AsyncMemoryClient:
"page": params.pop("page"), "page": params.pop("page"),
"page_size": params.pop("page_size"), "page_size": params.pop("page_size"),
} }
response = await self.async_client.post("/v2/memories/", json=params, params=query_params) response = await self.async_client.post("/v3/memories/", json=params, params=query_params)
else: else:
response = await self.async_client.post("/v2/memories/", json=params) response = await self.async_client.post("/v3/memories/", json=params)
response.raise_for_status() response.raise_for_status()
if "metadata" in kwargs: if "metadata" in kwargs:
del kwargs["metadata"] del kwargs["metadata"]
@@ -1159,12 +1154,7 @@ class AsyncMemoryClient:
"sync_type": "async", "sync_type": "async",
}, },
) )
result = response.json() return response.json()
# Ensure v1.1 format (wrap raw list if needed)
if isinstance(result, list):
return {"results": result}
return result
@api_error_handler @api_error_handler
async def search(self, query: str, options: Optional[SearchMemoryOptions] = None, **kwargs) -> Dict[str, Any]: async def search(self, query: str, options: Optional[SearchMemoryOptions] = None, **kwargs) -> Dict[str, Any]: