@pyxmate/memory 0.3.0-beta → 0.4.2

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MemoryClient
3
- } from "./chunk-FEOGTCSW.mjs";
3
+ } from "./chunk-YL5QMN6G.mjs";
4
4
 
5
5
  // ../dashboard/src/aggregations/consolidation-analytics.ts
6
6
  function analyzeConsolidationLog(entries) {
@@ -94,6 +94,13 @@ var MemoryClient = class {
94
94
  return this.fetchApi(`/api/memory/entries${qs ? `?${qs}` : ""}`);
95
95
  }
96
96
  // --- Additional endpoints ---
97
+ /**
98
+ * Ingest a file with optional two-phase enrichment.
99
+ *
100
+ * Without enrichment callbacks: standard single-phase ingest (backwards compatible).
101
+ * With enrichment callbacks: fetches extracted images, calls describeImage for each,
102
+ * optionally extracts entities, then submits enrichment data back to the server.
103
+ */
97
104
  async ingestFile(file, options) {
98
105
  const formData = new FormData();
99
106
  formData.append("file", file);
@@ -105,7 +112,64 @@ var MemoryClient = class {
105
112
  body: formData,
106
113
  headers: this._authHeaders
107
114
  });
108
- return this.parseApiResponse(res);
115
+ const result = await this.parseApiResponse(res);
116
+ if (result.enrichment && options?.enrichment?.describeImage) {
117
+ const { fileId, token, expiresAt, images } = result.enrichment;
118
+ const descriptions = [];
119
+ const CONCURRENCY = 5;
120
+ for (let i = 0; i < images.length; i += CONCURRENCY) {
121
+ const batch = images.slice(i, i + CONCURRENCY);
122
+ const batchResults = await Promise.all(
123
+ batch.map(async (imageMeta) => {
124
+ const imageRes = await fetch(
125
+ `${this.baseUrl}/api/memory/files/${fileId}/images/${imageMeta.imageId}?token=${encodeURIComponent(token)}`,
126
+ { headers: this._authHeaders }
127
+ );
128
+ if (!imageRes.ok) {
129
+ throw new MemoryServerError(
130
+ `Failed to fetch image ${imageMeta.imageId}: ${imageRes.status}`,
131
+ imageRes.status
132
+ );
133
+ }
134
+ const imageBuffer = await imageRes.arrayBuffer();
135
+ const description = await options.enrichment.describeImage(imageBuffer, imageMeta);
136
+ return { imageId: imageMeta.imageId, description };
137
+ })
138
+ );
139
+ descriptions.push(...batchResults);
140
+ }
141
+ let entities;
142
+ let relationships;
143
+ if (options.enrichment.extractEntities && descriptions.length > 0) {
144
+ const extracted = await options.enrichment.extractEntities(
145
+ descriptions.map((d) => d.description)
146
+ );
147
+ entities = extracted.entities;
148
+ relationships = extracted.relationships;
149
+ }
150
+ const enrichTokenHeader = `${token}:${expiresAt}`;
151
+ const enrichRes = await fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
152
+ method: "POST",
153
+ headers: {
154
+ "Content-Type": "application/json",
155
+ "X-Enrichment-Token": enrichTokenHeader,
156
+ ...this._authHeaders
157
+ },
158
+ body: JSON.stringify({
159
+ imageDescriptions: descriptions,
160
+ entities,
161
+ relationships
162
+ })
163
+ });
164
+ if (!enrichRes.ok) {
165
+ const body = await enrichRes.json().catch(() => ({}));
166
+ throw new MemoryServerError(
167
+ body.error ?? `Enrichment failed: ${enrichRes.status}`,
168
+ enrichRes.status
169
+ );
170
+ }
171
+ }
172
+ return result;
109
173
  }
110
174
  /** @deprecated Use {@link list} instead. Kept for backwards compatibility. */
111
175
  async listEntries(params) {
@@ -11,8 +11,8 @@ import {
11
11
  toGraphologyFormat,
12
12
  transformGraphData,
13
13
  unreachableHealth
14
- } from "./chunk-L62JXWDM.mjs";
15
- import "./chunk-FEOGTCSW.mjs";
14
+ } from "./chunk-AXTWVLDO.mjs";
15
+ import "./chunk-YL5QMN6G.mjs";
16
16
  export {
17
17
  DashboardClient,
18
18
  Poller,
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, 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, IngestEntity as IngestEntity$1, IngestRelationship as IngestRelationship$1, FileIngestResult, 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 {
@@ -65,14 +65,16 @@ interface ExtendedMemoryInterface extends MemoryInterface {
65
65
  deleteBySource(source: string): Promise<number>;
66
66
  }
67
67
 
68
- interface IngestionResult {
69
- filename: string;
70
- fileType: string;
71
- chunks: number;
72
- entryIds: string[];
73
- totalCharacters: number;
68
+ /** Callbacks for two-phase file enrichment. */
69
+ interface EnrichmentCallbacks {
70
+ /** Describe an image using LLM vision. Receives the raw image buffer and metadata. */
71
+ describeImage: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta) => Promise<string>;
72
+ /** Optionally extract entities/relationships from all image descriptions. */
73
+ extractEntities?: (descriptions: string[]) => Promise<{
74
+ entities: IngestEntity$1[];
75
+ relationships: IngestRelationship$1[];
76
+ }>;
74
77
  }
75
-
76
78
  /** Error thrown by MemoryClient when the server returns a non-success response. */
77
79
  declare class MemoryServerError extends Error {
78
80
  readonly status: number;
@@ -95,9 +97,17 @@ declare class MemoryClient implements ExtendedMemoryInterface {
95
97
  stats(): Promise<MemoryStats$1>;
96
98
  shutdown(): Promise<void>;
97
99
  list(params?: MemoryListParams): Promise<MemoryListResult>;
100
+ /**
101
+ * Ingest a file with optional two-phase enrichment.
102
+ *
103
+ * Without enrichment callbacks: standard single-phase ingest (backwards compatible).
104
+ * With enrichment callbacks: fetches extracted images, calls describeImage for each,
105
+ * optionally extracts entities, then submits enrichment data back to the server.
106
+ */
98
107
  ingestFile(file: File, options?: {
99
108
  description?: string;
100
- }): Promise<IngestionResult>;
109
+ enrichment?: EnrichmentCallbacks;
110
+ }): Promise<FileIngestResult>;
101
111
  /** @deprecated Use {@link list} instead. Kept for backwards compatibility. */
102
112
  listEntries(params?: {
103
113
  page?: number;
@@ -127,6 +137,14 @@ declare class MemoryClient implements ExtendedMemoryInterface {
127
137
  private parseApiResponse;
128
138
  }
129
139
 
140
+ interface IngestionResult {
141
+ filename: string;
142
+ fileType: string;
143
+ chunks: number;
144
+ entryIds: string[];
145
+ totalCharacters: number;
146
+ }
147
+
130
148
  declare const DEFAULTS: {
131
149
  readonly DATA_DIR: "./data";
132
150
  readonly VECTOR_PROVIDER: "lancedb";
@@ -336,4 +354,4 @@ interface GraphTraversalResult {
336
354
  }>;
337
355
  }
338
356
 
339
- export { type AgentId, type ApiResponse, type ConsolidationRunResult, DEFAULTS, EmbeddingProviderName, type ExtendedMemoryInterface, type GraphNode, type GraphRelationship, type GraphTraversalResult, type IngestEntity, type IngestRelationship, type IngestionResult, MemoryClient, type MemoryEntry, type MemoryIngestRequest, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, RAGStrategy, type StoreInput, StoreTarget, type TemporalQueryFilters, type Timestamp, VectorProvider };
357
+ export { type AgentId, type ApiResponse, type ConsolidationRunResult, DEFAULTS, EmbeddingProviderName, type EnrichmentCallbacks, type ExtendedMemoryInterface, type GraphNode, type GraphRelationship, type GraphTraversalResult, type IngestEntity, type IngestRelationship, type IngestionResult, MemoryClient, type MemoryEntry, type MemoryIngestRequest, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, RAGStrategy, type StoreInput, StoreTarget, type TemporalQueryFilters, type Timestamp, VectorProvider };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MemoryClient,
3
3
  MemoryServerError
4
- } from "./chunk-FEOGTCSW.mjs";
4
+ } from "./chunk-YL5QMN6G.mjs";
5
5
 
6
6
  // ../shared/src/constants/defaults.ts
7
7
  var DEFAULTS = {
package/dist/react.mjs CHANGED
@@ -11,8 +11,8 @@ import {
11
11
  toGraphologyFormat,
12
12
  transformGraphData,
13
13
  unreachableHealth
14
- } from "./chunk-L62JXWDM.mjs";
15
- import "./chunk-FEOGTCSW.mjs";
14
+ } from "./chunk-AXTWVLDO.mjs";
15
+ import "./chunk-YL5QMN6G.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyxmate/memory",
3
- "version": "0.3.0-beta",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",
@@ -38,6 +38,15 @@ if (memory) {
38
38
  const file = new File([buffer], 'report.pdf', { type: 'application/pdf' });
39
39
  const result = await memory.ingestFile(file);
40
40
 
41
+ // Image ingestion with AI description
42
+ // pyx-memory saves the original image to {DATA_DIR}/files/ and indexes the
43
+ // description in vector + SQLite for semantic search. Without a description,
44
+ // images get a minimal placeholder ("[Image] filename (size KB)").
45
+ const image = new File([imgBuffer], 'photo.png', { type: 'image/png' });
46
+ const imgResult = await memory.ingestFile(image, {
47
+ description: 'A screenshot of the login page showing an error dialog',
48
+ });
49
+
41
50
  // Lifecycle
42
51
  await memory.consolidate();
43
52
  await memory.runDecay();
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Authentication
4
4
 
5
- When the server has `API_KEY` configured, all requests (except `/health`) require one of:
5
+ When the server has `API_KEY` configured, all requests (except `/health` and enrichment routes) require one of:
6
6
 
7
7
  ```
8
8
  Authorization: Bearer <your-api-key>
@@ -11,19 +11,23 @@ X-API-Key: <your-api-key>
11
11
 
12
12
  Destructive operations (DELETE, forget, decay, consolidate, reindex) require `ADMIN_API_KEY` when configured (falls back to `API_KEY`).
13
13
 
14
+ **Enrichment routes** (`/api/memory/files/*`) use HMAC token auth instead of API keys. Tokens are issued by the server during file ingestion and are short-lived (30 minutes).
15
+
14
16
  `MemoryClient` handles this automatically when `apiKey` is passed to the constructor:
15
17
  ```typescript
16
18
  const client = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_KEY);
17
19
  ```
18
20
 
19
- ## Core (10 endpoints)
21
+ ## Core (12 endpoints)
20
22
 
21
23
  | Method | Endpoint | Description |
22
24
  |--------|----------|-------------|
23
25
  | GET | `/health` | Public health check (status only — no internals exposed) |
24
26
  | GET | `/admin/health` | Admin health check (version, uptime, embedding provider, memory stats) |
25
27
  | POST | `/api/memory/ingest` | Store a memory (JSON: `{ content, type, metadata, agentId?, sessionId?, targets?, entities?, relationships?, importance?, source?, eventTime?, id?, parentId?, ingestTime? }`) |
26
- | POST | `/api/memory/ingest/file` | Upload file (multipart, 50MB limit). Supports optional `description` field for agent-provided image descriptions (via LLM vision). Formats: txt, md, csv, pdf, docx, json, jsonl, html, png, jpg, jpeg, webp, gif, bmp, tiff, svg |
28
+ | POST | `/api/memory/ingest/file` | Upload file (multipart, 50MB limit). For PDFs with images, returns an `enrichment` block with HMAC token and image metadata for two-phase enrichment. |
29
+ | GET | `/api/memory/files/:fileId/images/:imageId?token=...` | Fetch an extracted PDF image (binary). Requires HMAC token from ingest response. |
30
+ | POST | `/api/memory/files/:fileId/enrich` | Submit image descriptions + entities after LLM vision processing. Requires `X-Enrichment-Token` header. |
27
31
  | GET | `/api/memory/search?query=...&strategy=...&limit=...` | Search memories — **does NOT support** filters, enableHyDE, enableRerank |
28
32
  | GET | `/api/memory/stats` | Memory statistics |
29
33
  | GET | `/api/memory/entries?page=...&limit=...` | List entries (paginated) |
@@ -54,6 +58,129 @@ const client = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_
54
58
  | GET | `/api/memory/query-as-of?asOf=...` | Bi-temporal point-in-time query (asOf, type, agentId, source, limit) |
55
59
  | GET | `/api/memory/query-by-event-time?startTime=...&endTime=...` | Bi-temporal event time range query (startTime, endTime, type, agentId, source, limit) |
56
60
 
61
+ ## File Ingestion (Images + Documents)
62
+
63
+ `POST /api/memory/ingest/file` accepts multipart/form-data with:
64
+
65
+ | Field | Type | Required | Description |
66
+ |-------|------|----------|-------------|
67
+ | `file` | File | Yes | The file to ingest (max 50MB) |
68
+ | `description` | string | No | Agent-provided description (e.g., from LLM vision). Used instead of parser output for images. |
69
+
70
+ **Supported formats**: `.txt`, `.md`, `.csv`, `.pdf`, `.docx`, `.json`, `.jsonl`, `.html`, `.htm`, `.log`, `.tsv`, `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.bmp`, `.tiff`, `.svg`
71
+
72
+ **What happens on upload**:
73
+ 1. Original file is saved to `{DATA_DIR}/files/{filename}` (persistent across restarts)
74
+ 2. Text is extracted (documents) or description is used (images)
75
+ 3. Content is chunked and stored in SQLite + vector for semantic search
76
+ 4. Source-aware dedup: re-uploading the same filename replaces the previous version
77
+ 5. **PDFs with images**: Images are extracted via pdfjs-dist, saved to a temp directory, and an `enrichment` block is returned with HMAC-signed tokens for two-phase enrichment
78
+
79
+ **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)`).
80
+
81
+ **PDF enrichment**: When a PDF contains images (≥50x50px), the response includes an `enrichment` block. The SDK's `ingestFile()` with `EnrichmentCallbacks` handles the full flow automatically — see [sdk-guide.md](sdk-guide.md#two-phase-pdf-enrichment).
82
+
83
+ ### Example: ingest a document
84
+
85
+ ```bash
86
+ curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
87
+ -H "Authorization: Bearer {{API_KEY}}" \
88
+ -F "file=@report.pdf"
89
+ ```
90
+
91
+ ### Example: ingest an image with description
92
+
93
+ ```bash
94
+ curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
95
+ -H "Authorization: Bearer {{API_KEY}}" \
96
+ -F "file=@screenshot.png" \
97
+ -F "description=A screenshot of the dashboard showing memory usage at 85%"
98
+ ```
99
+
100
+ ### SDK usage
101
+
102
+ ```typescript
103
+ // Document (text-only)
104
+ const doc = new File([buffer], 'report.pdf', { type: 'application/pdf' });
105
+ await memory.ingestFile(doc);
106
+
107
+ // Image with description
108
+ const img = new File([imgBuffer], 'photo.png', { type: 'image/png' });
109
+ await memory.ingestFile(img, {
110
+ description: 'A photo of the whiteboard from the architecture meeting',
111
+ });
112
+
113
+ // PDF with two-phase enrichment (images described via LLM vision)
114
+ await memory.ingestFile(pdfFile, {
115
+ enrichment: {
116
+ describeImage: async (buffer, meta) => {
117
+ // Call your LLM vision model to describe the image
118
+ return 'A bar chart showing Q4 revenue growth of 23%';
119
+ },
120
+ extractEntities: async (descriptions) => ({
121
+ entities: [{ name: 'Q4 Revenue', type: 'METRIC' }],
122
+ relationships: [],
123
+ }),
124
+ },
125
+ });
126
+ ```
127
+
128
+ ## Two-Phase PDF Enrichment
129
+
130
+ When a PDF contains extractable images, the server returns an `enrichment` block from `POST /api/memory/ingest/file`. The SDK wraps the full three-phase flow into a single `ingestFile()` call with `EnrichmentCallbacks`.
131
+
132
+ ### Flow
133
+
134
+ ```
135
+ Phase 1: POST /api/memory/ingest/file → text stored immediately
136
+ Response: { ...result, enrichment: { fileId, token, expiresAt, images: [...] } }
137
+
138
+ Phase 2: GET /api/memory/files/{fileId}/images/{imageId}?token=... → binary image
139
+ (SDK fetches each image, calls describeImage callback)
140
+
141
+ Phase 3: POST /api/memory/files/{fileId}/enrich
142
+ Header: X-Enrichment-Token: {token}:{expiresAt}
143
+ Body: { imageDescriptions: [...], entities?: [...], relationships?: [...] }
144
+ → descriptions stored as memory entries, temp images cleaned up
145
+ ```
146
+
147
+ ### Image fetch endpoint
148
+
149
+ `GET /api/memory/files/:fileId/images/:imageId?token=<hmac>`
150
+
151
+ Returns the binary image with `Content-Type: image/png` or `image/jpeg`. The HMAC token is issued by the server during Phase 1 and expires after 30 minutes.
152
+
153
+ ### Enrich endpoint
154
+
155
+ `POST /api/memory/files/:fileId/enrich`
156
+
157
+ | Header | Required | Description |
158
+ |--------|----------|-------------|
159
+ | `X-Enrichment-Token` | Yes | Format: `{hmac_token}:{expiresAt_iso}` |
160
+
161
+ Body:
162
+ ```json
163
+ {
164
+ "imageDescriptions": [
165
+ { "imageId": "uuid-img-0", "description": "A chart showing..." }
166
+ ],
167
+ "entities": [{ "name": "Revenue", "type": "METRIC" }],
168
+ "relationships": []
169
+ }
170
+ ```
171
+
172
+ Input limits: descriptions max 10,000 chars each, entities max 200, relationships max 500.
173
+
174
+ ### Security
175
+
176
+ - Tokens are HMAC-SHA256 signed (server secret + fileId + expiresAt)
177
+ - `fileId` must be a valid UUIDv4
178
+ - `imageId` must match `^[a-zA-Z0-9_-]+$`
179
+ - Image paths are sandboxed via `resolve()` + `startsWith()` to prevent path traversal
180
+ - Enrichment routes bypass API key auth — they use their own HMAC verification
181
+
182
+ ---
183
+
57
184
  ## Entity Extraction (Knowledge Graph)
58
185
 
59
186
  When storing memories that mention named subjects, include `entities` and `relationships` in the ingest request to populate the knowledge graph. This enables graph-based RAG and dashboard visualization.
@@ -121,3 +248,4 @@ All responses follow: `{ success: boolean, data?: T, error?: string }`
121
248
  | `NODE_ENV` | `development` | Set to `production` to mask 5xx error details and enable HSTS |
122
249
  | `PII_POLICY` | `flag` | PII handling: `flag` (detect + tag), `redact` (replace with [REDACTED]), `block` (reject 400) |
123
250
  | `RATE_LIMIT_RPM` | `0` | Requests per minute per IP. 0 = disabled |
251
+ | `ENRICHMENT_SECRET` | (auto-generated) | HMAC secret for enrichment token signing. Auto-generated if unset (tokens won't survive restarts). Min 32 bytes for production. |
@@ -97,7 +97,7 @@ Start the server: `bun packages/server/src/index.ts`
97
97
  @pyx-memory/core → Memory class, embeddings, graph, RAG, ingestion, lifecycle
98
98
  ↑ (re-exports everything from client and shared)
99
99
  |
100
- @pyx-memory/server → HTTP sidecar server (23 endpoints)
100
+ @pyx-memory/server → HTTP sidecar server (25 endpoints)
101
101
 
102
102
  @pyx-memory/dashboard → DashboardClient (extends MemoryClient), React hooks,
103
103
  Poller, aggregations, graph transforms (D3/Graphology)
@@ -164,3 +164,39 @@ MemoryServerError — HTTP client errors (has .status, .isNotFound)
164
164
  ```
165
165
 
166
166
  All from `@pyx-memory/core` except `MemoryServerError` from `@pyx-memory/client`.
167
+
168
+ ## Two-Phase PDF Enrichment
169
+
170
+ When ingesting PDFs that contain images, `ingestFile()` supports a two-phase enrichment flow via `EnrichmentCallbacks`. The SDK handles all three phases automatically:
171
+
172
+ 1. **Upload** — text extracted and stored immediately
173
+ 2. **Image description** — each extracted image is fetched and described via your LLM vision callback (batched, 5 concurrent)
174
+ 3. **Enrichment** — descriptions + entities submitted back to the server
175
+
176
+ ```typescript
177
+ import { MemoryClient, type EnrichmentCallbacks } from '@pyx-memory/client';
178
+
179
+ const memory = new MemoryClient('http://localhost:7822', apiKey);
180
+
181
+ const enrichment: EnrichmentCallbacks = {
182
+ // Required: describe each image using LLM vision
183
+ describeImage: async (imageBuffer, meta) => {
184
+ // imageBuffer is ArrayBuffer of the extracted PNG/JPEG
185
+ // meta has: imageId, pageNumber, width, height, mimeType
186
+ const description = await myVisionModel.describe(imageBuffer);
187
+ return description;
188
+ },
189
+ // Optional: extract entities from all descriptions at once
190
+ extractEntities: async (descriptions) => ({
191
+ entities: [{ name: 'Revenue', type: 'METRIC' }],
192
+ relationships: [{ source: 'Revenue', target: 'Q4', type: 'RELATED_TO' }],
193
+ }),
194
+ };
195
+
196
+ const result = await memory.ingestFile(pdfFile, { enrichment });
197
+ // result.enrichment is defined when PDF had images
198
+ // Text chunks are already stored from Phase 1
199
+ // Image descriptions are stored from Phase 3
200
+ ```
201
+
202
+ Without `enrichment` callbacks, `ingestFile()` behaves identically to before — fully backwards compatible.
@@ -28,6 +28,13 @@
28
28
  | `MemoryListParams` | `{ page?, limit?, type?, agentId? }` | `@pyx-memory/client` |
29
29
  | `MemoryListResult` | `{ entries, totalCount, page, limit }` | `@pyx-memory/client` |
30
30
  | `IngestionResult` | `{ filename, chunks, totalCharacters }` | `@pyx-memory/client` |
31
+ | `FileIngestResult` | `IngestionResult & { enrichment?: EnrichmentPending }` | `@pyx-memory/shared` |
32
+ | `EnrichmentPending` | `{ fileId, token, expiresAt, images: ExtractedImageMeta[] }` | `@pyx-memory/shared` |
33
+ | `ExtractedImageMeta` | `{ imageId, pageNumber, width, height, mimeType }` | `@pyx-memory/shared` |
34
+ | `EnrichmentCallbacks` | `{ describeImage, extractEntities? }` | `@pyx-memory/client` |
35
+ | `EnrichRequest` | `{ imageDescriptions, entities?, relationships? }` | `@pyx-memory/shared` |
36
+ | `EnrichResult` | `{ entryIds, entitiesStored, relationshipsStored }` | `@pyx-memory/shared` |
37
+ | `ImageDescription` | `{ imageId, description }` | `@pyx-memory/shared` |
31
38
  | `MemoryServerError` | `Error` with `.status` and `.isNotFound` | `@pyx-memory/client` |
32
39
  | `GraphNode` | `{ id, name, type, properties, memoryEntryIds }` | `@pyx-memory/shared` |
33
40
  | `GraphTraversalResult` | `{ nodes, relationships, paths }` | `@pyx-memory/shared` |
@@ -86,8 +93,13 @@ class MemoryClient implements ExtendedMemoryInterface {
86
93
  graphEdges(): Promise<{ stats: { nodeCount: number; edgeCount: number } }>;
87
94
  graphQuery(query: { nodeId: string; depth?: number }): Promise<GraphTraversalResult>;
88
95
 
89
- // File ingestion (multipart upload to server)
90
- ingestFile(file: File): Promise<IngestionResult>;
96
+ // File ingestion with optional two-phase enrichment
97
+ // Without callbacks: standard single-phase ingest (backwards compatible)
98
+ // With callbacks: fetches extracted images, calls describeImage, submits enrichment
99
+ ingestFile(file: File, options?: {
100
+ description?: string;
101
+ enrichment?: EnrichmentCallbacks;
102
+ }): Promise<FileIngestResult>;
91
103
 
92
104
  // Bi-temporal queries
93
105
  queryAsOf(asOf: string, filters?: TemporalQueryFilters): Promise<MemoryEntry[]>;