@pyxmate/memory 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-DZZHJ66P.mjs → chunk-JBKJDTFO.mjs} +1 -1
- package/dist/chunk-QNRIO462.mjs +658 -0
- package/dist/dashboard.mjs +2 -2
- package/dist/index.d.ts +158 -7
- package/dist/index.mjs +1 -1
- package/dist/react.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-4YIKI2BA.mjs +0 -404
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
// ../client/src/memory-client.ts
|
|
2
|
+
var MemoryServerError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "MemoryServerError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
/** True when the server returned HTTP 404 (not found). */
|
|
10
|
+
get isNotFound() {
|
|
11
|
+
return this.status === 404;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
15
|
+
var INGEST_EVENT_HEARTBEAT_MS = 2e4;
|
|
16
|
+
var MemoryClient = class {
|
|
17
|
+
baseUrl;
|
|
18
|
+
_authHeaders;
|
|
19
|
+
_requestTimeoutMs;
|
|
20
|
+
constructor(memoryUrl, apiKeyOrOptions) {
|
|
21
|
+
this.baseUrl = memoryUrl.replace(/\/$/, "");
|
|
22
|
+
let apiKey;
|
|
23
|
+
let defaultHeaders = {};
|
|
24
|
+
let requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
25
|
+
if (typeof apiKeyOrOptions === "string") {
|
|
26
|
+
apiKey = apiKeyOrOptions;
|
|
27
|
+
} else if (apiKeyOrOptions) {
|
|
28
|
+
apiKey = apiKeyOrOptions.apiKey;
|
|
29
|
+
defaultHeaders = apiKeyOrOptions.defaultHeaders ?? {};
|
|
30
|
+
if (apiKeyOrOptions.requestTimeoutMs !== void 0) {
|
|
31
|
+
requestTimeoutMs = apiKeyOrOptions.requestTimeoutMs;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const trimmed = apiKey?.trim();
|
|
35
|
+
this._authHeaders = {
|
|
36
|
+
...trimmed ? { Authorization: `Bearer ${trimmed}` } : {},
|
|
37
|
+
...defaultHeaders
|
|
38
|
+
};
|
|
39
|
+
this._requestTimeoutMs = requestTimeoutMs;
|
|
40
|
+
}
|
|
41
|
+
/** Encode a path segment to prevent URL injection */
|
|
42
|
+
encodePathSegment(segment) {
|
|
43
|
+
return encodeURIComponent(segment);
|
|
44
|
+
}
|
|
45
|
+
async initialize() {
|
|
46
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
47
|
+
headers: this._authHeaders
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Memory server not reachable at ${this.baseUrl}: ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async store(entry) {
|
|
54
|
+
return this.fetchApi("/api/memory/ingest", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: JSON.stringify(entry)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async search(params) {
|
|
60
|
+
const searchParams = new URLSearchParams({ query: params.query });
|
|
61
|
+
if (params.limit) searchParams.set("limit", String(params.limit));
|
|
62
|
+
if (params.type) searchParams.set("type", params.type);
|
|
63
|
+
if (params.agentId) searchParams.set("agentId", params.agentId);
|
|
64
|
+
if (params.strategy) searchParams.set("strategy", params.strategy);
|
|
65
|
+
if (params.eventTimeRange) {
|
|
66
|
+
searchParams.set("eventTimeStart", params.eventTimeRange[0]);
|
|
67
|
+
searchParams.set("eventTimeEnd", params.eventTimeRange[1]);
|
|
68
|
+
}
|
|
69
|
+
if (params.asOf) searchParams.set("asOf", params.asOf);
|
|
70
|
+
if (params.abstentionThreshold != null)
|
|
71
|
+
searchParams.set("abstentionThreshold", String(params.abstentionThreshold));
|
|
72
|
+
return this.fetchApi(`/api/memory/search?${searchParams}`);
|
|
73
|
+
}
|
|
74
|
+
async get(id) {
|
|
75
|
+
try {
|
|
76
|
+
return await this.fetchApi(`/api/memory/entries/${this.encodePathSegment(id)}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof MemoryServerError && error.isNotFound) return null;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async delete(id) {
|
|
83
|
+
try {
|
|
84
|
+
await this.fetchApi(`/api/memory/entries/${this.encodePathSegment(id)}`, {
|
|
85
|
+
method: "DELETE"
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof MemoryServerError && error.isNotFound) return false;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async clearSession(sessionId) {
|
|
94
|
+
const result = await this.fetchApi(
|
|
95
|
+
`/api/memory/sessions/${this.encodePathSegment(sessionId)}`,
|
|
96
|
+
{ method: "DELETE" }
|
|
97
|
+
);
|
|
98
|
+
return result.cleared;
|
|
99
|
+
}
|
|
100
|
+
async stats() {
|
|
101
|
+
const stats = await this.fetchApi("/api/memory/stats");
|
|
102
|
+
return { ...stats, connected: true };
|
|
103
|
+
}
|
|
104
|
+
async shutdown() {
|
|
105
|
+
}
|
|
106
|
+
async list(params = {}) {
|
|
107
|
+
const searchParams = new URLSearchParams();
|
|
108
|
+
if (params.page != null) searchParams.set("page", String(params.page));
|
|
109
|
+
if (params.limit != null) searchParams.set("limit", String(params.limit));
|
|
110
|
+
if (params.type) searchParams.set("type", params.type);
|
|
111
|
+
if (params.agentId) searchParams.set("agentId", params.agentId);
|
|
112
|
+
const qs = searchParams.toString();
|
|
113
|
+
return this.fetchApi(`/api/memory/entries${qs ? `?${qs}` : ""}`);
|
|
114
|
+
}
|
|
115
|
+
// --- Additional endpoints ---
|
|
116
|
+
/**
|
|
117
|
+
* Ingest a file with optional two-phase enrichment.
|
|
118
|
+
*
|
|
119
|
+
* Without enrichment callbacks: standard single-phase ingest (backwards compatible).
|
|
120
|
+
* With enrichment callbacks: fetches extracted images, calls describeImage for each,
|
|
121
|
+
* optionally extracts entities, then submits enrichment data back to the server.
|
|
122
|
+
*/
|
|
123
|
+
/**
|
|
124
|
+
* Ingest a file end-to-end: server upload + (optionally) SDK-side LLM
|
|
125
|
+
* enrichment + graph write. Returns the final {@link FileIngestResult}.
|
|
126
|
+
*
|
|
127
|
+
* This is the Promise-shaped convenience that consumes
|
|
128
|
+
* {@link MemoryClient.ingestFileEvents} internally — every consumer that
|
|
129
|
+
* doesn't need progress observability stays on this exact signature.
|
|
130
|
+
* Throws {@link MemoryServerError} on terminal error events.
|
|
131
|
+
*/
|
|
132
|
+
async ingestFile(file, options) {
|
|
133
|
+
for await (const event of this.ingestFileEvents(file, options)) {
|
|
134
|
+
if (event.type === "result") return this.fileIngestResultFromEvent(event);
|
|
135
|
+
if (event.type === "error") {
|
|
136
|
+
throw new MemoryServerError(event.error, event.status ?? 500);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new MemoryServerError("File ingest stream ended without a terminal event", 0);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Native streaming ingest. Yields typed {@link IngestEvent}s as the server
|
|
143
|
+
* (parsing/storing) and the SDK (enrichment/result) make progress.
|
|
144
|
+
*
|
|
145
|
+
* Wire layout: SDK POSTs with `Accept: application/x-ndjson`; the server
|
|
146
|
+
* returns NDJSON `progress`/`heartbeat`/`result` events. After the server's
|
|
147
|
+
* terminal event, the SDK runs its own enrichment phase (image-describe,
|
|
148
|
+
* entity-extract, `/enrich` POST), emitting its own progress + heartbeat
|
|
149
|
+
* events around each step, then yields the single terminal `result` event.
|
|
150
|
+
*
|
|
151
|
+
* On older servers that don't honor `Accept: application/x-ndjson`, the
|
|
152
|
+
* SDK falls back to JSON parsing and synthesizes the SDK-side events as
|
|
153
|
+
* if it had streamed — same observable contract for callers either way.
|
|
154
|
+
*/
|
|
155
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: NDJSON dispatch + JSON fallback + SDK enrichment fan-out is genuinely 3 cooperating paths; each is documented inline and tested in memory-client-events.test.ts.
|
|
156
|
+
async *ingestFileEvents(file, options) {
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const relayAbort = () => controller.abort(options?.signal?.reason);
|
|
159
|
+
if (options?.signal?.aborted) relayAbort();
|
|
160
|
+
options?.signal?.addEventListener("abort", relayAbort, { once: true });
|
|
161
|
+
try {
|
|
162
|
+
const formData = new FormData();
|
|
163
|
+
formData.append("file", file);
|
|
164
|
+
if (options?.description) formData.append("description", options.description);
|
|
165
|
+
const wantsTextWindows = Boolean(
|
|
166
|
+
options?.enrichment?.extractEntities || options?.enrichment?.extractEntitiesV2
|
|
167
|
+
);
|
|
168
|
+
const headers = {
|
|
169
|
+
Accept: "application/x-ndjson",
|
|
170
|
+
...this._authHeaders
|
|
171
|
+
};
|
|
172
|
+
if (wantsTextWindows) headers["X-Pyx-Enrichment-Capabilities"] = "text_windows_v1";
|
|
173
|
+
let res;
|
|
174
|
+
try {
|
|
175
|
+
res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
body: formData,
|
|
178
|
+
headers,
|
|
179
|
+
signal: controller.signal
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
yield this.ingestErrorEvent(
|
|
183
|
+
this.translateFetchError(err, "/api/memory/ingest/file"),
|
|
184
|
+
"parsing"
|
|
185
|
+
);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
189
|
+
if (!contentType.includes("application/x-ndjson")) {
|
|
190
|
+
try {
|
|
191
|
+
const result = await this.parseApiResponse(res);
|
|
192
|
+
yield* this.completeIngestFileEvents(file, result, options, controller.signal);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
yield this.ingestErrorEvent(err, "parsing");
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!res.body) {
|
|
199
|
+
yield this.ingestErrorEvent(
|
|
200
|
+
new MemoryServerError("Memory server returned an empty stream", res.status),
|
|
201
|
+
"parsing"
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const reader = res.body.getReader();
|
|
206
|
+
const decoder = new TextDecoder();
|
|
207
|
+
let buffer = "";
|
|
208
|
+
let currentStage = "parsing";
|
|
209
|
+
let serverResult = null;
|
|
210
|
+
try {
|
|
211
|
+
while (true) {
|
|
212
|
+
const { done, value } = await reader.read();
|
|
213
|
+
if (done) break;
|
|
214
|
+
buffer += decoder.decode(value, { stream: true });
|
|
215
|
+
const lines = buffer.split("\n");
|
|
216
|
+
buffer = lines.pop() ?? "";
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
if (!line.trim()) continue;
|
|
219
|
+
const raw = JSON.parse(line);
|
|
220
|
+
const type = raw.type;
|
|
221
|
+
if (type === "progress" || type === "heartbeat") {
|
|
222
|
+
const stage = this.normalizeActiveIngestStage(raw.stage);
|
|
223
|
+
if (!stage) continue;
|
|
224
|
+
currentStage = stage;
|
|
225
|
+
yield { ...raw, schemaVersion: 1, type, stage };
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (type === "result") {
|
|
229
|
+
serverResult = this.fileIngestResultFromEvent({
|
|
230
|
+
...raw,
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
type: "result",
|
|
233
|
+
stage: "complete"
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
if (type === "error") {
|
|
238
|
+
yield {
|
|
239
|
+
schemaVersion: 1,
|
|
240
|
+
type: "error",
|
|
241
|
+
stage: this.normalizeActiveIngestStage(raw.stage) ?? currentStage,
|
|
242
|
+
error: typeof raw.error === "string" ? raw.error : "File ingest failed",
|
|
243
|
+
message: typeof raw.message === "string" ? raw.message : void 0,
|
|
244
|
+
code: typeof raw.code === "string" || typeof raw.code === "number" ? raw.code : void 0,
|
|
245
|
+
status: typeof raw.status === "number" ? raw.status : void 0
|
|
246
|
+
};
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (serverResult) break;
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
yield this.ingestErrorEvent(err, currentStage);
|
|
254
|
+
return;
|
|
255
|
+
} finally {
|
|
256
|
+
reader.releaseLock();
|
|
257
|
+
}
|
|
258
|
+
if (!serverResult) {
|
|
259
|
+
yield this.ingestErrorEvent(
|
|
260
|
+
new MemoryServerError("File ingest stream ended without a server result", 0),
|
|
261
|
+
currentStage
|
|
262
|
+
);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
yield* this.completeIngestFileEvents(file, serverResult, options, controller.signal);
|
|
266
|
+
} finally {
|
|
267
|
+
options?.signal?.removeEventListener("abort", relayAbort);
|
|
268
|
+
controller.abort();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Run the SDK-side enrichment phase (image-describe → entity-extract →
|
|
273
|
+
* `/enrich` POST) for a server result, emitting progress + heartbeat
|
|
274
|
+
* events around the slow steps and yielding the single terminal
|
|
275
|
+
* {@link IngestResultEvent} at the end. Skips work cleanly when the
|
|
276
|
+
* server emitted no enrichment block or the caller wired no callbacks.
|
|
277
|
+
*/
|
|
278
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: image-describe / extract-entities-v2 / extract-entities-v1 / no-op are documented decision branches; this is the same fan-out the original ingestFile() had, plus heartbeat plumbing.
|
|
279
|
+
async *completeIngestFileEvents(file, result, options, signal) {
|
|
280
|
+
try {
|
|
281
|
+
if (!result.enrichment || !options?.enrichment) {
|
|
282
|
+
yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const enrichment = result.enrichment;
|
|
286
|
+
const { fileId, token, expiresAt, images } = enrichment;
|
|
287
|
+
const isV2 = "version" in enrichment;
|
|
288
|
+
const textWindows = isV2 ? enrichment.textWindows : [];
|
|
289
|
+
const descriptions = [];
|
|
290
|
+
const describeImage = options.enrichment.describeImage;
|
|
291
|
+
if (describeImage && images.length > 0) {
|
|
292
|
+
yield {
|
|
293
|
+
schemaVersion: 1,
|
|
294
|
+
type: "progress",
|
|
295
|
+
stage: "enrichment",
|
|
296
|
+
filename: file.name,
|
|
297
|
+
message: "Describing extracted images"
|
|
298
|
+
};
|
|
299
|
+
const CONCURRENCY = 5;
|
|
300
|
+
for (let i = 0; i < images.length; i += CONCURRENCY) {
|
|
301
|
+
const batch = images.slice(i, i + CONCURRENCY);
|
|
302
|
+
const batchResults = yield* this.withSdkHeartbeats(
|
|
303
|
+
"enrichment",
|
|
304
|
+
Promise.all(
|
|
305
|
+
batch.map(async (imageMeta) => {
|
|
306
|
+
const imageRes = await fetch(
|
|
307
|
+
`${this.baseUrl}/api/memory/files/${fileId}/images/${imageMeta.imageId}?token=${encodeURIComponent(token)}`,
|
|
308
|
+
{ headers: this._authHeaders, signal }
|
|
309
|
+
);
|
|
310
|
+
if (!imageRes.ok) {
|
|
311
|
+
throw new MemoryServerError(
|
|
312
|
+
`Failed to fetch image ${imageMeta.imageId}: ${imageRes.status}`,
|
|
313
|
+
imageRes.status
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const imageBuffer = await imageRes.arrayBuffer();
|
|
317
|
+
const description = await describeImage(imageBuffer, imageMeta);
|
|
318
|
+
return { imageId: imageMeta.imageId, description };
|
|
319
|
+
})
|
|
320
|
+
),
|
|
321
|
+
signal
|
|
322
|
+
);
|
|
323
|
+
descriptions.push(...batchResults);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
let entities;
|
|
327
|
+
let relationships;
|
|
328
|
+
const imageDescriptionTexts = descriptions.map((d) => d.description);
|
|
329
|
+
if (options.enrichment.extractEntitiesV2 && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
|
|
330
|
+
yield {
|
|
331
|
+
schemaVersion: 1,
|
|
332
|
+
type: "progress",
|
|
333
|
+
stage: "enrichment",
|
|
334
|
+
filename: file.name,
|
|
335
|
+
message: "Extracting entities"
|
|
336
|
+
};
|
|
337
|
+
const extracted = yield* this.withSdkHeartbeats(
|
|
338
|
+
"enrichment",
|
|
339
|
+
options.enrichment.extractEntitiesV2({
|
|
340
|
+
textWindows,
|
|
341
|
+
imageDescriptions: imageDescriptionTexts,
|
|
342
|
+
mimeType: file.type,
|
|
343
|
+
filename: file.name
|
|
344
|
+
}),
|
|
345
|
+
signal
|
|
346
|
+
);
|
|
347
|
+
entities = extracted.entities;
|
|
348
|
+
relationships = extracted.relationships;
|
|
349
|
+
} else if (options.enrichment.extractEntities) {
|
|
350
|
+
const allInputs = [...textWindows, ...imageDescriptionTexts];
|
|
351
|
+
if (allInputs.length > 0) {
|
|
352
|
+
yield {
|
|
353
|
+
schemaVersion: 1,
|
|
354
|
+
type: "progress",
|
|
355
|
+
stage: "enrichment",
|
|
356
|
+
filename: file.name,
|
|
357
|
+
message: "Extracting entities"
|
|
358
|
+
};
|
|
359
|
+
const extracted = yield* this.withSdkHeartbeats(
|
|
360
|
+
"enrichment",
|
|
361
|
+
options.enrichment.extractEntities(allInputs),
|
|
362
|
+
signal
|
|
363
|
+
);
|
|
364
|
+
entities = extracted.entities;
|
|
365
|
+
relationships = extracted.relationships;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const hasGraph = (entities?.length ?? 0) > 0;
|
|
369
|
+
const hasImages = descriptions.length > 0;
|
|
370
|
+
if (!hasGraph && !hasImages) {
|
|
371
|
+
yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
yield {
|
|
375
|
+
schemaVersion: 1,
|
|
376
|
+
type: "progress",
|
|
377
|
+
stage: "enrichment",
|
|
378
|
+
filename: file.name,
|
|
379
|
+
message: "Persisting enrichment"
|
|
380
|
+
};
|
|
381
|
+
const enrichRes = yield* this.withSdkHeartbeats(
|
|
382
|
+
"enrichment",
|
|
383
|
+
fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: {
|
|
386
|
+
"Content-Type": "application/json",
|
|
387
|
+
"X-Enrichment-Token": `${token}:${expiresAt}`,
|
|
388
|
+
...this._authHeaders
|
|
389
|
+
},
|
|
390
|
+
signal,
|
|
391
|
+
body: JSON.stringify({ imageDescriptions: descriptions, entities, relationships })
|
|
392
|
+
}),
|
|
393
|
+
signal
|
|
394
|
+
);
|
|
395
|
+
if (!enrichRes.ok) {
|
|
396
|
+
const body = await enrichRes.json().catch(() => ({}));
|
|
397
|
+
throw new MemoryServerError(
|
|
398
|
+
body.error ?? `Enrichment failed: ${enrichRes.status}`,
|
|
399
|
+
enrichRes.status
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const enrichData = await this.parseApiResponse(enrichRes);
|
|
403
|
+
result.entryIds.push(...enrichData.entryIds);
|
|
404
|
+
result.chunks += descriptions.length;
|
|
405
|
+
result.totalCharacters += descriptions.reduce((sum, d) => sum + d.description.length, 0);
|
|
406
|
+
yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
|
|
407
|
+
} catch (err) {
|
|
408
|
+
yield this.ingestErrorEvent(err, "enrichment");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Race a Promise against a periodic heartbeat tick. Yields a heartbeat
|
|
413
|
+
* IngestEvent every {@link INGEST_EVENT_HEARTBEAT_MS} until the promise
|
|
414
|
+
* settles, then returns the resolved value (or rethrows). Lets callers
|
|
415
|
+
* keep upstream sockets alive through long LLM/HTTP work without
|
|
416
|
+
* coupling the heartbeat cadence to the work itself.
|
|
417
|
+
*/
|
|
418
|
+
async *withSdkHeartbeats(stage, work, signal) {
|
|
419
|
+
let settled = false;
|
|
420
|
+
let value;
|
|
421
|
+
let thrown;
|
|
422
|
+
const done = work.then(
|
|
423
|
+
(v) => {
|
|
424
|
+
settled = true;
|
|
425
|
+
value = v;
|
|
426
|
+
},
|
|
427
|
+
(err) => {
|
|
428
|
+
settled = true;
|
|
429
|
+
thrown = err;
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
while (!settled) {
|
|
433
|
+
let timer;
|
|
434
|
+
const tick = new Promise((resolve) => {
|
|
435
|
+
timer = setTimeout(() => resolve("tick"), INGEST_EVENT_HEARTBEAT_MS);
|
|
436
|
+
});
|
|
437
|
+
let cleanupAbort = () => {
|
|
438
|
+
};
|
|
439
|
+
const abort = new Promise((resolve) => {
|
|
440
|
+
if (!signal) return;
|
|
441
|
+
if (signal.aborted) return resolve("abort");
|
|
442
|
+
const onAbort = () => resolve("abort");
|
|
443
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
444
|
+
cleanupAbort = () => signal.removeEventListener("abort", onAbort);
|
|
445
|
+
});
|
|
446
|
+
const winner = await Promise.race([done.then(() => "done"), tick, abort]);
|
|
447
|
+
if (timer) clearTimeout(timer);
|
|
448
|
+
cleanupAbort();
|
|
449
|
+
if (winner === "abort") throw new DOMException("File ingest aborted", "AbortError");
|
|
450
|
+
if (winner === "tick" && !settled) {
|
|
451
|
+
yield {
|
|
452
|
+
schemaVersion: 1,
|
|
453
|
+
type: "heartbeat",
|
|
454
|
+
stage,
|
|
455
|
+
message: "SDK enrichment still running"
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
await done;
|
|
460
|
+
if (thrown) throw thrown;
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
normalizeActiveIngestStage(stage) {
|
|
464
|
+
if (stage === "parsing" || stage === "storing" || stage === "enrichment") return stage;
|
|
465
|
+
if (stage === "chunking") return "storing";
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
fileIngestResultFromEvent(event) {
|
|
469
|
+
const { schemaVersion: _v, type: _t, stage: _s, message: _m, ...result } = event;
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
ingestErrorEvent(error, stage) {
|
|
473
|
+
const status = error instanceof MemoryServerError ? error.status : error instanceof Error && error.name === "AbortError" ? 499 : void 0;
|
|
474
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
475
|
+
return {
|
|
476
|
+
schemaVersion: 1,
|
|
477
|
+
type: "error",
|
|
478
|
+
stage,
|
|
479
|
+
error: message,
|
|
480
|
+
message,
|
|
481
|
+
...status != null ? { status, code: status } : {}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get the download URL for an uploaded file.
|
|
486
|
+
* Returns a URL that serves the original file binary with proper Content-Type.
|
|
487
|
+
*/
|
|
488
|
+
getFileDownloadUrl(filename) {
|
|
489
|
+
return `${this.baseUrl}/api/memory/files/download/${encodeURIComponent(filename)}`;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Download an uploaded file by filename.
|
|
493
|
+
* Returns the raw Response (caller handles the body — arrayBuffer, blob, stream, etc.).
|
|
494
|
+
*/
|
|
495
|
+
async downloadFile(filename) {
|
|
496
|
+
const url = this.getFileDownloadUrl(filename);
|
|
497
|
+
const res = await fetch(url, { headers: this._authHeaders });
|
|
498
|
+
if (!res.ok) {
|
|
499
|
+
throw new MemoryServerError(`File download failed: ${res.status}`, res.status);
|
|
500
|
+
}
|
|
501
|
+
return res;
|
|
502
|
+
}
|
|
503
|
+
/** @deprecated Use {@link list} instead. Kept for backwards compatibility. */
|
|
504
|
+
async listEntries(params) {
|
|
505
|
+
const result = await this.list(params);
|
|
506
|
+
return result.entries;
|
|
507
|
+
}
|
|
508
|
+
async graphNodes() {
|
|
509
|
+
const result = await this.fetchApi(
|
|
510
|
+
"/api/memory/graph/nodes"
|
|
511
|
+
);
|
|
512
|
+
return result.nodes;
|
|
513
|
+
}
|
|
514
|
+
async graphEdges() {
|
|
515
|
+
return this.fetchApi(
|
|
516
|
+
"/api/memory/graph/edges"
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
async graphQuery(query) {
|
|
520
|
+
return this.fetchApi("/api/memory/graph/query", {
|
|
521
|
+
method: "POST",
|
|
522
|
+
body: JSON.stringify(query)
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
// --- ExtendedMemoryInterface methods ---
|
|
526
|
+
async consolidate() {
|
|
527
|
+
return this.fetchApi("/api/memory/consolidate", {
|
|
528
|
+
method: "POST"
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
async forget(id, reason) {
|
|
532
|
+
try {
|
|
533
|
+
await this.fetchApi(`/api/memory/forget/${this.encodePathSegment(id)}`, {
|
|
534
|
+
method: "POST",
|
|
535
|
+
body: JSON.stringify({ reason })
|
|
536
|
+
});
|
|
537
|
+
return true;
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (error instanceof MemoryServerError && error.isNotFound) return false;
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async summarizeSession(sessionId) {
|
|
544
|
+
try {
|
|
545
|
+
return await this.fetchApi(
|
|
546
|
+
`/api/memory/sessions/${this.encodePathSegment(sessionId)}/summarize`,
|
|
547
|
+
{ method: "POST" }
|
|
548
|
+
);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (error instanceof MemoryServerError && error.isNotFound) return null;
|
|
551
|
+
throw error;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async runDecay() {
|
|
555
|
+
const result = await this.fetchApi("/api/memory/decay", {
|
|
556
|
+
method: "POST"
|
|
557
|
+
});
|
|
558
|
+
return result.archived;
|
|
559
|
+
}
|
|
560
|
+
async reindex() {
|
|
561
|
+
await this.fetchApi("/api/memory/reindex", { method: "POST" });
|
|
562
|
+
}
|
|
563
|
+
async clearGraph() {
|
|
564
|
+
const result = await this.fetchApi("/api/memory/graph/clear", {
|
|
565
|
+
method: "POST"
|
|
566
|
+
});
|
|
567
|
+
return result.deleted;
|
|
568
|
+
}
|
|
569
|
+
async deleteBySource(source) {
|
|
570
|
+
const result = await this.fetchApi(
|
|
571
|
+
`/api/memory/source/${this.encodePathSegment(source)}`,
|
|
572
|
+
{ method: "DELETE" }
|
|
573
|
+
);
|
|
574
|
+
return result.deleted;
|
|
575
|
+
}
|
|
576
|
+
async queryAsOf(asOfDate, filters = {}) {
|
|
577
|
+
const params = new URLSearchParams({ asOf: asOfDate });
|
|
578
|
+
if (filters.type) params.set("type", filters.type);
|
|
579
|
+
if (filters.agentId) params.set("agentId", filters.agentId);
|
|
580
|
+
if (filters.source) params.set("source", filters.source);
|
|
581
|
+
if (filters.limit) params.set("limit", String(filters.limit));
|
|
582
|
+
const result = await this.fetchApi(
|
|
583
|
+
`/api/memory/query-as-of?${params}`
|
|
584
|
+
);
|
|
585
|
+
return result.entries;
|
|
586
|
+
}
|
|
587
|
+
async queryByEventTime(startTime, endTime, filters = {}) {
|
|
588
|
+
const params = new URLSearchParams({ startTime, endTime });
|
|
589
|
+
if (filters.type) params.set("type", filters.type);
|
|
590
|
+
if (filters.agentId) params.set("agentId", filters.agentId);
|
|
591
|
+
if (filters.source) params.set("source", filters.source);
|
|
592
|
+
if (filters.limit) params.set("limit", String(filters.limit));
|
|
593
|
+
const result = await this.fetchApi(
|
|
594
|
+
`/api/memory/query-by-event-time?${params}`
|
|
595
|
+
);
|
|
596
|
+
return result.entries;
|
|
597
|
+
}
|
|
598
|
+
async fetchApi(path, options) {
|
|
599
|
+
const signal = options?.signal ?? AbortSignal.timeout(this._requestTimeoutMs);
|
|
600
|
+
let res;
|
|
601
|
+
try {
|
|
602
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
603
|
+
...options,
|
|
604
|
+
headers: {
|
|
605
|
+
"Content-Type": "application/json",
|
|
606
|
+
...options?.headers,
|
|
607
|
+
...this._authHeaders
|
|
608
|
+
},
|
|
609
|
+
signal
|
|
610
|
+
});
|
|
611
|
+
} catch (err) {
|
|
612
|
+
throw this.translateFetchError(err, path);
|
|
613
|
+
}
|
|
614
|
+
return this.parseApiResponse(res);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Map fetch-layer rejections into a typed `MemoryServerError` so callers
|
|
618
|
+
* can react uniformly. AbortSignal.timeout fires a `TimeoutError`; the
|
|
619
|
+
* caller's signal generally fires an `AbortError`. Anything else (DNS,
|
|
620
|
+
* TCP reset, TLS) becomes a wrapped error with status 0.
|
|
621
|
+
*/
|
|
622
|
+
translateFetchError(err, path) {
|
|
623
|
+
if (err instanceof Error) {
|
|
624
|
+
if (err.name === "TimeoutError") {
|
|
625
|
+
return new MemoryServerError(
|
|
626
|
+
`Memory server request timed out after ${this._requestTimeoutMs}ms (${path})`,
|
|
627
|
+
504
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
if (err.name === "AbortError") {
|
|
631
|
+
return new MemoryServerError(`Memory server request aborted (${path})`, 499);
|
|
632
|
+
}
|
|
633
|
+
return new MemoryServerError(`Memory server request failed: ${err.message} (${path})`, 0);
|
|
634
|
+
}
|
|
635
|
+
return new MemoryServerError(`Memory server request failed: ${String(err)} (${path})`, 0);
|
|
636
|
+
}
|
|
637
|
+
/** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
|
|
638
|
+
async parseApiResponse(res) {
|
|
639
|
+
let body;
|
|
640
|
+
try {
|
|
641
|
+
body = await res.json();
|
|
642
|
+
} catch {
|
|
643
|
+
throw new MemoryServerError(
|
|
644
|
+
`Memory server error: invalid JSON response (${res.status})`,
|
|
645
|
+
res.status
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (!body?.success || body.data == null) {
|
|
649
|
+
throw new MemoryServerError(body?.error ?? `Memory server error: ${res.status}`, res.status);
|
|
650
|
+
}
|
|
651
|
+
return body.data;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
export {
|
|
656
|
+
MemoryServerError,
|
|
657
|
+
MemoryClient
|
|
658
|
+
};
|
package/dist/dashboard.mjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StoreInput as StoreInput$1, MemoryEntry as MemoryEntry$1, MemorySearchParams as MemorySearchParams$1, MemorySearchResult as MemorySearchResult$1, MemoryType as MemoryType$1, MemoryStats as MemoryStats$1, ExtractedImageMeta, IngestEntity as IngestEntity$1, IngestRelationship as IngestRelationship$1, FileIngestResult, GraphNode as GraphNode$1, GraphTraversalResult as GraphTraversalResult$1 } from '@pyx-memory/shared';
|
|
1
|
+
import { StoreInput as StoreInput$1, MemoryEntry as MemoryEntry$1, MemorySearchParams as MemorySearchParams$1, MemorySearchResult as MemorySearchResult$1, MemoryType as MemoryType$1, MemoryStats as MemoryStats$1, ExtractedImageMeta as ExtractedImageMeta$1, IngestEntity as IngestEntity$1, IngestRelationship as IngestRelationship$1, FileIngestResult as FileIngestResult$1, IngestEvent as IngestEvent$1, GraphNode as GraphNode$1, GraphTraversalResult as GraphTraversalResult$1 } from '@pyx-memory/shared';
|
|
2
2
|
|
|
3
3
|
/** Parameters for paginated entry listing. */
|
|
4
4
|
interface MemoryListParams {
|
|
@@ -88,7 +88,7 @@ interface EnrichmentCallbacks {
|
|
|
88
88
|
* metadata. Required for image-bearing files; safe to omit for text-only
|
|
89
89
|
* uploads where the server emits zero images.
|
|
90
90
|
*/
|
|
91
|
-
describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta) => Promise<string>;
|
|
91
|
+
describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta$1) => Promise<string>;
|
|
92
92
|
/**
|
|
93
93
|
* Legacy entity-extraction callback. Receives a flat string array which
|
|
94
94
|
* the SDK constructs as `[...textWindows, ...imageDescriptions]` — callers
|
|
@@ -116,6 +116,16 @@ interface EnrichmentCallbacks {
|
|
|
116
116
|
relationships: IngestRelationship$1[];
|
|
117
117
|
}>;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Options for {@link MemoryClient.ingestFile} and
|
|
121
|
+
* {@link MemoryClient.ingestFileEvents}. `signal` lets long-running ingests
|
|
122
|
+
* (LLM enrichment + graph writes) be cancelled cleanly.
|
|
123
|
+
*/
|
|
124
|
+
interface IngestFileOptions {
|
|
125
|
+
description?: string;
|
|
126
|
+
enrichment?: EnrichmentCallbacks;
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
}
|
|
119
129
|
/** Error thrown by MemoryClient when the server returns a non-success response. */
|
|
120
130
|
declare class MemoryServerError extends Error {
|
|
121
131
|
readonly status: number;
|
|
@@ -167,10 +177,50 @@ declare class MemoryClient implements ExtendedMemoryInterface {
|
|
|
167
177
|
* With enrichment callbacks: fetches extracted images, calls describeImage for each,
|
|
168
178
|
* optionally extracts entities, then submits enrichment data back to the server.
|
|
169
179
|
*/
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Ingest a file end-to-end: server upload + (optionally) SDK-side LLM
|
|
182
|
+
* enrichment + graph write. Returns the final {@link FileIngestResult}.
|
|
183
|
+
*
|
|
184
|
+
* This is the Promise-shaped convenience that consumes
|
|
185
|
+
* {@link MemoryClient.ingestFileEvents} internally — every consumer that
|
|
186
|
+
* doesn't need progress observability stays on this exact signature.
|
|
187
|
+
* Throws {@link MemoryServerError} on terminal error events.
|
|
188
|
+
*/
|
|
189
|
+
ingestFile(file: File, options?: IngestFileOptions): Promise<FileIngestResult$1>;
|
|
190
|
+
/**
|
|
191
|
+
* Native streaming ingest. Yields typed {@link IngestEvent}s as the server
|
|
192
|
+
* (parsing/storing) and the SDK (enrichment/result) make progress.
|
|
193
|
+
*
|
|
194
|
+
* Wire layout: SDK POSTs with `Accept: application/x-ndjson`; the server
|
|
195
|
+
* returns NDJSON `progress`/`heartbeat`/`result` events. After the server's
|
|
196
|
+
* terminal event, the SDK runs its own enrichment phase (image-describe,
|
|
197
|
+
* entity-extract, `/enrich` POST), emitting its own progress + heartbeat
|
|
198
|
+
* events around each step, then yields the single terminal `result` event.
|
|
199
|
+
*
|
|
200
|
+
* On older servers that don't honor `Accept: application/x-ndjson`, the
|
|
201
|
+
* SDK falls back to JSON parsing and synthesizes the SDK-side events as
|
|
202
|
+
* if it had streamed — same observable contract for callers either way.
|
|
203
|
+
*/
|
|
204
|
+
ingestFileEvents(file: File, options?: IngestFileOptions): AsyncIterable<IngestEvent$1>;
|
|
205
|
+
/**
|
|
206
|
+
* Run the SDK-side enrichment phase (image-describe → entity-extract →
|
|
207
|
+
* `/enrich` POST) for a server result, emitting progress + heartbeat
|
|
208
|
+
* events around the slow steps and yielding the single terminal
|
|
209
|
+
* {@link IngestResultEvent} at the end. Skips work cleanly when the
|
|
210
|
+
* server emitted no enrichment block or the caller wired no callbacks.
|
|
211
|
+
*/
|
|
212
|
+
private completeIngestFileEvents;
|
|
213
|
+
/**
|
|
214
|
+
* Race a Promise against a periodic heartbeat tick. Yields a heartbeat
|
|
215
|
+
* IngestEvent every {@link INGEST_EVENT_HEARTBEAT_MS} until the promise
|
|
216
|
+
* settles, then returns the resolved value (or rethrows). Lets callers
|
|
217
|
+
* keep upstream sockets alive through long LLM/HTTP work without
|
|
218
|
+
* coupling the heartbeat cadence to the work itself.
|
|
219
|
+
*/
|
|
220
|
+
private withSdkHeartbeats;
|
|
221
|
+
private normalizeActiveIngestStage;
|
|
222
|
+
private fileIngestResultFromEvent;
|
|
223
|
+
private ingestErrorEvent;
|
|
174
224
|
/**
|
|
175
225
|
* Get the download URL for an uploaded file.
|
|
176
226
|
* Returns a URL that serves the original file binary with proper Content-Type.
|
|
@@ -492,4 +542,105 @@ interface GraphTraversalResult {
|
|
|
492
542
|
}>;
|
|
493
543
|
}
|
|
494
544
|
|
|
495
|
-
|
|
545
|
+
/** Metadata for a single image extracted from a PDF. */
|
|
546
|
+
interface ExtractedImageMeta {
|
|
547
|
+
imageId: string;
|
|
548
|
+
pageNumber: number;
|
|
549
|
+
width: number;
|
|
550
|
+
height: number;
|
|
551
|
+
mimeType: 'image/png' | 'image/jpeg';
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Legacy Phase 1 enrichment block (v1). Returned by servers running the
|
|
555
|
+
* pre-Patch-#2 pipeline OR by Patch-#2+ servers when the client does NOT
|
|
556
|
+
* signal `text_windows_v1` capability via the
|
|
557
|
+
* `X-Pyx-Enrichment-Capabilities` request header.
|
|
558
|
+
*/
|
|
559
|
+
interface EnrichmentPendingV1 {
|
|
560
|
+
fileId: string;
|
|
561
|
+
token: string;
|
|
562
|
+
expiresAt: string;
|
|
563
|
+
images: ExtractedImageMeta[];
|
|
564
|
+
}
|
|
565
|
+
/** Truncation report for v2 text-window emission. Always present in v2. */
|
|
566
|
+
interface EnrichmentTextWindowsTruncation {
|
|
567
|
+
/** Total characters seen by the parser, including content beyond the cap. */
|
|
568
|
+
originalChars: number;
|
|
569
|
+
/** Sum of emitted window lengths. <= originalChars. */
|
|
570
|
+
includedChars: number;
|
|
571
|
+
windowCount: number;
|
|
572
|
+
truncated: boolean;
|
|
573
|
+
reason: 'maxWindows' | 'maxTotalChars' | null;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* v2 enrichment block. Returned by Patch-#2+ servers when the client signals
|
|
577
|
+
* `text_windows_v1` capability. Adds `textWindows` so SDK callers that bring
|
|
578
|
+
* their own LLM can extract entities from any text-bearing file (text PDFs,
|
|
579
|
+
* .txt, .md, .docx, .pptx, .xlsx, …) — closing the structural gap that left
|
|
580
|
+
* Knowledge Graph + Vector Map blank for text-only uploads.
|
|
581
|
+
*/
|
|
582
|
+
interface EnrichmentPendingV2 {
|
|
583
|
+
/** Discriminator. v1 omits this field. */
|
|
584
|
+
version: 2;
|
|
585
|
+
fileId: string;
|
|
586
|
+
token: string;
|
|
587
|
+
expiresAt: string;
|
|
588
|
+
images: ExtractedImageMeta[];
|
|
589
|
+
/** UTF-16 text windows for entity extraction. Empty when no text content. */
|
|
590
|
+
textWindows: string[];
|
|
591
|
+
textWindowsTruncation: EnrichmentTextWindowsTruncation;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Discriminated union over enrichment shapes. Narrow with `'version' in prep`
|
|
595
|
+
* before accessing v2-only fields.
|
|
596
|
+
*/
|
|
597
|
+
type EnrichmentPending = EnrichmentPendingV1 | EnrichmentPendingV2;
|
|
598
|
+
/** Extended ingestion result when PDF contains images or v2 text windows. */
|
|
599
|
+
interface FileIngestResult {
|
|
600
|
+
filename: string;
|
|
601
|
+
fileType: string;
|
|
602
|
+
chunks: number;
|
|
603
|
+
entryIds: string[];
|
|
604
|
+
totalCharacters: number;
|
|
605
|
+
/** Present when images were extracted (v1) OR text windows / images were emitted (v2). */
|
|
606
|
+
enrichment?: EnrichmentPending;
|
|
607
|
+
}
|
|
608
|
+
/** Coarse pipeline stages. Stable vocabulary — finer detail goes in counters/message. */
|
|
609
|
+
type IngestStage = 'parsing' | 'storing' | 'enrichment' | 'complete';
|
|
610
|
+
/** Mid-stream progress notification — never carries the terminal result. */
|
|
611
|
+
interface IngestProgressEvent {
|
|
612
|
+
schemaVersion: 1;
|
|
613
|
+
type: 'progress';
|
|
614
|
+
stage: Exclude<IngestStage, 'complete'>;
|
|
615
|
+
filename?: string;
|
|
616
|
+
chunksStored?: number;
|
|
617
|
+
totalCharacters?: number;
|
|
618
|
+
message?: string;
|
|
619
|
+
}
|
|
620
|
+
/** Keepalive emitted while a stage is doing slow work (LLM, graph batch, etc). */
|
|
621
|
+
interface IngestHeartbeatEvent {
|
|
622
|
+
schemaVersion: 1;
|
|
623
|
+
type: 'heartbeat';
|
|
624
|
+
stage: Exclude<IngestStage, 'complete'>;
|
|
625
|
+
message?: string;
|
|
626
|
+
}
|
|
627
|
+
/** Terminal success — exactly one per stream, carries the full FileIngestResult. */
|
|
628
|
+
interface IngestResultEvent extends FileIngestResult {
|
|
629
|
+
schemaVersion: 1;
|
|
630
|
+
type: 'result';
|
|
631
|
+
stage: 'complete';
|
|
632
|
+
message?: string;
|
|
633
|
+
}
|
|
634
|
+
/** Terminal failure — exactly one per stream, mutually exclusive with result. */
|
|
635
|
+
interface IngestErrorEvent {
|
|
636
|
+
schemaVersion: 1;
|
|
637
|
+
type: 'error';
|
|
638
|
+
stage: IngestStage;
|
|
639
|
+
error: string;
|
|
640
|
+
message?: string;
|
|
641
|
+
code?: string | number;
|
|
642
|
+
status?: number;
|
|
643
|
+
}
|
|
644
|
+
type IngestEvent = IngestProgressEvent | IngestHeartbeatEvent | IngestResultEvent | IngestErrorEvent;
|
|
645
|
+
|
|
646
|
+
export { type AgentId, type ApiResponse, type ConsolidationRunResult, DEFAULTS, EmbeddingProviderName, type EnrichmentCallbacks, type ExtendedMemoryInterface, type GraphFailureMode, type GraphNode, type GraphRelationship, type GraphTraversalResult, type IngestEntity, type IngestErrorEvent, type IngestEvent, type IngestFileOptions, type IngestHeartbeatEvent, type IngestProgressEvent, type IngestRelationship, type IngestResultEvent, type IngestStage, type IngestionResult, MemoryClient, type MemoryClientOptions, type MemoryEntry, type MemoryIngestRequest, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, RAGStrategy, SensitivityLevel, type StoreInput, StoreTarget, type TemporalQueryFilters, type TenantScopeOptions, type Timestamp, VectorProvider };
|
package/dist/index.mjs
CHANGED
package/dist/react.mjs
CHANGED
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
toGraphologyFormat,
|
|
12
12
|
transformGraphData,
|
|
13
13
|
unreachableHealth
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-JBKJDTFO.mjs";
|
|
15
|
+
import "./chunk-QNRIO462.mjs";
|
|
16
16
|
|
|
17
17
|
// ../dashboard/src/hooks/use-consolidation-log.ts
|
|
18
18
|
import { useCallback as useCallback2, useMemo } from "react";
|
package/package.json
CHANGED
package/dist/chunk-4YIKI2BA.mjs
DELETED
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
// ../client/src/memory-client.ts
|
|
2
|
-
var MemoryServerError = class extends Error {
|
|
3
|
-
status;
|
|
4
|
-
constructor(message, status) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.name = "MemoryServerError";
|
|
7
|
-
this.status = status;
|
|
8
|
-
}
|
|
9
|
-
/** True when the server returned HTTP 404 (not found). */
|
|
10
|
-
get isNotFound() {
|
|
11
|
-
return this.status === 404;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
15
|
-
var MemoryClient = class {
|
|
16
|
-
baseUrl;
|
|
17
|
-
_authHeaders;
|
|
18
|
-
_requestTimeoutMs;
|
|
19
|
-
constructor(memoryUrl, apiKeyOrOptions) {
|
|
20
|
-
this.baseUrl = memoryUrl.replace(/\/$/, "");
|
|
21
|
-
let apiKey;
|
|
22
|
-
let defaultHeaders = {};
|
|
23
|
-
let requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
24
|
-
if (typeof apiKeyOrOptions === "string") {
|
|
25
|
-
apiKey = apiKeyOrOptions;
|
|
26
|
-
} else if (apiKeyOrOptions) {
|
|
27
|
-
apiKey = apiKeyOrOptions.apiKey;
|
|
28
|
-
defaultHeaders = apiKeyOrOptions.defaultHeaders ?? {};
|
|
29
|
-
if (apiKeyOrOptions.requestTimeoutMs !== void 0) {
|
|
30
|
-
requestTimeoutMs = apiKeyOrOptions.requestTimeoutMs;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const trimmed = apiKey?.trim();
|
|
34
|
-
this._authHeaders = {
|
|
35
|
-
...trimmed ? { Authorization: `Bearer ${trimmed}` } : {},
|
|
36
|
-
...defaultHeaders
|
|
37
|
-
};
|
|
38
|
-
this._requestTimeoutMs = requestTimeoutMs;
|
|
39
|
-
}
|
|
40
|
-
/** Encode a path segment to prevent URL injection */
|
|
41
|
-
encodePathSegment(segment) {
|
|
42
|
-
return encodeURIComponent(segment);
|
|
43
|
-
}
|
|
44
|
-
async initialize() {
|
|
45
|
-
const response = await fetch(`${this.baseUrl}/health`, {
|
|
46
|
-
headers: this._authHeaders
|
|
47
|
-
});
|
|
48
|
-
if (!response.ok) {
|
|
49
|
-
throw new Error(`Memory server not reachable at ${this.baseUrl}: ${response.status}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async store(entry) {
|
|
53
|
-
return this.fetchApi("/api/memory/ingest", {
|
|
54
|
-
method: "POST",
|
|
55
|
-
body: JSON.stringify(entry)
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
async search(params) {
|
|
59
|
-
const searchParams = new URLSearchParams({ query: params.query });
|
|
60
|
-
if (params.limit) searchParams.set("limit", String(params.limit));
|
|
61
|
-
if (params.type) searchParams.set("type", params.type);
|
|
62
|
-
if (params.agentId) searchParams.set("agentId", params.agentId);
|
|
63
|
-
if (params.strategy) searchParams.set("strategy", params.strategy);
|
|
64
|
-
if (params.eventTimeRange) {
|
|
65
|
-
searchParams.set("eventTimeStart", params.eventTimeRange[0]);
|
|
66
|
-
searchParams.set("eventTimeEnd", params.eventTimeRange[1]);
|
|
67
|
-
}
|
|
68
|
-
if (params.asOf) searchParams.set("asOf", params.asOf);
|
|
69
|
-
if (params.abstentionThreshold != null)
|
|
70
|
-
searchParams.set("abstentionThreshold", String(params.abstentionThreshold));
|
|
71
|
-
return this.fetchApi(`/api/memory/search?${searchParams}`);
|
|
72
|
-
}
|
|
73
|
-
async get(id) {
|
|
74
|
-
try {
|
|
75
|
-
return await this.fetchApi(`/api/memory/entries/${this.encodePathSegment(id)}`);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if (error instanceof MemoryServerError && error.isNotFound) return null;
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async delete(id) {
|
|
82
|
-
try {
|
|
83
|
-
await this.fetchApi(`/api/memory/entries/${this.encodePathSegment(id)}`, {
|
|
84
|
-
method: "DELETE"
|
|
85
|
-
});
|
|
86
|
-
return true;
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (error instanceof MemoryServerError && error.isNotFound) return false;
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async clearSession(sessionId) {
|
|
93
|
-
const result = await this.fetchApi(
|
|
94
|
-
`/api/memory/sessions/${this.encodePathSegment(sessionId)}`,
|
|
95
|
-
{ method: "DELETE" }
|
|
96
|
-
);
|
|
97
|
-
return result.cleared;
|
|
98
|
-
}
|
|
99
|
-
async stats() {
|
|
100
|
-
const stats = await this.fetchApi("/api/memory/stats");
|
|
101
|
-
return { ...stats, connected: true };
|
|
102
|
-
}
|
|
103
|
-
async shutdown() {
|
|
104
|
-
}
|
|
105
|
-
async list(params = {}) {
|
|
106
|
-
const searchParams = new URLSearchParams();
|
|
107
|
-
if (params.page != null) searchParams.set("page", String(params.page));
|
|
108
|
-
if (params.limit != null) searchParams.set("limit", String(params.limit));
|
|
109
|
-
if (params.type) searchParams.set("type", params.type);
|
|
110
|
-
if (params.agentId) searchParams.set("agentId", params.agentId);
|
|
111
|
-
const qs = searchParams.toString();
|
|
112
|
-
return this.fetchApi(`/api/memory/entries${qs ? `?${qs}` : ""}`);
|
|
113
|
-
}
|
|
114
|
-
// --- Additional endpoints ---
|
|
115
|
-
/**
|
|
116
|
-
* Ingest a file with optional two-phase enrichment.
|
|
117
|
-
*
|
|
118
|
-
* Without enrichment callbacks: standard single-phase ingest (backwards compatible).
|
|
119
|
-
* With enrichment callbacks: fetches extracted images, calls describeImage for each,
|
|
120
|
-
* optionally extracts entities, then submits enrichment data back to the server.
|
|
121
|
-
*/
|
|
122
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: two-phase enrichment fans out across image-describe + entity-extract + v1/v2 negotiation; each branch maps to a documented case in memory-client-enrichment.test.ts
|
|
123
|
-
async ingestFile(file, options) {
|
|
124
|
-
const formData = new FormData();
|
|
125
|
-
formData.append("file", file);
|
|
126
|
-
if (options?.description) {
|
|
127
|
-
formData.append("description", options.description);
|
|
128
|
-
}
|
|
129
|
-
const wantsTextWindows = Boolean(
|
|
130
|
-
options?.enrichment?.extractEntities || options?.enrichment?.extractEntitiesV2
|
|
131
|
-
);
|
|
132
|
-
const headers = { ...this._authHeaders };
|
|
133
|
-
if (wantsTextWindows) {
|
|
134
|
-
headers["X-Pyx-Enrichment-Capabilities"] = "text_windows_v1";
|
|
135
|
-
}
|
|
136
|
-
const res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
|
|
137
|
-
method: "POST",
|
|
138
|
-
body: formData,
|
|
139
|
-
headers
|
|
140
|
-
});
|
|
141
|
-
const result = await this.parseApiResponse(res);
|
|
142
|
-
if (!result.enrichment || !options?.enrichment) {
|
|
143
|
-
return result;
|
|
144
|
-
}
|
|
145
|
-
const enrichment = result.enrichment;
|
|
146
|
-
const { fileId, token, expiresAt, images } = enrichment;
|
|
147
|
-
const isV2 = "version" in enrichment;
|
|
148
|
-
const textWindows = isV2 ? enrichment.textWindows : [];
|
|
149
|
-
const descriptions = [];
|
|
150
|
-
const describeImage = options.enrichment.describeImage;
|
|
151
|
-
if (describeImage && images.length > 0) {
|
|
152
|
-
const CONCURRENCY = 5;
|
|
153
|
-
for (let i = 0; i < images.length; i += CONCURRENCY) {
|
|
154
|
-
const batch = images.slice(i, i + CONCURRENCY);
|
|
155
|
-
const batchResults = await Promise.all(
|
|
156
|
-
batch.map(async (imageMeta) => {
|
|
157
|
-
const imageRes = await fetch(
|
|
158
|
-
`${this.baseUrl}/api/memory/files/${fileId}/images/${imageMeta.imageId}?token=${encodeURIComponent(token)}`,
|
|
159
|
-
{ headers: this._authHeaders }
|
|
160
|
-
);
|
|
161
|
-
if (!imageRes.ok) {
|
|
162
|
-
throw new MemoryServerError(
|
|
163
|
-
`Failed to fetch image ${imageMeta.imageId}: ${imageRes.status}`,
|
|
164
|
-
imageRes.status
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
const imageBuffer = await imageRes.arrayBuffer();
|
|
168
|
-
const description = await describeImage(imageBuffer, imageMeta);
|
|
169
|
-
return { imageId: imageMeta.imageId, description };
|
|
170
|
-
})
|
|
171
|
-
);
|
|
172
|
-
descriptions.push(...batchResults);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
let entities;
|
|
176
|
-
let relationships;
|
|
177
|
-
const v2Callback = options.enrichment.extractEntitiesV2;
|
|
178
|
-
const legacyCallback = options.enrichment.extractEntities;
|
|
179
|
-
const imageDescriptionTexts = descriptions.map((d) => d.description);
|
|
180
|
-
if (v2Callback && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
|
|
181
|
-
const extracted = await v2Callback({
|
|
182
|
-
textWindows,
|
|
183
|
-
imageDescriptions: imageDescriptionTexts,
|
|
184
|
-
mimeType: file.type,
|
|
185
|
-
filename: file.name
|
|
186
|
-
});
|
|
187
|
-
entities = extracted.entities;
|
|
188
|
-
relationships = extracted.relationships;
|
|
189
|
-
} else if (legacyCallback) {
|
|
190
|
-
const allInputs = [...textWindows, ...imageDescriptionTexts];
|
|
191
|
-
if (allInputs.length > 0) {
|
|
192
|
-
const extracted = await legacyCallback(allInputs);
|
|
193
|
-
entities = extracted.entities;
|
|
194
|
-
relationships = extracted.relationships;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const hasGraph = (entities?.length ?? 0) > 0;
|
|
198
|
-
const hasImages = descriptions.length > 0;
|
|
199
|
-
if (!hasGraph && !hasImages) {
|
|
200
|
-
return result;
|
|
201
|
-
}
|
|
202
|
-
const enrichTokenHeader = `${token}:${expiresAt}`;
|
|
203
|
-
const enrichRes = await fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
|
|
204
|
-
method: "POST",
|
|
205
|
-
headers: {
|
|
206
|
-
"Content-Type": "application/json",
|
|
207
|
-
"X-Enrichment-Token": enrichTokenHeader,
|
|
208
|
-
...this._authHeaders
|
|
209
|
-
},
|
|
210
|
-
body: JSON.stringify({
|
|
211
|
-
imageDescriptions: descriptions,
|
|
212
|
-
entities,
|
|
213
|
-
relationships
|
|
214
|
-
})
|
|
215
|
-
});
|
|
216
|
-
if (!enrichRes.ok) {
|
|
217
|
-
const body = await enrichRes.json().catch(() => ({}));
|
|
218
|
-
throw new MemoryServerError(
|
|
219
|
-
body.error ?? `Enrichment failed: ${enrichRes.status}`,
|
|
220
|
-
enrichRes.status
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
const enrichData = await this.parseApiResponse(enrichRes);
|
|
224
|
-
const enrichCharacters = descriptions.reduce((sum, d) => sum + d.description.length, 0);
|
|
225
|
-
result.entryIds.push(...enrichData.entryIds);
|
|
226
|
-
result.chunks += descriptions.length;
|
|
227
|
-
result.totalCharacters += enrichCharacters;
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Get the download URL for an uploaded file.
|
|
232
|
-
* Returns a URL that serves the original file binary with proper Content-Type.
|
|
233
|
-
*/
|
|
234
|
-
getFileDownloadUrl(filename) {
|
|
235
|
-
return `${this.baseUrl}/api/memory/files/download/${encodeURIComponent(filename)}`;
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Download an uploaded file by filename.
|
|
239
|
-
* Returns the raw Response (caller handles the body — arrayBuffer, blob, stream, etc.).
|
|
240
|
-
*/
|
|
241
|
-
async downloadFile(filename) {
|
|
242
|
-
const url = this.getFileDownloadUrl(filename);
|
|
243
|
-
const res = await fetch(url, { headers: this._authHeaders });
|
|
244
|
-
if (!res.ok) {
|
|
245
|
-
throw new MemoryServerError(`File download failed: ${res.status}`, res.status);
|
|
246
|
-
}
|
|
247
|
-
return res;
|
|
248
|
-
}
|
|
249
|
-
/** @deprecated Use {@link list} instead. Kept for backwards compatibility. */
|
|
250
|
-
async listEntries(params) {
|
|
251
|
-
const result = await this.list(params);
|
|
252
|
-
return result.entries;
|
|
253
|
-
}
|
|
254
|
-
async graphNodes() {
|
|
255
|
-
const result = await this.fetchApi(
|
|
256
|
-
"/api/memory/graph/nodes"
|
|
257
|
-
);
|
|
258
|
-
return result.nodes;
|
|
259
|
-
}
|
|
260
|
-
async graphEdges() {
|
|
261
|
-
return this.fetchApi(
|
|
262
|
-
"/api/memory/graph/edges"
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
async graphQuery(query) {
|
|
266
|
-
return this.fetchApi("/api/memory/graph/query", {
|
|
267
|
-
method: "POST",
|
|
268
|
-
body: JSON.stringify(query)
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
// --- ExtendedMemoryInterface methods ---
|
|
272
|
-
async consolidate() {
|
|
273
|
-
return this.fetchApi("/api/memory/consolidate", {
|
|
274
|
-
method: "POST"
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
async forget(id, reason) {
|
|
278
|
-
try {
|
|
279
|
-
await this.fetchApi(`/api/memory/forget/${this.encodePathSegment(id)}`, {
|
|
280
|
-
method: "POST",
|
|
281
|
-
body: JSON.stringify({ reason })
|
|
282
|
-
});
|
|
283
|
-
return true;
|
|
284
|
-
} catch (error) {
|
|
285
|
-
if (error instanceof MemoryServerError && error.isNotFound) return false;
|
|
286
|
-
throw error;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
async summarizeSession(sessionId) {
|
|
290
|
-
try {
|
|
291
|
-
return await this.fetchApi(
|
|
292
|
-
`/api/memory/sessions/${this.encodePathSegment(sessionId)}/summarize`,
|
|
293
|
-
{ method: "POST" }
|
|
294
|
-
);
|
|
295
|
-
} catch (error) {
|
|
296
|
-
if (error instanceof MemoryServerError && error.isNotFound) return null;
|
|
297
|
-
throw error;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
async runDecay() {
|
|
301
|
-
const result = await this.fetchApi("/api/memory/decay", {
|
|
302
|
-
method: "POST"
|
|
303
|
-
});
|
|
304
|
-
return result.archived;
|
|
305
|
-
}
|
|
306
|
-
async reindex() {
|
|
307
|
-
await this.fetchApi("/api/memory/reindex", { method: "POST" });
|
|
308
|
-
}
|
|
309
|
-
async clearGraph() {
|
|
310
|
-
const result = await this.fetchApi("/api/memory/graph/clear", {
|
|
311
|
-
method: "POST"
|
|
312
|
-
});
|
|
313
|
-
return result.deleted;
|
|
314
|
-
}
|
|
315
|
-
async deleteBySource(source) {
|
|
316
|
-
const result = await this.fetchApi(
|
|
317
|
-
`/api/memory/source/${this.encodePathSegment(source)}`,
|
|
318
|
-
{ method: "DELETE" }
|
|
319
|
-
);
|
|
320
|
-
return result.deleted;
|
|
321
|
-
}
|
|
322
|
-
async queryAsOf(asOfDate, filters = {}) {
|
|
323
|
-
const params = new URLSearchParams({ asOf: asOfDate });
|
|
324
|
-
if (filters.type) params.set("type", filters.type);
|
|
325
|
-
if (filters.agentId) params.set("agentId", filters.agentId);
|
|
326
|
-
if (filters.source) params.set("source", filters.source);
|
|
327
|
-
if (filters.limit) params.set("limit", String(filters.limit));
|
|
328
|
-
const result = await this.fetchApi(
|
|
329
|
-
`/api/memory/query-as-of?${params}`
|
|
330
|
-
);
|
|
331
|
-
return result.entries;
|
|
332
|
-
}
|
|
333
|
-
async queryByEventTime(startTime, endTime, filters = {}) {
|
|
334
|
-
const params = new URLSearchParams({ startTime, endTime });
|
|
335
|
-
if (filters.type) params.set("type", filters.type);
|
|
336
|
-
if (filters.agentId) params.set("agentId", filters.agentId);
|
|
337
|
-
if (filters.source) params.set("source", filters.source);
|
|
338
|
-
if (filters.limit) params.set("limit", String(filters.limit));
|
|
339
|
-
const result = await this.fetchApi(
|
|
340
|
-
`/api/memory/query-by-event-time?${params}`
|
|
341
|
-
);
|
|
342
|
-
return result.entries;
|
|
343
|
-
}
|
|
344
|
-
async fetchApi(path, options) {
|
|
345
|
-
const signal = options?.signal ?? AbortSignal.timeout(this._requestTimeoutMs);
|
|
346
|
-
let res;
|
|
347
|
-
try {
|
|
348
|
-
res = await fetch(`${this.baseUrl}${path}`, {
|
|
349
|
-
...options,
|
|
350
|
-
headers: {
|
|
351
|
-
"Content-Type": "application/json",
|
|
352
|
-
...options?.headers,
|
|
353
|
-
...this._authHeaders
|
|
354
|
-
},
|
|
355
|
-
signal
|
|
356
|
-
});
|
|
357
|
-
} catch (err) {
|
|
358
|
-
throw this.translateFetchError(err, path);
|
|
359
|
-
}
|
|
360
|
-
return this.parseApiResponse(res);
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Map fetch-layer rejections into a typed `MemoryServerError` so callers
|
|
364
|
-
* can react uniformly. AbortSignal.timeout fires a `TimeoutError`; the
|
|
365
|
-
* caller's signal generally fires an `AbortError`. Anything else (DNS,
|
|
366
|
-
* TCP reset, TLS) becomes a wrapped error with status 0.
|
|
367
|
-
*/
|
|
368
|
-
translateFetchError(err, path) {
|
|
369
|
-
if (err instanceof Error) {
|
|
370
|
-
if (err.name === "TimeoutError") {
|
|
371
|
-
return new MemoryServerError(
|
|
372
|
-
`Memory server request timed out after ${this._requestTimeoutMs}ms (${path})`,
|
|
373
|
-
504
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
if (err.name === "AbortError") {
|
|
377
|
-
return new MemoryServerError(`Memory server request aborted (${path})`, 499);
|
|
378
|
-
}
|
|
379
|
-
return new MemoryServerError(`Memory server request failed: ${err.message} (${path})`, 0);
|
|
380
|
-
}
|
|
381
|
-
return new MemoryServerError(`Memory server request failed: ${String(err)} (${path})`, 0);
|
|
382
|
-
}
|
|
383
|
-
/** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
|
|
384
|
-
async parseApiResponse(res) {
|
|
385
|
-
let body;
|
|
386
|
-
try {
|
|
387
|
-
body = await res.json();
|
|
388
|
-
} catch {
|
|
389
|
-
throw new MemoryServerError(
|
|
390
|
-
`Memory server error: invalid JSON response (${res.status})`,
|
|
391
|
-
res.status
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
if (!body?.success || body.data == null) {
|
|
395
|
-
throw new MemoryServerError(body?.error ?? `Memory server error: ${res.status}`, res.status);
|
|
396
|
-
}
|
|
397
|
-
return body.data;
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
export {
|
|
402
|
-
MemoryServerError,
|
|
403
|
-
MemoryClient
|
|
404
|
-
};
|