@pyxmate/memory 0.10.0 → 0.12.1
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.
|
@@ -11,24 +11,31 @@ var MemoryServerError = class extends Error {
|
|
|
11
11
|
return this.status === 404;
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
14
15
|
var MemoryClient = class {
|
|
15
16
|
baseUrl;
|
|
16
17
|
_authHeaders;
|
|
18
|
+
_requestTimeoutMs;
|
|
17
19
|
constructor(memoryUrl, apiKeyOrOptions) {
|
|
18
20
|
this.baseUrl = memoryUrl.replace(/\/$/, "");
|
|
19
21
|
let apiKey;
|
|
20
22
|
let defaultHeaders = {};
|
|
23
|
+
let requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
21
24
|
if (typeof apiKeyOrOptions === "string") {
|
|
22
25
|
apiKey = apiKeyOrOptions;
|
|
23
26
|
} else if (apiKeyOrOptions) {
|
|
24
27
|
apiKey = apiKeyOrOptions.apiKey;
|
|
25
28
|
defaultHeaders = apiKeyOrOptions.defaultHeaders ?? {};
|
|
29
|
+
if (apiKeyOrOptions.requestTimeoutMs !== void 0) {
|
|
30
|
+
requestTimeoutMs = apiKeyOrOptions.requestTimeoutMs;
|
|
31
|
+
}
|
|
26
32
|
}
|
|
27
33
|
const trimmed = apiKey?.trim();
|
|
28
34
|
this._authHeaders = {
|
|
29
35
|
...trimmed ? { Authorization: `Bearer ${trimmed}` } : {},
|
|
30
36
|
...defaultHeaders
|
|
31
37
|
};
|
|
38
|
+
this._requestTimeoutMs = requestTimeoutMs;
|
|
32
39
|
}
|
|
33
40
|
/** Encode a path segment to prevent URL injection */
|
|
34
41
|
encodePathSegment(segment) {
|
|
@@ -112,21 +119,36 @@ var MemoryClient = class {
|
|
|
112
119
|
* With enrichment callbacks: fetches extracted images, calls describeImage for each,
|
|
113
120
|
* optionally extracts entities, then submits enrichment data back to the server.
|
|
114
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
|
|
115
123
|
async ingestFile(file, options) {
|
|
116
124
|
const formData = new FormData();
|
|
117
125
|
formData.append("file", file);
|
|
118
126
|
if (options?.description) {
|
|
119
127
|
formData.append("description", options.description);
|
|
120
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
|
+
}
|
|
121
136
|
const res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
|
|
122
137
|
method: "POST",
|
|
123
138
|
body: formData,
|
|
124
|
-
headers
|
|
139
|
+
headers
|
|
125
140
|
});
|
|
126
141
|
const result = await this.parseApiResponse(res);
|
|
127
|
-
if (result.enrichment
|
|
128
|
-
|
|
129
|
-
|
|
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) {
|
|
130
152
|
const CONCURRENCY = 5;
|
|
131
153
|
for (let i = 0; i < images.length; i += CONCURRENCY) {
|
|
132
154
|
const batch = images.slice(i, i + CONCURRENCY);
|
|
@@ -143,48 +165,66 @@ var MemoryClient = class {
|
|
|
143
165
|
);
|
|
144
166
|
}
|
|
145
167
|
const imageBuffer = await imageRes.arrayBuffer();
|
|
146
|
-
const description = await
|
|
168
|
+
const description = await describeImage(imageBuffer, imageMeta);
|
|
147
169
|
return { imageId: imageMeta.imageId, description };
|
|
148
170
|
})
|
|
149
171
|
);
|
|
150
172
|
descriptions.push(...batchResults);
|
|
151
173
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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);
|
|
158
193
|
entities = extracted.entities;
|
|
159
194
|
relationships = extracted.relationships;
|
|
160
195
|
}
|
|
161
|
-
const enrichTokenHeader = `${token}:${expiresAt}`;
|
|
162
|
-
const enrichRes = await fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
|
|
163
|
-
method: "POST",
|
|
164
|
-
headers: {
|
|
165
|
-
"Content-Type": "application/json",
|
|
166
|
-
"X-Enrichment-Token": enrichTokenHeader,
|
|
167
|
-
...this._authHeaders
|
|
168
|
-
},
|
|
169
|
-
body: JSON.stringify({
|
|
170
|
-
imageDescriptions: descriptions,
|
|
171
|
-
entities,
|
|
172
|
-
relationships
|
|
173
|
-
})
|
|
174
|
-
});
|
|
175
|
-
if (!enrichRes.ok) {
|
|
176
|
-
const body = await enrichRes.json().catch(() => ({}));
|
|
177
|
-
throw new MemoryServerError(
|
|
178
|
-
body.error ?? `Enrichment failed: ${enrichRes.status}`,
|
|
179
|
-
enrichRes.status
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
const enrichData = await this.parseApiResponse(enrichRes);
|
|
183
|
-
const enrichCharacters = descriptions.reduce((sum, d) => sum + d.description.length, 0);
|
|
184
|
-
result.entryIds.push(...enrichData.entryIds);
|
|
185
|
-
result.chunks += descriptions.length;
|
|
186
|
-
result.totalCharacters += enrichCharacters;
|
|
187
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;
|
|
188
228
|
return result;
|
|
189
229
|
}
|
|
190
230
|
/**
|
|
@@ -302,16 +342,44 @@ var MemoryClient = class {
|
|
|
302
342
|
return result.entries;
|
|
303
343
|
}
|
|
304
344
|
async fetchApi(path, options) {
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
...options
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
}
|
|
313
360
|
return this.parseApiResponse(res);
|
|
314
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
|
+
}
|
|
315
383
|
/** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
|
|
316
384
|
async parseApiResponse(res) {
|
|
317
385
|
let body;
|
package/dist/dashboard.mjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -71,15 +71,50 @@ interface ExtendedMemoryInterface extends MemoryInterface {
|
|
|
71
71
|
deleteBySource(source: string): Promise<number>;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Callbacks for two-phase file enrichment. All callbacks are optional so the
|
|
76
|
+
* SDK can support every combination of the v2 flow:
|
|
77
|
+
* - Image-rich PDF + describeImage only → describes images, no entity extraction
|
|
78
|
+
* - Image-rich PDF + describeImage + extractEntities[V2] → describes + extracts
|
|
79
|
+
* - Text-only file + extractEntities[V2] only → extracts from textWindows
|
|
80
|
+
* - Mixed file + all three → describes + extracts from both sources
|
|
81
|
+
*
|
|
82
|
+
* Without ANY callback, ingestFile returns the parsed result without running
|
|
83
|
+
* Phase 2/3 — caller opted out of enrichment entirely.
|
|
84
|
+
*/
|
|
75
85
|
interface EnrichmentCallbacks {
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Describe an image using LLM vision. Receives the raw image buffer and
|
|
88
|
+
* metadata. Required for image-bearing files; safe to omit for text-only
|
|
89
|
+
* uploads where the server emits zero images.
|
|
90
|
+
*/
|
|
91
|
+
describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta) => Promise<string>;
|
|
92
|
+
/**
|
|
93
|
+
* Legacy entity-extraction callback. Receives a flat string array which
|
|
94
|
+
* the SDK constructs as `[...textWindows, ...imageDescriptions]` — callers
|
|
95
|
+
* cannot distinguish the two sources. Kept for backwards compatibility;
|
|
96
|
+
* new code should prefer {@link extractEntitiesV2}.
|
|
97
|
+
*/
|
|
79
98
|
extractEntities?: (descriptions: string[]) => Promise<{
|
|
80
99
|
entities: IngestEntity$1[];
|
|
81
100
|
relationships: IngestRelationship$1[];
|
|
82
101
|
}>;
|
|
102
|
+
/**
|
|
103
|
+
* v2 entity-extraction callback. Receives text windows and image
|
|
104
|
+
* descriptions separately so callers can apply different prompts per
|
|
105
|
+
* source. Takes precedence over {@link extractEntities} when both are
|
|
106
|
+
* defined. Triggers the `X-Pyx-Enrichment-Capabilities: text_windows_v1`
|
|
107
|
+
* negotiation header on ingest.
|
|
108
|
+
*/
|
|
109
|
+
extractEntitiesV2?: (input: {
|
|
110
|
+
textWindows: string[];
|
|
111
|
+
imageDescriptions: string[];
|
|
112
|
+
mimeType: string;
|
|
113
|
+
filename: string;
|
|
114
|
+
}) => Promise<{
|
|
115
|
+
entities: IngestEntity$1[];
|
|
116
|
+
relationships: IngestRelationship$1[];
|
|
117
|
+
}>;
|
|
83
118
|
}
|
|
84
119
|
/** Error thrown by MemoryClient when the server returns a non-success response. */
|
|
85
120
|
declare class MemoryServerError extends Error {
|
|
@@ -93,10 +128,26 @@ interface MemoryClientOptions {
|
|
|
93
128
|
apiKey?: string;
|
|
94
129
|
/** Additional headers to send with every request (e.g., X-Caller-Access-Level). */
|
|
95
130
|
defaultHeaders?: Record<string, string>;
|
|
131
|
+
/**
|
|
132
|
+
* Default per-request timeout in milliseconds. Without this, a wedged
|
|
133
|
+
* memory server (e.g. event-loop blocked by inference) makes every
|
|
134
|
+
* caller hang forever — that was the Korens demo wedge in 2026-04 where
|
|
135
|
+
* a 161-second pyx-memory stall propagated through the runtime to the
|
|
136
|
+
* browser. Defaults to 30 s, which is high enough that normal
|
|
137
|
+
* `/search` and `/stats` requests never hit it but low enough that a
|
|
138
|
+
* stuck server fails loudly.
|
|
139
|
+
*
|
|
140
|
+
* Only applied when the caller does NOT pass their own `signal` via
|
|
141
|
+
* RequestInit. Long-running operations (large `consolidate`, `reindex`,
|
|
142
|
+
* file ingest with enrichment) should pass their own AbortSignal —
|
|
143
|
+
* that signal fully replaces the default ceiling.
|
|
144
|
+
*/
|
|
145
|
+
requestTimeoutMs?: number;
|
|
96
146
|
}
|
|
97
147
|
declare class MemoryClient implements ExtendedMemoryInterface {
|
|
98
148
|
protected baseUrl: string;
|
|
99
149
|
private readonly _authHeaders;
|
|
150
|
+
private readonly _requestTimeoutMs;
|
|
100
151
|
constructor(memoryUrl: string, apiKeyOrOptions?: string | MemoryClientOptions);
|
|
101
152
|
/** Encode a path segment to prevent URL injection */
|
|
102
153
|
private encodePathSegment;
|
|
@@ -156,6 +207,13 @@ declare class MemoryClient implements ExtendedMemoryInterface {
|
|
|
156
207
|
queryAsOf(asOfDate: string, filters?: TemporalQueryFilters): Promise<MemoryEntry$1[]>;
|
|
157
208
|
queryByEventTime(startTime: string, endTime: string, filters?: TemporalQueryFilters): Promise<MemoryEntry$1[]>;
|
|
158
209
|
protected fetchApi<T>(path: string, options?: RequestInit): Promise<T>;
|
|
210
|
+
/**
|
|
211
|
+
* Map fetch-layer rejections into a typed `MemoryServerError` so callers
|
|
212
|
+
* can react uniformly. AbortSignal.timeout fires a `TimeoutError`; the
|
|
213
|
+
* caller's signal generally fires an `AbortError`. Anything else (DNS,
|
|
214
|
+
* TCP reset, TLS) becomes a wrapped error with status 0.
|
|
215
|
+
*/
|
|
216
|
+
private translateFetchError;
|
|
159
217
|
/** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
|
|
160
218
|
private parseApiResponse;
|
|
161
219
|
}
|
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-DZZHJ66P.mjs";
|
|
15
|
+
import "./chunk-4YIKI2BA.mjs";
|
|
16
16
|
|
|
17
17
|
// ../dashboard/src/hooks/use-consolidation-log.ts
|
|
18
18
|
import { useCallback as useCallback2, useMemo } from "react";
|