@roj-ai/sdk 0.1.16 → 0.1.18
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/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +12 -2
- package/dist/bootstrap.js.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/image/types.d.ts +2 -0
- package/dist/core/image/types.d.ts.map +1 -1
- package/dist/core/image/vips-resizer.d.ts.map +1 -1
- package/dist/core/image/vips-resizer.js +12 -11
- package/dist/core/image/vips-resizer.js.map +1 -1
- package/dist/info.d.ts +7 -0
- package/dist/info.d.ts.map +1 -0
- package/dist/info.js +8 -0
- package/dist/info.js.map +1 -0
- package/dist/plugins/uploads/preprocessors/image-classifier.d.ts +20 -0
- package/dist/plugins/uploads/preprocessors/image-classifier.d.ts.map +1 -1
- package/dist/plugins/uploads/preprocessors/image-classifier.js +78 -26
- package/dist/plugins/uploads/preprocessors/image-classifier.js.map +1 -1
- package/dist/plugins/uploads/preprocessors/index.d.ts +1 -0
- package/dist/plugins/uploads/preprocessors/index.d.ts.map +1 -1
- package/dist/plugins/uploads/preprocessors/index.js +1 -0
- package/dist/plugins/uploads/preprocessors/index.js.map +1 -1
- package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts +52 -5
- package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts.map +1 -1
- package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js +152 -97
- package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js.map +1 -1
- package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts +71 -0
- package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts.map +1 -0
- package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js +274 -0
- package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js.map +1 -0
- package/dist/transport/http/app.d.ts.map +1 -1
- package/dist/transport/http/app.js +6 -1
- package/dist/transport/http/app.js.map +1 -1
- package/package.json +2 -2
- package/src/bootstrap.ts +12 -2
- package/src/config.ts +13 -0
- package/src/core/image/types.ts +2 -0
- package/src/core/image/vips-resizer.ts +12 -11
- package/src/info.ts +9 -0
- package/src/plugins/uploads/preprocessors/image-classifier.ts +93 -27
- package/src/plugins/uploads/preprocessors/index.ts +1 -0
- package/src/plugins/uploads/preprocessors/markitdown-preprocessor.ts +173 -108
- package/src/plugins/uploads/preprocessors/pdf-preprocessor.ts +342 -0
- package/src/transport/http/app.ts +6 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF Preprocessor
|
|
3
|
+
*
|
|
4
|
+
* Dedicated PDF pipeline:
|
|
5
|
+
*
|
|
6
|
+
* 1. Text extraction via `pdftotext` (poppler-utils, C++) — ~1 s for a
|
|
7
|
+
* 3 MB PDF. Replaces markitdown/pdfminer.six (~22 s for the same file)
|
|
8
|
+
* because PDFs in practice don't carry the rich markdown structure
|
|
9
|
+
* that justifies the slower backend.
|
|
10
|
+
*
|
|
11
|
+
* 2. Image extraction via `pdfimages -all` — keeps the original embedded
|
|
12
|
+
* format (JPEG stays JPEG) instead of re-encoding everything to PNG
|
|
13
|
+
* (~10× faster, much smaller files).
|
|
14
|
+
*
|
|
15
|
+
* 3. Text and image extraction run in parallel.
|
|
16
|
+
*
|
|
17
|
+
* 4. Images stream into the classifier as soon as `pdfimages` writes them
|
|
18
|
+
* to disk — the classifier doesn't wait for the whole extraction to
|
|
19
|
+
* finish. A density filter (bytes/pixel) drops alpha masks and overlay
|
|
20
|
+
* layers before the vision call.
|
|
21
|
+
*/
|
|
22
|
+
import { dirname } from 'node:path';
|
|
23
|
+
import { Err, Ok } from '../../../lib/utils/result.js';
|
|
24
|
+
import { getImageDimensions, guessImageMime, IMAGE_EXT_RE, MIN_IMAGE_DENSITY_BYTES_PER_PX, MIN_IMAGE_PIXELS, shouldClassifyImage, } from './markitdown-preprocessor.js';
|
|
25
|
+
const PDFTOTEXT_TIMEOUT_MS = 60_000;
|
|
26
|
+
const PDFIMAGES_TIMEOUT_MS = 5 * 60_000;
|
|
27
|
+
const MAX_IMAGES = 20;
|
|
28
|
+
const CLASSIFY_CONCURRENCY = 10;
|
|
29
|
+
const STREAM_POLL_INTERVAL_MS = 250;
|
|
30
|
+
const SUPPORTED_MIME_TYPES = ['application/pdf'];
|
|
31
|
+
export class PdfPreprocessor {
|
|
32
|
+
name = 'pdf';
|
|
33
|
+
supportedMimeTypes = SUPPORTED_MIME_TYPES;
|
|
34
|
+
registry;
|
|
35
|
+
logger;
|
|
36
|
+
fs;
|
|
37
|
+
processRunner;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.registry = config.registry;
|
|
40
|
+
this.logger = config.logger;
|
|
41
|
+
this.fs = config.fs;
|
|
42
|
+
this.processRunner = config.process;
|
|
43
|
+
}
|
|
44
|
+
async process(filePath, mimeType, ctx) {
|
|
45
|
+
const totalStart = Date.now();
|
|
46
|
+
this.logger.info('PDF processing started', { filePath });
|
|
47
|
+
const contentPathResult = ctx.files.realPath('content.md');
|
|
48
|
+
if (!contentPathResult.ok)
|
|
49
|
+
return Err(new Error('Failed to resolve content output path'));
|
|
50
|
+
const imagesDirResult = ctx.files.scoped('images').realPath('');
|
|
51
|
+
if (!imagesDirResult.ok)
|
|
52
|
+
return Err(new Error('Failed to resolve images output path'));
|
|
53
|
+
await this.fs.mkdir(dirname(contentPathResult.value), { recursive: true });
|
|
54
|
+
await this.fs.mkdir(imagesDirResult.value, { recursive: true });
|
|
55
|
+
// Run text extraction and image extraction (with streaming classification)
|
|
56
|
+
// in parallel. They share no state and don't block each other.
|
|
57
|
+
const [textResult, images] = await Promise.all([
|
|
58
|
+
this.extractText(filePath, contentPathResult.value),
|
|
59
|
+
this.extractAndClassifyImages(filePath, imagesDirResult.value, ctx),
|
|
60
|
+
]);
|
|
61
|
+
const markdown = textResult.ok ? textResult.value : '';
|
|
62
|
+
const derivedPaths = ['content.md'];
|
|
63
|
+
const imageEntries = [];
|
|
64
|
+
for (const img of images) {
|
|
65
|
+
derivedPaths.push(img.relativePath);
|
|
66
|
+
imageEntries.push(`- ${img.relativePath} — ${img.description}`);
|
|
67
|
+
}
|
|
68
|
+
const manifestLines = ['Extracted files:'];
|
|
69
|
+
manifestLines.push(`- content.md (text, ${markdown.length} chars)`);
|
|
70
|
+
manifestLines.push(...imageEntries);
|
|
71
|
+
this.logger.info('PDF processing complete', {
|
|
72
|
+
filePath,
|
|
73
|
+
contentLength: markdown.length,
|
|
74
|
+
imagesClassified: imageEntries.length,
|
|
75
|
+
totalDurationMs: Date.now() - totalStart,
|
|
76
|
+
});
|
|
77
|
+
return Ok({
|
|
78
|
+
extractedContent: manifestLines.join('\n'),
|
|
79
|
+
derivedPaths,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Extract plain text via pdftotext. Writes to content.md verbatim — no
|
|
84
|
+
* markdown structure to preserve, but the file extension stays .md for
|
|
85
|
+
* consistency with the markitdown pipeline (downstream consumers expect
|
|
86
|
+
* "content.md" in the upload directory).
|
|
87
|
+
*
|
|
88
|
+
* `-layout` preserves the original visual layout (columns, tables),
|
|
89
|
+
* which is what users typically expect when looking at PDFs.
|
|
90
|
+
*/
|
|
91
|
+
async extractText(filePath, outputPath) {
|
|
92
|
+
const start = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
await this.processRunner.execFile('pdftotext', ['-layout', filePath, outputPath], { timeout: PDFTOTEXT_TIMEOUT_MS, maxBuffer: 50 * 1024 * 1024 });
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
this.logger.warn('pdftotext failed', { filePath, durationMs: Date.now() - start, error: message });
|
|
99
|
+
return Err(new Error(`pdftotext failed: ${message}`));
|
|
100
|
+
}
|
|
101
|
+
let text = '';
|
|
102
|
+
try {
|
|
103
|
+
text = await this.fs.readFile(outputPath, 'utf-8');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// File missing — pdftotext succeeded but produced no output.
|
|
107
|
+
}
|
|
108
|
+
this.logger.info('pdftotext complete', {
|
|
109
|
+
filePath,
|
|
110
|
+
durationMs: Date.now() - start,
|
|
111
|
+
contentLength: text.length,
|
|
112
|
+
});
|
|
113
|
+
return Ok(text);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract images via pdfimages and classify them as they appear on disk.
|
|
117
|
+
*
|
|
118
|
+
* pdfimages writes files atomically per image (open temp, write, rename
|
|
119
|
+
* to final name), so polling `readdir` is safe — we either see a name or
|
|
120
|
+
* we don't, never a half-written file.
|
|
121
|
+
*
|
|
122
|
+
* Streaming overlaps the extraction tail with the first classification
|
|
123
|
+
* batches. Hard cap of MAX_IMAGES applies across the *filtered* set: as
|
|
124
|
+
* soon as MAX_IMAGES images have passed the density filter, further
|
|
125
|
+
* candidates are stat-checked but not classified.
|
|
126
|
+
*
|
|
127
|
+
* `-all` keeps the embedded format (JPEG, JBIG2, JP2). We only classify
|
|
128
|
+
* those Anthropic vision accepts (PNG/JPEG/GIF/WebP); other formats are
|
|
129
|
+
* extracted to disk for reference but skipped at the classification step.
|
|
130
|
+
*/
|
|
131
|
+
async extractAndClassifyImages(filePath, imagesDir, ctx) {
|
|
132
|
+
const extractStart = Date.now();
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const acceptedQueue = [];
|
|
135
|
+
const classifyPromises = [];
|
|
136
|
+
let stopAccepting = false;
|
|
137
|
+
let droppedByDensity = 0;
|
|
138
|
+
let skippedUnsupportedExt = 0;
|
|
139
|
+
// Active classification gate — caps in-flight vision calls.
|
|
140
|
+
let active = 0;
|
|
141
|
+
const waiters = [];
|
|
142
|
+
const acquire = () => new Promise(resolve => {
|
|
143
|
+
if (active < CLASSIFY_CONCURRENCY) {
|
|
144
|
+
active++;
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
else
|
|
148
|
+
waiters.push(() => { active++; resolve(); });
|
|
149
|
+
});
|
|
150
|
+
const release = () => {
|
|
151
|
+
active--;
|
|
152
|
+
const next = waiters.shift();
|
|
153
|
+
if (next)
|
|
154
|
+
next();
|
|
155
|
+
};
|
|
156
|
+
const classifyOne = async (name) => {
|
|
157
|
+
await acquire();
|
|
158
|
+
try {
|
|
159
|
+
const mime = guessImageMime(name);
|
|
160
|
+
const fullPath = `${imagesDir}/${name}`;
|
|
161
|
+
const imageStore = ctx.files.scoped('images');
|
|
162
|
+
let description = mime;
|
|
163
|
+
const classifier = this.registry.getForMimeType(mime);
|
|
164
|
+
if (classifier) {
|
|
165
|
+
const result = await classifier.process(fullPath, mime, {
|
|
166
|
+
files: ctx.files.scoped(`images/${name}-meta`),
|
|
167
|
+
});
|
|
168
|
+
if (result.ok && result.value.extractedContent) {
|
|
169
|
+
description = result.value.extractedContent;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { relativePath: `images/${name}`, description };
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
release();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const inspectAndMaybeClassify = async (name) => {
|
|
179
|
+
if (seen.has(name) || stopAccepting)
|
|
180
|
+
return;
|
|
181
|
+
seen.add(name);
|
|
182
|
+
if (!IMAGE_EXT_RE.test(name)) {
|
|
183
|
+
skippedUnsupportedExt++;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const fullPath = `${imagesDir}/${name}`;
|
|
187
|
+
let sizeBytes = 0;
|
|
188
|
+
try {
|
|
189
|
+
sizeBytes = (await this.fs.stat(fullPath)).size;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const dims = await getImageDimensions(fullPath, this.processRunner);
|
|
195
|
+
const hasDims = dims !== null;
|
|
196
|
+
const passesFilter = hasDims
|
|
197
|
+
? shouldClassifyImage({ width: dims.width, height: dims.height, sizeBytes })
|
|
198
|
+
: sizeBytes >= MIN_IMAGE_PIXELS * MIN_IMAGE_DENSITY_BYTES_PER_PX; // fall back to absolute byte floor
|
|
199
|
+
if (!passesFilter) {
|
|
200
|
+
droppedByDensity++;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
acceptedQueue.push({
|
|
204
|
+
name,
|
|
205
|
+
sizeBytes,
|
|
206
|
+
width: dims?.width ?? 0,
|
|
207
|
+
height: dims?.height ?? 0,
|
|
208
|
+
});
|
|
209
|
+
if (acceptedQueue.length >= MAX_IMAGES) {
|
|
210
|
+
stopAccepting = true;
|
|
211
|
+
}
|
|
212
|
+
classifyPromises.push(classifyOne(name));
|
|
213
|
+
};
|
|
214
|
+
// Run pdfimages and a parallel poll loop. The poll calls readdir
|
|
215
|
+
// periodically and dispatches `inspectAndMaybeClassify` for newly
|
|
216
|
+
// appeared files; doing it this way avoids fs.watch quirks (some
|
|
217
|
+
// container filesystems don't deliver events).
|
|
218
|
+
const pdfimagesPromise = this.processRunner.execFile('pdfimages', ['-all', filePath, `${imagesDir}/img`], { timeout: PDFIMAGES_TIMEOUT_MS, maxBuffer: 1024 * 1024 }).then(() => true).catch((error) => {
|
|
219
|
+
this.logger.warn('pdfimages failed (will classify any partial output)', {
|
|
220
|
+
filePath,
|
|
221
|
+
durationMs: Date.now() - extractStart,
|
|
222
|
+
error: error instanceof Error ? error.message : String(error),
|
|
223
|
+
});
|
|
224
|
+
return false;
|
|
225
|
+
});
|
|
226
|
+
let extractionDone = false;
|
|
227
|
+
const poll = async () => {
|
|
228
|
+
while (!extractionDone) {
|
|
229
|
+
await this.scanAndDispatch(imagesDir, inspectAndMaybeClassify);
|
|
230
|
+
await sleep(STREAM_POLL_INTERVAL_MS);
|
|
231
|
+
}
|
|
232
|
+
// Final sweep — pick up anything that landed between the last poll
|
|
233
|
+
// and pdfimages exiting.
|
|
234
|
+
await this.scanAndDispatch(imagesDir, inspectAndMaybeClassify);
|
|
235
|
+
};
|
|
236
|
+
const pollPromise = poll();
|
|
237
|
+
const extractSucceeded = await pdfimagesPromise;
|
|
238
|
+
extractionDone = true;
|
|
239
|
+
await pollPromise;
|
|
240
|
+
this.logger.info(extractSucceeded ? 'pdfimages complete' : 'pdfimages failed (partial)', {
|
|
241
|
+
filePath,
|
|
242
|
+
durationMs: Date.now() - extractStart,
|
|
243
|
+
filesEmitted: seen.size,
|
|
244
|
+
passedFilter: acceptedQueue.length,
|
|
245
|
+
droppedByDensity,
|
|
246
|
+
skippedUnsupportedExt,
|
|
247
|
+
});
|
|
248
|
+
const classifyStart = Date.now();
|
|
249
|
+
const settled = await Promise.all(classifyPromises);
|
|
250
|
+
const images = settled.filter((r) => r !== null);
|
|
251
|
+
this.logger.info('PDF image classification complete', {
|
|
252
|
+
filePath,
|
|
253
|
+
count: images.length,
|
|
254
|
+
durationMs: Date.now() - classifyStart,
|
|
255
|
+
});
|
|
256
|
+
return images;
|
|
257
|
+
}
|
|
258
|
+
async scanAndDispatch(dir, handle) {
|
|
259
|
+
let entries;
|
|
260
|
+
try {
|
|
261
|
+
entries = await this.fs.readdir(dir);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// Fire dispatches in parallel — `inspectAndMaybeClassify` is internally
|
|
267
|
+
// idempotent for already-seen names.
|
|
268
|
+
await Promise.all(entries.map(handle));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function sleep(ms) {
|
|
272
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=pdf-preprocessor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf-preprocessor.js","sourceRoot":"","sources":["../../../../src/plugins/uploads/preprocessors/pdf-preprocessor.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/transport/http/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/transport/http/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAA;AAC7E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAA;AAQ5E;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG;IACpC,cAAc,EAAE,cAAc,CAAA;IAC9B,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iHAAiH;IACjH,oBAAoB,CAAC,EAAE,oBAAoB,CAAA;CAC3C,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG;IACpB,SAAS,EAAE;QACV,QAAQ,EAAE,WAAW,CAAA;KACrB,CAAA;CACD,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;AAEvD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,UAAU,GAAG,WAAW,CAEtD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,CA8E7D"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
7
|
import { cors } from 'hono/cors';
|
|
8
|
+
import { SDK_VERSION } from '../../info.js';
|
|
8
9
|
import { createBearerAuth } from './middleware/bearer-auth.js';
|
|
9
10
|
import { errorHandler } from './middleware/error-handler.js';
|
|
10
11
|
import { createFileRoutes } from './routes/files.js';
|
|
@@ -43,10 +44,14 @@ export function createApp(services) {
|
|
|
43
44
|
// Activity status for DO polling (protected)
|
|
44
45
|
// Returns lastActivityAt timestamp for the caller to determine if agent is active
|
|
45
46
|
app.get('/status', bearerAuth, async (c) => {
|
|
46
|
-
const { sessionRuntime } = getServices(c);
|
|
47
|
+
const { sessionRuntime, config } = getServices(c);
|
|
47
48
|
const stats = await sessionRuntime.getStats();
|
|
48
49
|
return c.json({
|
|
49
50
|
lastActivityAt: stats.lastActivityAt,
|
|
51
|
+
versions: {
|
|
52
|
+
sdk: SDK_VERSION,
|
|
53
|
+
runtime: config.agentRuntime ?? null,
|
|
54
|
+
},
|
|
50
55
|
stats: {
|
|
51
56
|
sessionCount: stats.sessionCount,
|
|
52
57
|
pendingAgents: stats.pendingAgents,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../../src/transport/http/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../../src/transport/http/app.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roj-ai/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"type-check": "tsc --noEmit"
|
|
136
136
|
},
|
|
137
137
|
"dependencies": {
|
|
138
|
-
"@roj-ai/transport": "^0.1.
|
|
138
|
+
"@roj-ai/transport": "^0.1.18",
|
|
139
139
|
"@hono/zod-validator": "0.7.6",
|
|
140
140
|
"hono": "4.12.5",
|
|
141
141
|
"ignore": "7.0.5",
|
package/src/bootstrap.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { RoutableLLMProvider } from '~/core/llm/routing-provider.js'
|
|
|
18
18
|
import type { Preset } from '~/core/preset/index.js'
|
|
19
19
|
import type { Platform } from '~/platform/index.js'
|
|
20
20
|
import { PreprocessorRegistry } from '~/plugins/uploads/preprocessor.js'
|
|
21
|
-
import { ImageClassifierPreprocessor, MarkitdownPreprocessor, ZipPreprocessor } from '~/plugins/uploads/preprocessors/index.js'
|
|
21
|
+
import { ImageClassifierPreprocessor, MarkitdownPreprocessor, PdfPreprocessor, ZipPreprocessor } from '~/plugins/uploads/preprocessors/index.js'
|
|
22
22
|
import type { Config } from './config.js'
|
|
23
23
|
import { SessionFileStore } from './core/file-store/file-store.js'
|
|
24
24
|
import type { SessionManager } from './core/sessions/session-manager.js'
|
|
@@ -121,7 +121,17 @@ export function bootstrap(config: Config, userConfig: RojConfig, platform: Platf
|
|
|
121
121
|
|
|
122
122
|
const preprocessorRegistry = new PreprocessorRegistry()
|
|
123
123
|
const imageClassifierGate = new Semaphore(config.imageClassifierConcurrency ?? 10)
|
|
124
|
-
|
|
124
|
+
// Dedicated resizer for the classification path: separate from the LLM
|
|
125
|
+
// provider's general-purpose ImageProcessor (which keeps a higher
|
|
126
|
+
// maxDimension for agent file-inspection tool calls). The classifier
|
|
127
|
+
// hands each image to the resizer with its own 1024px override.
|
|
128
|
+
const classifierImageResizer = new VipsImageResizer({ fs: platform.fs, process: platform.process, tmpDir: platform.tmpDir })
|
|
129
|
+
preprocessorRegistry.register(new ImageClassifierPreprocessor({ llmProvider, logger, fs: platform.fs, gate: imageClassifierGate, imageResizer: classifierImageResizer }))
|
|
130
|
+
// PdfPreprocessor must come before MarkitdownPreprocessor — both could
|
|
131
|
+
// match `application/pdf` in principle, but the registry uses first-hit
|
|
132
|
+
// and PdfPreprocessor's `pdftotext + pdfimages -all + streaming` pipeline
|
|
133
|
+
// is dramatically faster than markitdown's pdfminer.six backend.
|
|
134
|
+
preprocessorRegistry.register(new PdfPreprocessor({ registry: preprocessorRegistry, logger, fs: platform.fs, process: platform.process }))
|
|
125
135
|
preprocessorRegistry.register(new MarkitdownPreprocessor({ registry: preprocessorRegistry, logger, fs: platform.fs, process: platform.process }))
|
|
126
136
|
preprocessorRegistry.register(new ZipPreprocessor({ registry: preprocessorRegistry, logger, process: platform.process }))
|
|
127
137
|
|
package/src/config.ts
CHANGED
|
@@ -33,6 +33,19 @@ export interface Config {
|
|
|
33
33
|
/** Max concurrent vision LLM calls when classifying uploaded images. Default 10. */
|
|
34
34
|
imageClassifierConcurrency?: number
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Identity of the application embedding this SDK. Reported via `/status`
|
|
38
|
+
* so platform health-checks can surface "what's actually running" in debug
|
|
39
|
+
* tooling alongside the SDK's own version.
|
|
40
|
+
*
|
|
41
|
+
* sandbox-runtime sets this from its own package.json; other embedders
|
|
42
|
+
* (e.g. standalone-server, custom bundles) can supply their own.
|
|
43
|
+
*/
|
|
44
|
+
agentRuntime?: {
|
|
45
|
+
name: string
|
|
46
|
+
version: string
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
// Logging
|
|
37
50
|
logLevel: LogLevel
|
|
38
51
|
logFormat: 'console' | 'json'
|
package/src/core/image/types.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { ToolResultContent } from '~/core/llm/llm-log-types.js'
|
|
|
3
3
|
|
|
4
4
|
export interface ImageResizeOptions {
|
|
5
5
|
maxFileSizeBytes?: number
|
|
6
|
+
/** Override the resizer's default max dimension (long side, px). */
|
|
7
|
+
maxDimension?: number
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export interface ImageResizer {
|
|
@@ -24,9 +24,10 @@ export class VipsImageResizer implements ImageResizer {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async resize(filePath: string, mimeType: string, options?: ImageResizeOptions): Promise<ImageResizeResult> {
|
|
27
|
+
const effectiveMaxDimension = options?.maxDimension ?? this.maxDimension
|
|
27
28
|
try {
|
|
28
29
|
// Step 1: Dimension resize if needed
|
|
29
|
-
const result = await this.dimensionResize(filePath, mimeType)
|
|
30
|
+
const result = await this.dimensionResize(filePath, mimeType, effectiveMaxDimension)
|
|
30
31
|
|
|
31
32
|
// Step 2: If no size constraint, done
|
|
32
33
|
if (!options?.maxFileSizeBytes) return result
|
|
@@ -40,7 +41,7 @@ export class VipsImageResizer implements ImageResizer {
|
|
|
40
41
|
await this.fs.unlink(result.tempFile).catch(() => {})
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
return await this.compressToFit(filePath, options.maxFileSizeBytes)
|
|
44
|
+
return await this.compressToFit(filePath, options.maxFileSizeBytes, effectiveMaxDimension)
|
|
44
45
|
} catch (e) {
|
|
45
46
|
console.warn('[image-resize] failed, using original image:', e instanceof Error ? e.message : e)
|
|
46
47
|
return { path: filePath, mimeType }
|
|
@@ -57,9 +58,9 @@ export class VipsImageResizer implements ImageResizer {
|
|
|
57
58
|
return { width, height }
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
private async dimensionResize(filePath: string, mimeType: string): Promise<ImageResizeResult> {
|
|
61
|
+
private async dimensionResize(filePath: string, mimeType: string, maxDimension: number): Promise<ImageResizeResult> {
|
|
61
62
|
const dims = await this.getImageDimensions(filePath)
|
|
62
|
-
const needsResize = dims !== null && (dims.width >
|
|
63
|
+
const needsResize = dims !== null && (dims.width > maxDimension || dims.height > maxDimension)
|
|
63
64
|
|
|
64
65
|
// JPEGs within dimension limits pass through unchanged
|
|
65
66
|
if (mimeType === 'image/jpeg' && !needsResize) {
|
|
@@ -73,20 +74,20 @@ export class VipsImageResizer implements ImageResizer {
|
|
|
73
74
|
await this.process.execFile('vipsthumbnail', [
|
|
74
75
|
filePath,
|
|
75
76
|
'--size',
|
|
76
|
-
`${
|
|
77
|
+
`${maxDimension}x${maxDimension}`,
|
|
77
78
|
'-o',
|
|
78
79
|
outputPath,
|
|
79
80
|
], { timeout: 30_000 })
|
|
80
81
|
return { path: outputPath, mimeType: 'image/jpeg', tempFile: outputPath }
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
private async compressToFit(filePath: string, maxFileSizeBytes: number): Promise<ImageResizeResult> {
|
|
84
|
-
const halfDim = Math.floor(
|
|
84
|
+
private async compressToFit(filePath: string, maxFileSizeBytes: number, maxDimension: number): Promise<ImageResizeResult> {
|
|
85
|
+
const halfDim = Math.floor(maxDimension / 2)
|
|
85
86
|
const attempts = [
|
|
86
|
-
{ dimension:
|
|
87
|
-
{ dimension:
|
|
88
|
-
{ dimension:
|
|
89
|
-
{ dimension:
|
|
87
|
+
{ dimension: maxDimension, quality: 85 },
|
|
88
|
+
{ dimension: maxDimension, quality: 70 },
|
|
89
|
+
{ dimension: maxDimension, quality: 50 },
|
|
90
|
+
{ dimension: maxDimension, quality: 30 },
|
|
90
91
|
{ dimension: halfDim, quality: 70 },
|
|
91
92
|
{ dimension: halfDim, quality: 50 },
|
|
92
93
|
{ dimension: halfDim, quality: 30 },
|
package/src/info.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time SDK metadata. Read from this package's own package.json so the
|
|
3
|
+
* value tracks releases automatically — no risk of a forgotten string update
|
|
4
|
+
* after a version bump.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
8
|
+
|
|
9
|
+
export const SDK_VERSION = pkg.version as string
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Falls back to basic metadata if vision is not available.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ImageResizer } from '~/core/image/types.js'
|
|
8
9
|
import type { LLMProvider } from '~/core/llm/provider.js'
|
|
9
10
|
import { ModelId } from '~/core/llm/schema.js'
|
|
10
11
|
import type { Semaphore } from '~/lib/utils/concurrency.js'
|
|
@@ -14,6 +15,15 @@ import type { FileSystem } from '~/platform/fs.js'
|
|
|
14
15
|
import type { Logger } from '../../../lib/logger/logger.js'
|
|
15
16
|
import type { Preprocessor, PreprocessorContext, PreprocessorResult } from '../preprocessor.js'
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Anthropic vision API internally downsamples images to ~1568px long side.
|
|
20
|
+
* Anything larger just wastes bandwidth and LLM tokens. For 1–2 sentence
|
|
21
|
+
* descriptions, 1024px is more than enough detail.
|
|
22
|
+
*/
|
|
23
|
+
const CLASSIFY_MAX_DIMENSION = 1024
|
|
24
|
+
/** Hard cap to keep base64 payloads small (LLM still accepts up to 5MB). */
|
|
25
|
+
const CLASSIFY_MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024
|
|
26
|
+
|
|
17
27
|
// ============================================================================
|
|
18
28
|
// Configuration
|
|
19
29
|
// ============================================================================
|
|
@@ -27,6 +37,14 @@ export interface ImageClassifierConfig {
|
|
|
27
37
|
logger: Logger
|
|
28
38
|
/** FileSystem adapter (for checking + reading image files) */
|
|
29
39
|
fs: FileSystem
|
|
40
|
+
/**
|
|
41
|
+
* Optional resizer. When provided, classifier downscales images to
|
|
42
|
+
* ~1024px before sending to the vision LLM. Skipping this is fine for
|
|
43
|
+
* tests; in production it dramatically cuts payload size and token cost
|
|
44
|
+
* (brand PDFs embed 2000–3000px JPEGs the model would otherwise see at
|
|
45
|
+
* full resolution).
|
|
46
|
+
*/
|
|
47
|
+
imageResizer?: ImageResizer
|
|
30
48
|
/** Whether to skip vision and just return metadata */
|
|
31
49
|
skipVision?: boolean
|
|
32
50
|
/**
|
|
@@ -54,6 +72,7 @@ export class ImageClassifierPreprocessor implements Preprocessor {
|
|
|
54
72
|
private readonly visionModel: ModelId
|
|
55
73
|
private readonly logger: Logger
|
|
56
74
|
private readonly fs: FileSystem
|
|
75
|
+
private readonly imageResizer: ImageResizer | undefined
|
|
57
76
|
private readonly skipVision: boolean
|
|
58
77
|
private readonly gate: Semaphore | undefined
|
|
59
78
|
|
|
@@ -62,6 +81,7 @@ export class ImageClassifierPreprocessor implements Preprocessor {
|
|
|
62
81
|
this.visionModel = config.visionModel ? ModelId(config.visionModel) : ModelId('anthropic/claude-haiku-4.5')
|
|
63
82
|
this.logger = config.logger
|
|
64
83
|
this.fs = config.fs
|
|
84
|
+
this.imageResizer = config.imageResizer
|
|
65
85
|
this.skipVision = config.skipVision ?? false
|
|
66
86
|
this.gate = config.gate
|
|
67
87
|
}
|
|
@@ -146,36 +166,41 @@ export class ImageClassifierPreprocessor implements Preprocessor {
|
|
|
146
166
|
mimeType: string,
|
|
147
167
|
): Promise<string | null> {
|
|
148
168
|
try {
|
|
149
|
-
|
|
150
|
-
const inferenceCall = () => this.llmProvider.inference({
|
|
151
|
-
model: this.visionModel,
|
|
152
|
-
systemPrompt: 'You are an image description assistant. Describe images concisely in 1-2 sentences.',
|
|
153
|
-
messages: [
|
|
154
|
-
{
|
|
155
|
-
role: 'user',
|
|
156
|
-
content: [
|
|
157
|
-
{
|
|
158
|
-
type: 'text',
|
|
159
|
-
text: 'Please describe this image concisely in 1-2 sentences. Focus on the main subject and any text visible.',
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
type: 'image_url',
|
|
163
|
-
imageUrl: { url: `file://${filePath}` },
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
maxTokens: 200,
|
|
169
|
-
temperature: 0.3,
|
|
170
|
-
})
|
|
169
|
+
const { url: imageUrl, cleanup } = await this.prepareImageUrl(filePath, mimeType)
|
|
171
170
|
|
|
172
|
-
|
|
171
|
+
try {
|
|
172
|
+
const inferenceCall = () => this.llmProvider.inference({
|
|
173
|
+
model: this.visionModel,
|
|
174
|
+
systemPrompt: 'You are an image description assistant. Describe images concisely in 1-2 sentences.',
|
|
175
|
+
messages: [
|
|
176
|
+
{
|
|
177
|
+
role: 'user',
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: 'Please describe this image concisely in 1-2 sentences. Focus on the main subject and any text visible.',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: 'image_url',
|
|
185
|
+
imageUrl: { url: imageUrl },
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
maxTokens: 200,
|
|
191
|
+
temperature: 0.3,
|
|
192
|
+
})
|
|
173
193
|
|
|
174
|
-
|
|
175
|
-
return result.value.content.trim()
|
|
176
|
-
}
|
|
194
|
+
const result = await (this.gate ? this.gate.run(inferenceCall) : inferenceCall())
|
|
177
195
|
|
|
178
|
-
|
|
196
|
+
if (result.ok && result.value.content) {
|
|
197
|
+
return result.value.content.trim()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null
|
|
201
|
+
} finally {
|
|
202
|
+
await cleanup()
|
|
203
|
+
}
|
|
179
204
|
} catch (error) {
|
|
180
205
|
this.logger.warn('Vision inference failed', {
|
|
181
206
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -184,6 +209,47 @@ export class ImageClassifierPreprocessor implements Preprocessor {
|
|
|
184
209
|
}
|
|
185
210
|
}
|
|
186
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Pre-resize the image for vision classification.
|
|
214
|
+
*
|
|
215
|
+
* Returns either a `data:` URL with the resized JPEG (when a resizer is
|
|
216
|
+
* available) or a `file://` URL fallback (LLM provider will resolve it via
|
|
217
|
+
* the global ImageProcessor, with its bigger maxDimension default).
|
|
218
|
+
*
|
|
219
|
+
* Cleanup removes any temp file produced by the resizer.
|
|
220
|
+
*/
|
|
221
|
+
private async prepareImageUrl(
|
|
222
|
+
filePath: string,
|
|
223
|
+
mimeType: string,
|
|
224
|
+
): Promise<{ url: string; cleanup: () => Promise<void> }> {
|
|
225
|
+
if (!this.imageResizer) {
|
|
226
|
+
return { url: `file://${filePath}`, cleanup: async () => {} }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const resized = await this.imageResizer.resize(filePath, mimeType, {
|
|
231
|
+
maxDimension: CLASSIFY_MAX_DIMENSION,
|
|
232
|
+
maxFileSizeBytes: CLASSIFY_MAX_FILE_SIZE_BYTES,
|
|
233
|
+
})
|
|
234
|
+
const buffer = await this.fs.readFile(resized.path)
|
|
235
|
+
const base64 = buffer.toString('base64')
|
|
236
|
+
return {
|
|
237
|
+
url: `data:${resized.mimeType};base64,${base64}`,
|
|
238
|
+
cleanup: async () => {
|
|
239
|
+
if (resized.tempFile) {
|
|
240
|
+
await this.fs.unlink(resized.tempFile).catch(() => {})
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
this.logger.warn('Pre-resize for classification failed, falling back to file:// URL', {
|
|
246
|
+
filePath,
|
|
247
|
+
error: error instanceof Error ? error.message : String(error),
|
|
248
|
+
})
|
|
249
|
+
return { url: `file://${filePath}`, cleanup: async () => {} }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
187
253
|
private formatSize(bytes: number): string {
|
|
188
254
|
if (bytes < 1024) return `${bytes}B`
|
|
189
255
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
|
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
|
|
5
5
|
export { createImageClassifierPreprocessor, type ImageClassifierConfig, ImageClassifierPreprocessor } from './image-classifier.js'
|
|
6
6
|
export { MarkitdownPreprocessor, type MarkitdownPreprocessorConfig } from './markitdown-preprocessor.js'
|
|
7
|
+
export { PdfPreprocessor, type PdfPreprocessorConfig } from './pdf-preprocessor.js'
|
|
7
8
|
export { ZipPreprocessor, type ZipPreprocessorConfig } from './zip-preprocessor.js'
|