fix: adopt new v3 memory endpoints in Python + TS clients (#4856)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import axios from "axios";
|
||||
import {
|
||||
AllUsers,
|
||||
PaginatedMemories,
|
||||
ProjectOptions,
|
||||
Memory,
|
||||
MemoryHistory,
|
||||
@@ -221,7 +222,7 @@ export default class MemoryClient {
|
||||
this._captureEvent("add", [payloadKeys]);
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v3/memories/`,
|
||||
`${this.host}/v3/memories/add/`,
|
||||
{
|
||||
method: "POST",
|
||||
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
|
||||
rejectTopLevelEntityParams(options as Record<string, any>, "getAll");
|
||||
|
||||
@@ -293,12 +294,11 @@ export default class MemoryClient {
|
||||
this._captureEvent("get_all", [payloadKeys]);
|
||||
const { page, pageSize, filters, ...rest } = options ?? {};
|
||||
const body: Record<string, any> = {
|
||||
output_format: "v1.1",
|
||||
...camelToSnakeKeys(rest),
|
||||
...(filters && { filters }),
|
||||
};
|
||||
|
||||
let url = `${this.host}/v2/memories/`;
|
||||
let url = `${this.host}/v3/memories/`;
|
||||
if (page && pageSize) {
|
||||
url += `?page=${page}&page_size=${pageSize}`;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,13 @@ export interface AllUsers {
|
||||
previous: any;
|
||||
}
|
||||
|
||||
export interface PaginatedMemories {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: Array<Memory>;
|
||||
}
|
||||
|
||||
export interface ProjectResponse {
|
||||
customInstructions?: string;
|
||||
customCategories?: string[];
|
||||
|
||||
@@ -124,13 +124,16 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||
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");
|
||||
const memories = (response as any).results;
|
||||
expect(Array.isArray(memories)).toBe(true);
|
||||
expect(memories.length).toBeGreaterThanOrEqual(memoryIds.length);
|
||||
expect(typeof response.count).toBe("number");
|
||||
expect(Array.isArray(response.results)).toBe(true);
|
||||
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.memory).toBe("string");
|
||||
}
|
||||
@@ -143,9 +146,12 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||
pageSize: 1,
|
||||
});
|
||||
|
||||
// Paginated response is an object with results array
|
||||
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()}` },
|
||||
});
|
||||
|
||||
// v1.1 output_format returns { results: [...] }
|
||||
expect(response).toHaveProperty("results");
|
||||
const memories = (response as any).results;
|
||||
expect(Array.isArray(memories)).toBe(true);
|
||||
expect(memories.length).toBe(0);
|
||||
expect(Array.isArray(response.results)).toBe(true);
|
||||
expect(response.results.length).toBe(0);
|
||||
expect(response.count).toBe(0);
|
||||
});
|
||||
|
||||
test("deleteAll for non-existent user does not throw", async () => {
|
||||
|
||||
@@ -70,10 +70,7 @@ export async function waitForMemories(
|
||||
const response = await withRetry(() =>
|
||||
client.getAll({ filters: { user_id: userId } }),
|
||||
);
|
||||
// v1.1 output_format returns { results: [...] }
|
||||
const memories = Array.isArray(response)
|
||||
? response
|
||||
: ((response as any)?.results ?? []);
|
||||
const memories = response.results ?? [];
|
||||
if (memories.length >= minCount) {
|
||||
return memories;
|
||||
}
|
||||
|
||||
@@ -21,33 +21,33 @@ installConsoleSuppression();
|
||||
// ─── 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 }>();
|
||||
extra.set("/v3/memories/", { status: 200, body: [createMockMemory()] });
|
||||
extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
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 () => {
|
||||
const messages = [{ role: "user" as const, content: "Hello, I am Alex" }];
|
||||
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 client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
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);
|
||||
});
|
||||
|
||||
test("includes user_id in request body", async () => {
|
||||
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 client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
@@ -55,19 +55,19 @@ describe("MemoryClient - add()", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
test("sends empty messages array without crashing", async () => {
|
||||
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 client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -254,7 +254,7 @@ describe("MemoryClient - getAll() entity param rejection", () => {
|
||||
|
||||
test("accepts filters with user_id", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/", {
|
||||
extra.set("/v3/memories/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
@@ -262,6 +262,6 @@ describe("MemoryClient - getAll() entity param rejection", () => {
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.getAll({ filters: { user_id: "u1" } });
|
||||
expect(findFetchCall(mock, "/v2/memories/", "POST")).toBeDefined();
|
||||
expect(findFetchCall(mock, "/v3/memories/", "POST")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
+10
-20
@@ -167,7 +167,7 @@ class MemoryClient:
|
||||
|
||||
kwargs = self._prepare_params(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()
|
||||
if "metadata" in kwargs:
|
||||
del kwargs["metadata"]
|
||||
@@ -207,7 +207,7 @@ class MemoryClient:
|
||||
**kwargs: Optional parameters for filtering (filters, page, page_size).
|
||||
|
||||
Returns:
|
||||
A dictionary containing memories in v1.1 format: {"results": [...]}
|
||||
A paginated dict: {"count": int, "next": str | None, "previous": str | None, "results": [...]}
|
||||
|
||||
Raises:
|
||||
ValidationError: If the input data is invalid.
|
||||
@@ -233,9 +233,9 @@ class MemoryClient:
|
||||
"page": params.pop("page"),
|
||||
"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:
|
||||
response = self.client.post("/v2/memories/", json=params)
|
||||
response = self.client.post("/v3/memories/", json=params)
|
||||
response.raise_for_status()
|
||||
if "metadata" in kwargs:
|
||||
del kwargs["metadata"]
|
||||
@@ -247,12 +247,7 @@ class MemoryClient:
|
||||
"sync_type": "sync",
|
||||
},
|
||||
)
|
||||
result = response.json()
|
||||
|
||||
# Ensure v1.1 format (wrap raw list if needed)
|
||||
if isinstance(result, list):
|
||||
return {"results": result}
|
||||
return result
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def search(self, query: str, options: Optional[SearchMemoryOptions] = None, **kwargs) -> Dict[str, Any]:
|
||||
@@ -1095,7 +1090,7 @@ class AsyncMemoryClient:
|
||||
|
||||
kwargs = self._prepare_params(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()
|
||||
if "metadata" in kwargs:
|
||||
del kwargs["metadata"]
|
||||
@@ -1119,7 +1114,7 @@ class AsyncMemoryClient:
|
||||
**kwargs: Optional parameters for filtering (filters, page, page_size).
|
||||
|
||||
Returns:
|
||||
A dictionary containing memories in v1.1 format: {"results": [...]}
|
||||
A paginated dict: {"count": int, "next": str | None, "previous": str | None, "results": [...]}
|
||||
|
||||
Raises:
|
||||
ValidationError: If the input data is invalid.
|
||||
@@ -1145,9 +1140,9 @@ class AsyncMemoryClient:
|
||||
"page": params.pop("page"),
|
||||
"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:
|
||||
response = await self.async_client.post("/v2/memories/", json=params)
|
||||
response = await self.async_client.post("/v3/memories/", json=params)
|
||||
response.raise_for_status()
|
||||
if "metadata" in kwargs:
|
||||
del kwargs["metadata"]
|
||||
@@ -1159,12 +1154,7 @@ class AsyncMemoryClient:
|
||||
"sync_type": "async",
|
||||
},
|
||||
)
|
||||
result = response.json()
|
||||
|
||||
# Ensure v1.1 format (wrap raw list if needed)
|
||||
if isinstance(result, list):
|
||||
return {"results": result}
|
||||
return result
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def search(self, query: str, options: Optional[SearchMemoryOptions] = None, **kwargs) -> Dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user