@pyxmate/memory 0.14.0 → 0.15.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-JBKJDTFO.mjs → chunk-K7JZXUBN.mjs} +1 -1
- package/dist/{chunk-QNRIO462.mjs → chunk-W4LB326D.mjs} +24 -64
- package/dist/dashboard.mjs +2 -2
- package/dist/index.d.ts +26 -52
- package/dist/index.mjs +1 -1
- package/dist/react.mjs +2 -2
- package/package.json +1 -1
- package/skills/pyx-memory/patterns/access-control.md +1 -1
- package/skills/pyx-memory/patterns/consumer.md +10 -4
- package/skills/pyx-memory/patterns/file-uploads.md +1 -1
- package/skills/pyx-memory/reference/http-api.md +46 -41
- package/skills/pyx-memory/reference/sdk-guide.md +23 -15
- package/skills/pyx-memory/reference/types.md +5 -7
|
@@ -112,47 +112,26 @@ var MemoryClient = class {
|
|
|
112
112
|
const qs = searchParams.toString();
|
|
113
113
|
return this.fetchApi(`/api/memory/entries${qs ? `?${qs}` : ""}`);
|
|
114
114
|
}
|
|
115
|
-
// ---
|
|
115
|
+
// --- File ingest ---
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
117
|
+
* Native streaming file ingest. Yields typed {@link IngestEvent}s as the
|
|
118
|
+
* server (parsing/storing) and the SDK (enrichment/result) make progress.
|
|
118
119
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Ingest a file end-to-end: server upload + (optionally) SDK-side LLM
|
|
125
|
-
* enrichment + graph write. Returns the final {@link FileIngestResult}.
|
|
120
|
+
* Wire contract: SDK POSTs with `Accept: application/x-ndjson`; the server
|
|
121
|
+
* MUST respond with NDJSON. There is no JSON fallback — older servers
|
|
122
|
+
* that emit `application/json` for this endpoint are not supported, and
|
|
123
|
+
* the SDK yields a terminal `error` event in that case.
|
|
126
124
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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.
|
|
125
|
+
* After the server's terminal `result`, the SDK runs its own enrichment
|
|
126
|
+
* phase (image-describe → entity-extract → `/enrich` POST), emitting
|
|
127
|
+
* progress + heartbeat events around each step, then yields the single
|
|
128
|
+
* terminal `result` event with the merged SDK + server result.
|
|
144
129
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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.
|
|
130
|
+
* Promise-shaped consumers should iterate the returned AsyncIterable and
|
|
131
|
+
* collect the terminal event; there is no separate `ingestFile()` Promise
|
|
132
|
+
* method by design (one wire format, one SDK method).
|
|
154
133
|
*/
|
|
155
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: NDJSON dispatch +
|
|
134
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: NDJSON dispatch + SDK enrichment fan-out is two cooperating paths; documented inline and tested in memory-client-events.test.ts.
|
|
156
135
|
async *ingestFileEvents(file, options) {
|
|
157
136
|
const controller = new AbortController();
|
|
158
137
|
const relayAbort = () => controller.abort(options?.signal?.reason);
|
|
@@ -162,9 +141,7 @@ var MemoryClient = class {
|
|
|
162
141
|
const formData = new FormData();
|
|
163
142
|
formData.append("file", file);
|
|
164
143
|
if (options?.description) formData.append("description", options.description);
|
|
165
|
-
const wantsTextWindows = Boolean(
|
|
166
|
-
options?.enrichment?.extractEntities || options?.enrichment?.extractEntitiesV2
|
|
167
|
-
);
|
|
144
|
+
const wantsTextWindows = Boolean(options?.enrichment?.extractEntitiesV2);
|
|
168
145
|
const headers = {
|
|
169
146
|
Accept: "application/x-ndjson",
|
|
170
147
|
...this._authHeaders
|
|
@@ -187,12 +164,13 @@ var MemoryClient = class {
|
|
|
187
164
|
}
|
|
188
165
|
const contentType = res.headers.get("content-type") ?? "";
|
|
189
166
|
if (!contentType.includes("application/x-ndjson")) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
167
|
+
yield this.ingestErrorEvent(
|
|
168
|
+
new MemoryServerError(
|
|
169
|
+
`Memory server returned ${contentType || "unknown content-type"} instead of application/x-ndjson \u2014 server is older than v0.15.0`,
|
|
170
|
+
res.status
|
|
171
|
+
),
|
|
172
|
+
"parsing"
|
|
173
|
+
);
|
|
196
174
|
return;
|
|
197
175
|
}
|
|
198
176
|
if (!res.body) {
|
|
@@ -275,7 +253,7 @@ var MemoryClient = class {
|
|
|
275
253
|
* {@link IngestResultEvent} at the end. Skips work cleanly when the
|
|
276
254
|
* server emitted no enrichment block or the caller wired no callbacks.
|
|
277
255
|
*/
|
|
278
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: image-describe / extract-entities-v2 /
|
|
256
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: image-describe / extract-entities-v2 / no-op are documented decision branches; the fan-out covers the v0.15.0 enrichment phase + heartbeat plumbing.
|
|
279
257
|
async *completeIngestFileEvents(file, result, options, signal) {
|
|
280
258
|
try {
|
|
281
259
|
if (!result.enrichment || !options?.enrichment) {
|
|
@@ -346,24 +324,6 @@ var MemoryClient = class {
|
|
|
346
324
|
);
|
|
347
325
|
entities = extracted.entities;
|
|
348
326
|
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
327
|
}
|
|
368
328
|
const hasGraph = (entities?.length ?? 0) > 0;
|
|
369
329
|
const hasImages = descriptions.length > 0;
|
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 as ExtractedImageMeta$1, IngestEntity as IngestEntity$1, IngestRelationship as IngestRelationship$1,
|
|
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, 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 {
|
|
@@ -72,15 +72,14 @@ interface ExtendedMemoryInterface extends MemoryInterface {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Callbacks for two-phase file enrichment. All callbacks are optional
|
|
76
|
-
* SDK can support every combination of the v2 flow:
|
|
75
|
+
* Callbacks for two-phase file enrichment. All callbacks are optional:
|
|
77
76
|
* - Image-rich PDF + describeImage only → describes images, no entity extraction
|
|
78
|
-
* - Image-rich PDF + describeImage +
|
|
79
|
-
* - Text-only file +
|
|
80
|
-
* - Mixed file +
|
|
77
|
+
* - Image-rich PDF + describeImage + extractEntitiesV2 → describes + extracts
|
|
78
|
+
* - Text-only file + extractEntitiesV2 only → extracts from textWindows
|
|
79
|
+
* - Mixed file + both → describes images + extracts from both sources
|
|
81
80
|
*
|
|
82
|
-
* Without
|
|
83
|
-
*
|
|
81
|
+
* Without any callback, the ingest stream emits the server result with no
|
|
82
|
+
* SDK-side enrichment — caller opted out entirely.
|
|
84
83
|
*/
|
|
85
84
|
interface EnrichmentCallbacks {
|
|
86
85
|
/**
|
|
@@ -90,21 +89,10 @@ interface EnrichmentCallbacks {
|
|
|
90
89
|
*/
|
|
91
90
|
describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta$1) => Promise<string>;
|
|
92
91
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*/
|
|
98
|
-
extractEntities?: (descriptions: string[]) => Promise<{
|
|
99
|
-
entities: IngestEntity$1[];
|
|
100
|
-
relationships: IngestRelationship$1[];
|
|
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.
|
|
92
|
+
* Entity-extraction callback. Receives text windows and image descriptions
|
|
93
|
+
* separately so callers can apply different prompts per source. Triggers
|
|
94
|
+
* the `X-Pyx-Enrichment-Capabilities: text_windows_v1` negotiation header
|
|
95
|
+
* on ingest.
|
|
108
96
|
*/
|
|
109
97
|
extractEntitiesV2?: (input: {
|
|
110
98
|
textWindows: string[];
|
|
@@ -117,9 +105,8 @@ interface EnrichmentCallbacks {
|
|
|
117
105
|
}>;
|
|
118
106
|
}
|
|
119
107
|
/**
|
|
120
|
-
* Options for {@link MemoryClient.
|
|
121
|
-
*
|
|
122
|
-
* (LLM enrichment + graph writes) be cancelled cleanly.
|
|
108
|
+
* Options for {@link MemoryClient.ingestFileEvents}. `signal` lets
|
|
109
|
+
* long-running ingests (LLM enrichment + graph writes) be cancelled cleanly.
|
|
123
110
|
*/
|
|
124
111
|
interface IngestFileOptions {
|
|
125
112
|
description?: string;
|
|
@@ -171,35 +158,22 @@ declare class MemoryClient implements ExtendedMemoryInterface {
|
|
|
171
158
|
shutdown(): Promise<void>;
|
|
172
159
|
list(params?: MemoryListParams): Promise<MemoryListResult>;
|
|
173
160
|
/**
|
|
174
|
-
*
|
|
161
|
+
* Native streaming file ingest. Yields typed {@link IngestEvent}s as the
|
|
162
|
+
* server (parsing/storing) and the SDK (enrichment/result) make progress.
|
|
175
163
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
|
|
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.
|
|
164
|
+
* Wire contract: SDK POSTs with `Accept: application/x-ndjson`; the server
|
|
165
|
+
* MUST respond with NDJSON. There is no JSON fallback — older servers
|
|
166
|
+
* that emit `application/json` for this endpoint are not supported, and
|
|
167
|
+
* the SDK yields a terminal `error` event in that case.
|
|
193
168
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* events around each step, then yields the single terminal `result` event.
|
|
169
|
+
* After the server's terminal `result`, the SDK runs its own enrichment
|
|
170
|
+
* phase (image-describe → entity-extract → `/enrich` POST), emitting
|
|
171
|
+
* progress + heartbeat events around each step, then yields the single
|
|
172
|
+
* terminal `result` event with the merged SDK + server result.
|
|
199
173
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
174
|
+
* Promise-shaped consumers should iterate the returned AsyncIterable and
|
|
175
|
+
* collect the terminal event; there is no separate `ingestFile()` Promise
|
|
176
|
+
* method by design (one wire format, one SDK method).
|
|
203
177
|
*/
|
|
204
178
|
ingestFileEvents(file: File, options?: IngestFileOptions): AsyncIterable<IngestEvent$1>;
|
|
205
179
|
/**
|
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-K7JZXUBN.mjs";
|
|
15
|
+
import "./chunk-W4LB326D.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
|
@@ -207,7 +207,7 @@ pyx-memory has two auth tiers: `API_KEY` (read + write) and `ADMIN_API_KEY` (des
|
|
|
207
207
|
|
|
208
208
|
| Key | Allowed operations |
|
|
209
209
|
|-----|-------------------|
|
|
210
|
-
| `API_KEY` | All read operations (search, get, list, stats) + write operations (store,
|
|
210
|
+
| `API_KEY` | All read operations (search, get, list, stats) + write operations (store, ingestFileEvents) |
|
|
211
211
|
| `ADMIN_API_KEY` | Destructive operations: DELETE, forget, decay, consolidate, reindex, deleteBySource |
|
|
212
212
|
|
|
213
213
|
```bash
|
|
@@ -42,18 +42,24 @@ if (memory) {
|
|
|
42
42
|
const nodes = await memory.graphNodes();
|
|
43
43
|
const traversal = await memory.graphQuery({ nodeId: 'node-1', depth: 2 });
|
|
44
44
|
|
|
45
|
-
// File ingestion —
|
|
45
|
+
// File ingestion — native NDJSON streaming via ingestFileEvents()
|
|
46
46
|
const file = new File([buffer], 'report.pdf', { type: 'application/pdf' });
|
|
47
|
-
const
|
|
47
|
+
for await (const event of memory.ingestFileEvents(file)) {
|
|
48
|
+
if (event.type === 'progress') console.log(`[${event.stage}] ${event.message ?? ''}`);
|
|
49
|
+
if (event.type === 'error') throw new Error(event.message ?? event.error);
|
|
50
|
+
// event.type === 'result' carries the terminal { filename, chunks, entryIds, ... }
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
// Image ingestion with AI description
|
|
50
54
|
// pyx-memory saves the original image to {DATA_DIR}/files/ and indexes the
|
|
51
55
|
// description in vector + SQLite for semantic search. Without a description,
|
|
52
56
|
// images get a minimal placeholder ("[Image] filename (size KB)").
|
|
53
57
|
const image = new File([imgBuffer], 'photo.png', { type: 'image/png' });
|
|
54
|
-
const
|
|
58
|
+
for await (const _ of memory.ingestFileEvents(image, {
|
|
55
59
|
description: 'A screenshot of the login page showing an error dialog',
|
|
56
|
-
})
|
|
60
|
+
})) {
|
|
61
|
+
/* drain to terminal */
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
// Lifecycle
|
|
59
65
|
await memory.consolidate();
|
|
@@ -90,53 +90,42 @@ For deterministic peak memory or timing (production UX), consumers should pre-ex
|
|
|
90
90
|
|
|
91
91
|
**Image ingestion**: Images cannot have text extracted. Pass a `description` field with a natural-language description (e.g., from an LLM with vision). Without a description, images get a minimal placeholder (`[Image] filename (size KB)`).
|
|
92
92
|
|
|
93
|
-
**PDF enrichment**: When a PDF contains images (≥50x50px), the
|
|
93
|
+
**PDF enrichment**: When a PDF contains images (≥50x50px), the server emits an `enrichment` block on the terminal `result` event. The SDK's `ingestFileEvents()` with `EnrichmentCallbacks` handles the full flow automatically — see [sdk-guide.md](sdk-guide.md#two-phase-pdf-enrichment).
|
|
94
94
|
|
|
95
|
-
###
|
|
95
|
+
### NDJSON Wire Format (the only response shape)
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
# Via Accept header
|
|
101
|
-
curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
|
|
102
|
-
-H "Authorization: Bearer {{API_KEY}}" \
|
|
103
|
-
-H "Accept: application/x-ndjson" \
|
|
104
|
-
-F "file=@large-report.pdf"
|
|
105
|
-
|
|
106
|
-
# Via query parameter
|
|
107
|
-
curl -X POST "{{ENDPOINT}}/api/memory/ingest/file?stream=true" \
|
|
108
|
-
-H "Authorization: Bearer {{API_KEY}}" \
|
|
109
|
-
-F "file=@large-report.pdf"
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
The response is streamed as newline-delimited JSON (`Content-Type: application/x-ndjson`). Each line is a JSON object:
|
|
97
|
+
`POST /api/memory/ingest/file` always streams `Content-Type: application/x-ndjson`. Each line is one typed event with `schemaVersion: 1`. The terminal event is exactly one of `result` (success) or `error` (failure):
|
|
113
98
|
|
|
114
99
|
```
|
|
115
|
-
{"type":"progress","stage":"parsing","filename":"large-report.pdf","message":"Extracting text..."}
|
|
116
|
-
{"type":"progress","stage":"storing","filename":"large-report.pdf","chunksStored":10,"totalCharacters":4800
|
|
117
|
-
{"
|
|
118
|
-
{"type":"progress","stage":"
|
|
119
|
-
{"type":"result","filename":"large-report.pdf","fileType":".pdf","chunks":24,"entryIds":[
|
|
100
|
+
{"schemaVersion":1,"type":"progress","stage":"parsing","filename":"large-report.pdf","message":"Extracting text..."}
|
|
101
|
+
{"schemaVersion":1,"type":"progress","stage":"storing","filename":"large-report.pdf","chunksStored":10,"totalCharacters":4800}
|
|
102
|
+
{"schemaVersion":1,"type":"heartbeat","stage":"storing","message":"File ingest still running"}
|
|
103
|
+
{"schemaVersion":1,"type":"progress","stage":"enrichment","filename":"large-report.pdf"}
|
|
104
|
+
{"schemaVersion":1,"type":"result","stage":"complete","filename":"large-report.pdf","fileType":".pdf","chunks":24,"entryIds":["…"],"totalCharacters":11520}
|
|
120
105
|
```
|
|
121
106
|
|
|
122
|
-
**
|
|
107
|
+
**Stable stages**: `parsing` (text/image extraction), `storing` (chunk + vector writes), `enrichment` (LLM image-describe + entity-extract + `/enrich` POST), `complete` (terminal result only). Finer detail goes in optional counters (`chunksStored`, `totalCharacters`) and `message`, not in new stage names.
|
|
108
|
+
|
|
109
|
+
**Heartbeat**: emitted every 20s while the pipeline is doing slow work. Sized below undici's default 300s `headersTimeout` and Envoy's default 5m stream-idle so an LLM call or graph batch never trips an upstream socket timeout.
|
|
123
110
|
|
|
124
|
-
|
|
111
|
+
**Errors before the stream starts** (multipart parse, file size cap, validation) come back as a single-line NDJSON `error` event with the appropriate HTTP status code on the response.
|
|
125
112
|
|
|
126
|
-
|
|
113
|
+
Pre-v0.15.0 servers returned `application/json` for this endpoint. v0.15.0 SDK consumers will see a terminal `error` event with the message `Memory server returned application/json instead of application/x-ndjson — server is older than v0.15.0` if they hit such a server; upgrade the server.
|
|
127
114
|
|
|
128
115
|
### Example: ingest a document
|
|
129
116
|
|
|
130
117
|
```bash
|
|
131
|
-
curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
|
|
118
|
+
curl -N -X POST {{ENDPOINT}}/api/memory/ingest/file \
|
|
132
119
|
-H "Authorization: Bearer {{API_KEY}}" \
|
|
133
120
|
-F "file=@report.pdf"
|
|
134
121
|
```
|
|
135
122
|
|
|
123
|
+
`-N` disables curl buffering so events appear as the server emits them.
|
|
124
|
+
|
|
136
125
|
### Example: ingest an image with description
|
|
137
126
|
|
|
138
127
|
```bash
|
|
139
|
-
curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
|
|
128
|
+
curl -N -X POST {{ENDPOINT}}/api/memory/ingest/file \
|
|
140
129
|
-H "Authorization: Bearer {{API_KEY}}" \
|
|
141
130
|
-F "file=@screenshot.png" \
|
|
142
131
|
-F "description=A screenshot of the dashboard showing memory usage at 85%"
|
|
@@ -145,29 +134,45 @@ curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
|
|
|
145
134
|
### SDK usage
|
|
146
135
|
|
|
147
136
|
```typescript
|
|
137
|
+
import { MemoryClient, type FileIngestResult } from '@pyxmate/memory';
|
|
138
|
+
|
|
139
|
+
// Iterate the AsyncIterable. The terminal event is `result` (success) or `error` (failure).
|
|
140
|
+
async function ingestAndCollect(memory: MemoryClient, file: File): Promise<FileIngestResult> {
|
|
141
|
+
for await (const event of memory.ingestFileEvents(file)) {
|
|
142
|
+
if (event.type === 'progress' || event.type === 'heartbeat') continue;
|
|
143
|
+
if (event.type === 'error') throw new Error(event.message ?? event.error);
|
|
144
|
+
// event.type === 'result'
|
|
145
|
+
const { schemaVersion: _v, type: _t, stage: _s, message: _m, ...result } = event;
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
throw new Error('memory ingest stream ended without a terminal event');
|
|
149
|
+
}
|
|
150
|
+
|
|
148
151
|
// Document (text-only)
|
|
149
152
|
const doc = new File([buffer], 'report.pdf', { type: 'application/pdf' });
|
|
150
|
-
await memory
|
|
153
|
+
await ingestAndCollect(memory, doc);
|
|
151
154
|
|
|
152
155
|
// Image with description
|
|
153
156
|
const img = new File([imgBuffer], 'photo.png', { type: 'image/png' });
|
|
154
|
-
await memory
|
|
157
|
+
await ingestAndCollect(memory, img); // pass via options when needed
|
|
158
|
+
for await (const _ of memory.ingestFileEvents(img, {
|
|
155
159
|
description: 'A photo of the whiteboard from the architecture meeting',
|
|
156
|
-
})
|
|
160
|
+
})) { /* drain */ }
|
|
157
161
|
|
|
158
162
|
// PDF with two-phase enrichment (images described via LLM vision)
|
|
159
|
-
await memory.
|
|
163
|
+
for await (const event of memory.ingestFileEvents(pdfFile, {
|
|
160
164
|
enrichment: {
|
|
161
|
-
describeImage: async (buffer, meta) =>
|
|
162
|
-
|
|
163
|
-
return 'A bar chart showing Q4 revenue growth of 23%';
|
|
164
|
-
},
|
|
165
|
-
extractEntities: async (descriptions) => ({
|
|
165
|
+
describeImage: async (buffer, meta) => 'A bar chart showing Q4 revenue growth of 23%',
|
|
166
|
+
extractEntitiesV2: async ({ textWindows, imageDescriptions, filename, mimeType }) => ({
|
|
166
167
|
entities: [{ name: 'Q4 Revenue', type: 'METRIC' }],
|
|
167
168
|
relationships: [],
|
|
168
169
|
}),
|
|
169
170
|
},
|
|
170
|
-
})
|
|
171
|
+
})) {
|
|
172
|
+
if (event.type === 'progress') {
|
|
173
|
+
console.log(`[${event.stage}] ${event.message ?? ''}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
171
176
|
```
|
|
172
177
|
|
|
173
178
|
## File Downloads
|
|
@@ -201,13 +206,13 @@ const buffer = await response.arrayBuffer();
|
|
|
201
206
|
|
|
202
207
|
## Two-Phase PDF Enrichment
|
|
203
208
|
|
|
204
|
-
When a PDF contains extractable images, the server
|
|
209
|
+
When a PDF contains extractable images, the server's terminal `result` event carries an `enrichment` block. The SDK's `ingestFileEvents()` with `EnrichmentCallbacks` runs the full three-phase flow automatically, emitting its own `enrichment` progress + heartbeat events while it works.
|
|
205
210
|
|
|
206
211
|
### Flow
|
|
207
212
|
|
|
208
213
|
```
|
|
209
|
-
Phase 1: POST /api/memory/ingest/file → text stored immediately
|
|
210
|
-
|
|
214
|
+
Phase 1: POST /api/memory/ingest/file → text stored immediately, enrichment block emitted on terminal `result`
|
|
215
|
+
Result event: { schemaVersion: 1, type: 'result', stage: 'complete', ...ingestResult, enrichment: { fileId, token, expiresAt, images: [...] } }
|
|
211
216
|
|
|
212
217
|
Phase 2: GET /api/memory/files/{fileId}/images/{imageId}?token=... → binary image
|
|
213
218
|
(SDK fetches each image, calls describeImage callback)
|
|
@@ -179,36 +179,44 @@ All from `@pyx-memory/core` except `MemoryServerError` from `@pyx-memory/client`
|
|
|
179
179
|
|
|
180
180
|
## Two-Phase PDF Enrichment
|
|
181
181
|
|
|
182
|
-
When ingesting PDFs that contain images, `
|
|
182
|
+
When ingesting PDFs that contain images, `ingestFileEvents()` runs a three-phase enrichment flow via `EnrichmentCallbacks`. The SDK handles all phases automatically and emits typed `IngestEvent`s as it goes:
|
|
183
183
|
|
|
184
|
-
1. **
|
|
185
|
-
2. **Image description** — each extracted image is fetched and described via your LLM vision callback (batched, 5 concurrent)
|
|
186
|
-
3. **Enrichment** — descriptions + entities
|
|
184
|
+
1. **Server upload** — text extracted and stored immediately; server emits `progress` events with stage `parsing` then `storing`
|
|
185
|
+
2. **Image description** — each extracted image is fetched and described via your LLM vision callback (batched, 5 concurrent); SDK emits `progress`/`heartbeat` events with stage `enrichment`
|
|
186
|
+
3. **Enrichment write** — descriptions + entities POSTed back to the server's `/enrich` endpoint; SDK emits the terminal `result` event
|
|
187
187
|
|
|
188
188
|
```typescript
|
|
189
|
-
import { MemoryClient, type EnrichmentCallbacks } from '@
|
|
189
|
+
import { MemoryClient, type EnrichmentCallbacks, type FileIngestResult } from '@pyxmate/memory';
|
|
190
190
|
|
|
191
191
|
const memory = new MemoryClient('http://localhost:7822', apiKey);
|
|
192
192
|
|
|
193
193
|
const enrichment: EnrichmentCallbacks = {
|
|
194
|
-
//
|
|
194
|
+
// Optional: describe each image using LLM vision
|
|
195
195
|
describeImage: async (imageBuffer, meta) => {
|
|
196
196
|
// imageBuffer is ArrayBuffer of the extracted PNG/JPEG
|
|
197
197
|
// meta has: imageId, pageNumber, width, height, mimeType
|
|
198
|
-
|
|
199
|
-
return description;
|
|
198
|
+
return await myVisionModel.describe(imageBuffer);
|
|
200
199
|
},
|
|
201
|
-
// Optional: extract entities from
|
|
202
|
-
|
|
200
|
+
// Optional: extract entities from text windows + image descriptions
|
|
201
|
+
extractEntitiesV2: async ({ textWindows, imageDescriptions, filename, mimeType }) => ({
|
|
203
202
|
entities: [{ name: 'Revenue', type: 'METRIC' }],
|
|
204
203
|
relationships: [{ source: 'Revenue', target: 'Q4', type: 'RELATED_TO' }],
|
|
205
204
|
}),
|
|
206
205
|
};
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
207
|
+
let result: FileIngestResult | null = null;
|
|
208
|
+
for await (const event of memory.ingestFileEvents(pdfFile, { enrichment })) {
|
|
209
|
+
if (event.type === 'progress') {
|
|
210
|
+
console.log(`[${event.stage}] ${event.message ?? ''}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (event.type === 'error') throw new Error(event.message ?? event.error);
|
|
214
|
+
if (event.type === 'result') {
|
|
215
|
+
const { schemaVersion: _v, type: _t, stage: _s, message: _m, ...payload } = event;
|
|
216
|
+
result = payload;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!result) throw new Error('memory ingest stream ended without a result');
|
|
212
220
|
```
|
|
213
221
|
|
|
214
|
-
Without `enrichment` callbacks, `
|
|
222
|
+
Without `enrichment` callbacks, the stream still emits `parsing` → `storing` progress events and a terminal `result` — the SDK just skips Phase 2/3.
|
|
@@ -107,13 +107,11 @@ class MemoryClient implements ExtendedMemoryInterface {
|
|
|
107
107
|
graphEdges(): Promise<{ stats: { nodeCount: number; edgeCount: number } }>;
|
|
108
108
|
graphQuery(query: { nodeId: string; depth?: number }): Promise<GraphTraversalResult>;
|
|
109
109
|
|
|
110
|
-
// File ingestion
|
|
111
|
-
//
|
|
112
|
-
// With callbacks: fetches extracted images, calls describeImage,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
enrichment?: EnrichmentCallbacks;
|
|
116
|
-
}): Promise<FileIngestResult>;
|
|
110
|
+
// File ingestion — native NDJSON streaming (v0.15.0+)
|
|
111
|
+
// Yields typed IngestEvents (progress / heartbeat / result / error).
|
|
112
|
+
// With enrichment callbacks: fetches extracted images, calls describeImage,
|
|
113
|
+
// calls extractEntitiesV2, posts /enrich, then yields the terminal `result`.
|
|
114
|
+
ingestFileEvents(file: File, options?: IngestFileOptions): AsyncIterable<IngestEvent>;
|
|
117
115
|
|
|
118
116
|
// Bi-temporal queries
|
|
119
117
|
queryAsOf(asOf: string, filters?: TemporalQueryFilters): Promise<MemoryEntry[]>;
|