@pyxmate/memory 0.3.1-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.1-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",
@@ -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) |
@@ -70,9 +74,12 @@ const client = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_
70
74
  2. Text is extracted (documents) or description is used (images)
71
75
  3. Content is chunked and stored in SQLite + vector for semantic search
72
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
73
78
 
74
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)`).
75
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
+
76
83
  ### Example: ingest a document
77
84
 
78
85
  ```bash
@@ -93,7 +100,7 @@ curl -X POST {{ENDPOINT}}/api/memory/ingest/file \
93
100
  ### SDK usage
94
101
 
95
102
  ```typescript
96
- // Document
103
+ // Document (text-only)
97
104
  const doc = new File([buffer], 'report.pdf', { type: 'application/pdf' });
98
105
  await memory.ingestFile(doc);
99
106
 
@@ -102,8 +109,76 @@ const img = new File([imgBuffer], 'photo.png', { type: 'image/png' });
102
109
  await memory.ingestFile(img, {
103
110
  description: 'A photo of the whiteboard from the architecture meeting',
104
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
105
145
  ```
106
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
+
107
182
  ---
108
183
 
109
184
  ## Entity Extraction (Knowledge Graph)
@@ -173,3 +248,4 @@ All responses follow: `{ success: boolean, data?: T, error?: string }`
173
248
  | `NODE_ENV` | `development` | Set to `production` to mask 5xx error details and enable HSTS |
174
249
  | `PII_POLICY` | `flag` | PII handling: `flag` (detect + tag), `redact` (replace with [REDACTED]), `block` (reject 400) |
175
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,9 +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
- // For images: pass { description } from LLM vision to enable semantic search
91
- ingestFile(file: File, options?: { description?: string }): 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>;
92
103
 
93
104
  // Bi-temporal queries
94
105
  queryAsOf(asOf: string, filters?: TemporalQueryFilters): Promise<MemoryEntry[]>;