@juspay/neurolink 12.14.20 → 12.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/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +386 -386
- package/dist/core/baseProvider.d.ts +6 -3
- package/dist/core/baseProvider.js +7 -3
- package/dist/providers/amazonBedrock/client.d.ts +7 -5
- package/dist/providers/amazonBedrock/client.js +125 -16
- package/dist/providers/cohere.d.ts +2 -2
- package/dist/providers/cohere.js +5 -1
- package/dist/providers/googleAiStudio/client.d.ts +3 -3
- package/dist/providers/googleAiStudio/client.js +6 -2
- package/dist/providers/googleVertex/client.d.ts +2 -2
- package/dist/providers/googleVertex/client.js +5 -1
- package/dist/providers/jina.d.ts +2 -2
- package/dist/providers/jina.js +5 -1
- package/dist/providers/litellm/client.d.ts +2 -2
- package/dist/providers/litellm/client.js +5 -1
- package/dist/providers/ollama/client.d.ts +2 -2
- package/dist/providers/ollama/client.js +5 -1
- package/dist/providers/openAI/client.d.ts +3 -3
- package/dist/providers/openAI/client.js +6 -2
- package/dist/providers/voyage.d.ts +2 -2
- package/dist/providers/voyage.js +5 -1
- package/dist/rag/document/MDocument.d.ts +19 -1
- package/dist/rag/document/MDocument.js +30 -3
- package/dist/rag/document/imageLoader.d.ts +55 -0
- package/dist/rag/document/imageLoader.js +183 -0
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +2 -0
- package/dist/rag/pipeline/RAGPipeline.d.ts +54 -1
- package/dist/rag/pipeline/RAGPipeline.js +358 -1
- package/dist/rag/ragIntegration.js +146 -25
- package/dist/types/generate.d.ts +9 -0
- package/dist/types/providers.d.ts +21 -1
- package/dist/types/rag.d.ts +144 -0
- package/dist/types/rag.js +12 -1
- package/dist/utils/fileDetector.d.ts +4 -0
- package/dist/utils/fileDetector.js +65 -52
- package/dist/utils/messageBuilder.js +143 -76
- package/package.json +2 -1
|
@@ -284,7 +284,27 @@ export class FileDetector {
|
|
|
284
284
|
[ATTR.FILE_SIZE_BYTES]: inputSizeBytes,
|
|
285
285
|
},
|
|
286
286
|
}, async (span) => {
|
|
287
|
-
|
|
287
|
+
// A URL used to be detected before it was loaded: MimeTypeStrategy
|
|
288
|
+
// issued a HEAD for Content-Type, then loadFromURL issued a GET for
|
|
289
|
+
// the body. Fetch it once and reuse the GET headers for the MIME
|
|
290
|
+
// strategy. A caller's explicit mimetypeHint remains authoritative,
|
|
291
|
+
// while a server header still comes after magic bytes.
|
|
292
|
+
const isUrlInput = typeof input === "string" &&
|
|
293
|
+
(input.startsWith("http://") || input.startsWith("https://"));
|
|
294
|
+
const loadedUrl = isUrlInput
|
|
295
|
+
? await FileDetector.loadUrl(input, options)
|
|
296
|
+
: undefined;
|
|
297
|
+
const detection = loadedUrl
|
|
298
|
+
? {
|
|
299
|
+
...(await FileDetector.detect(loadedUrl.content, {
|
|
300
|
+
...options,
|
|
301
|
+
filenameHint: options?.filenameHint ||
|
|
302
|
+
FileDetector.deriveInputFilename(input),
|
|
303
|
+
}, loadedUrl.contentType, options?.filenameHint ||
|
|
304
|
+
FileDetector.deriveInputFilename(input))),
|
|
305
|
+
source: "url",
|
|
306
|
+
}
|
|
307
|
+
: await FileDetector.detect(input, options);
|
|
288
308
|
span.setAttribute(ATTR.FILE_CATEGORY, detection.type);
|
|
289
309
|
span.setAttribute(ATTR.FILE_MIMETYPE, detection.mimeType || "unknown");
|
|
290
310
|
span.setAttribute(ATTR.FILE_CONFIDENCE, detection.metadata.confidence);
|
|
@@ -292,7 +312,8 @@ export class FileDetector {
|
|
|
292
312
|
// FD-018: Comprehensive fallback parsing for extension-less files
|
|
293
313
|
if (options?.allowedTypes &&
|
|
294
314
|
!options.allowedTypes.includes(detection.type)) {
|
|
295
|
-
const content =
|
|
315
|
+
const content = loadedUrl?.content ??
|
|
316
|
+
(await FileDetector.loadContent(input, detection, options));
|
|
296
317
|
const errors = [];
|
|
297
318
|
for (const allowedType of options.allowedTypes) {
|
|
298
319
|
try {
|
|
@@ -322,7 +343,8 @@ export class FileDetector {
|
|
|
322
343
|
FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
|
|
323
344
|
return result;
|
|
324
345
|
}
|
|
325
|
-
const content =
|
|
346
|
+
const content = loadedUrl?.content ??
|
|
347
|
+
(await FileDetector.loadContent(input, detection, options));
|
|
326
348
|
const csvOptions = options?.csvOptions;
|
|
327
349
|
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions);
|
|
328
350
|
FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
|
|
@@ -619,7 +641,7 @@ export class FileDetector {
|
|
|
619
641
|
* Detect file type using multi-strategy approach
|
|
620
642
|
* Stops at first strategy with confidence >= threshold (default: 80%)
|
|
621
643
|
*/
|
|
622
|
-
static async detect(input, options) {
|
|
644
|
+
static async detect(input, options, responseMimeType, extensionInput) {
|
|
623
645
|
// Short-circuit on a trustworthy caller-provided mimetype hint. This is
|
|
624
646
|
// the eager-path counterpart to FileReferenceRegistry.register()'s hint
|
|
625
647
|
// handling — necessary for tiny files (<= TINY_MAX) that skip the lazy
|
|
@@ -648,13 +670,15 @@ export class FileDetector {
|
|
|
648
670
|
const confidenceThreshold = options?.confidenceThreshold ?? 80;
|
|
649
671
|
const strategies = [
|
|
650
672
|
new MagicBytesStrategy(),
|
|
651
|
-
new MimeTypeStrategy(),
|
|
673
|
+
new MimeTypeStrategy(responseMimeType),
|
|
652
674
|
new ExtensionStrategy(),
|
|
653
675
|
new ContentHeuristicStrategy(),
|
|
654
676
|
];
|
|
655
677
|
let best = null;
|
|
656
678
|
for (const strategy of strategies) {
|
|
657
|
-
const result = await strategy.detect(
|
|
679
|
+
const result = await strategy.detect(strategy instanceof ExtensionStrategy && extensionInput
|
|
680
|
+
? extensionInput
|
|
681
|
+
: input);
|
|
658
682
|
if (!best || result.metadata.confidence > best.metadata.confidence) {
|
|
659
683
|
best = result;
|
|
660
684
|
}
|
|
@@ -1502,52 +1526,14 @@ export class FileDetector {
|
|
|
1502
1526
|
* Load file from URL with automatic retry on transient network errors
|
|
1503
1527
|
*/
|
|
1504
1528
|
static async loadFromURL(url, options) {
|
|
1529
|
+
return (await FileDetector.loadUrl(url, options)).content;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Fetch a URL once and retain its response MIME type for detection.
|
|
1533
|
+
*/
|
|
1534
|
+
static async loadUrl(url, options) {
|
|
1505
1535
|
const maxSize = options?.maxSize || 200 * 1024 * 1024; // 200MB default (matches Curator memory-safety cap)
|
|
1506
1536
|
const timeout = options?.timeout || FileDetector.DEFAULT_NETWORK_TIMEOUT;
|
|
1507
|
-
// #317: pre-flight HEAD to reject an oversized file BEFORE downloading any
|
|
1508
|
-
// body. content-length is advisory (chunked responses omit it), so a
|
|
1509
|
-
// missing/invalid header — or a server that refuses HEAD — falls through to
|
|
1510
|
-
// the streaming byte guard below; only a genuine oversize rejection stops
|
|
1511
|
-
// the GET from ever running.
|
|
1512
|
-
//
|
|
1513
|
-
// #323: skip the pre-flight entirely when this exact URL was recently seen
|
|
1514
|
-
// (its Content-Type is still cached, whether from a prior loadFromURL GET
|
|
1515
|
-
// or a MimeTypeStrategy HEAD) — issuing a fresh HEAD here would defeat the
|
|
1516
|
-
// whole point of that cache. The streaming byte guard in the GET below
|
|
1517
|
-
// still enforces maxSize even without a pre-flight, so this doesn't remove
|
|
1518
|
-
// the oversize protection — it only skips the redundant round-trip for a
|
|
1519
|
-
// URL we've already been talking to within the last 60s.
|
|
1520
|
-
if (getCachedUrlContentType(url, Date.now()) === undefined) {
|
|
1521
|
-
try {
|
|
1522
|
-
const head = await request(url, {
|
|
1523
|
-
dispatcher: redirectFollowingDispatcher(5),
|
|
1524
|
-
method: "HEAD",
|
|
1525
|
-
headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
|
|
1526
|
-
bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
|
|
1527
|
-
});
|
|
1528
|
-
// Drain/close the (empty) HEAD body so the connection can be reused.
|
|
1529
|
-
await head.body.dump();
|
|
1530
|
-
// Only trust `content-length` on a genuine 2xx response. A non-2xx
|
|
1531
|
-
// HEAD (redirect the dispatcher didn't follow, 403/404/405 "HEAD not
|
|
1532
|
-
// allowed", 5xx, …) can still carry a stale/irrelevant
|
|
1533
|
-
// `content-length` header — enforcing size off of that would reject
|
|
1534
|
-
// (or silently pass) based on the wrong body. Treat any non-2xx HEAD
|
|
1535
|
-
// as if the header were missing and fall through to the streaming
|
|
1536
|
-
// GET guard below, which enforces maxSize independently either way.
|
|
1537
|
-
if (head.statusCode >= 200 && head.statusCode < 300) {
|
|
1538
|
-
const declaredLength = Number(head.headers["content-length"]);
|
|
1539
|
-
if (Number.isFinite(declaredLength) && declaredLength > maxSize) {
|
|
1540
|
-
throw new Error(`File too large: ${formatFileSize(declaredLength)} (max: ${formatFileSize(maxSize)})`);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
catch (error) {
|
|
1545
|
-
if (error instanceof Error && /File too large/.test(error.message)) {
|
|
1546
|
-
throw error;
|
|
1547
|
-
}
|
|
1548
|
-
logger.debug(`[FileDetector] HEAD pre-flight skipped for ${redactUrlForError(url)}: ${sanitizeErrorCause(error).message}`);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
1537
|
return withRetry(async () => {
|
|
1552
1538
|
try {
|
|
1553
1539
|
const response = await request(url, {
|
|
@@ -1561,9 +1547,21 @@ export class FileDetector {
|
|
|
1561
1547
|
// not be echoed into a thrown error.
|
|
1562
1548
|
throw new Error(`HTTP ${response.statusCode} fetching ${redactUrlForError(url)}`);
|
|
1563
1549
|
}
|
|
1550
|
+
const contentType = response.headers["content-type"] || "";
|
|
1551
|
+
const declaredLength = Number(response.headers["content-length"]);
|
|
1552
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxSize) {
|
|
1553
|
+
// `request()` resolves after response headers arrive. Close the
|
|
1554
|
+
// unread stream before throwing so a declared oversize response
|
|
1555
|
+
// never enters the body-reading loop or holds its connection.
|
|
1556
|
+
// Undici emits an abort error asynchronously when the stream is
|
|
1557
|
+
// destroyed, so consume that expected event before closing it.
|
|
1558
|
+
response.body.once("error", () => { });
|
|
1559
|
+
response.body.destroy();
|
|
1560
|
+
throw new Error(`File too large: ${formatFileSize(declaredLength)} (max: ${formatFileSize(maxSize)})`);
|
|
1561
|
+
}
|
|
1564
1562
|
// #323: cache the Content-Type from this GET so a subsequent detection
|
|
1565
1563
|
// of the same URL needs no HEAD.
|
|
1566
|
-
setCachedUrlContentType(url,
|
|
1564
|
+
setCachedUrlContentType(url, contentType, Date.now());
|
|
1567
1565
|
const chunks = [];
|
|
1568
1566
|
let totalSize = 0;
|
|
1569
1567
|
for await (const chunk of response.body) {
|
|
@@ -1573,7 +1571,7 @@ export class FileDetector {
|
|
|
1573
1571
|
}
|
|
1574
1572
|
chunks.push(chunk);
|
|
1575
1573
|
}
|
|
1576
|
-
return Buffer.concat(chunks);
|
|
1574
|
+
return { content: Buffer.concat(chunks), contentType };
|
|
1577
1575
|
}
|
|
1578
1576
|
catch (error) {
|
|
1579
1577
|
// Node/undici DNS, TLS, and connect-timeout errors embed the full
|
|
@@ -2270,7 +2268,22 @@ class MagicBytesStrategy {
|
|
|
2270
2268
|
* Detects file type from HTTP Content-Type headers
|
|
2271
2269
|
*/
|
|
2272
2270
|
class MimeTypeStrategy {
|
|
2271
|
+
responseMimeType;
|
|
2272
|
+
constructor(responseMimeType) {
|
|
2273
|
+
this.responseMimeType = responseMimeType;
|
|
2274
|
+
}
|
|
2273
2275
|
async detect(input) {
|
|
2276
|
+
if (this.responseMimeType !== undefined) {
|
|
2277
|
+
const contentType = this.responseMimeType;
|
|
2278
|
+
const type = this.mimeToFileType(contentType);
|
|
2279
|
+
return {
|
|
2280
|
+
type,
|
|
2281
|
+
mimeType: contentType.split(";")[0].trim(),
|
|
2282
|
+
extension: null,
|
|
2283
|
+
source: "url",
|
|
2284
|
+
metadata: { confidence: type !== "unknown" ? 85 : 0 },
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2274
2287
|
if (typeof input !== "string" || !this.isURL(input)) {
|
|
2275
2288
|
return this.unknown();
|
|
2276
2289
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
2
|
import { readFile as readFileAsync, stat as statAsync } from "fs/promises";
|
|
3
|
+
import pLimit from "p-limit";
|
|
3
4
|
import { request } from "undici";
|
|
4
5
|
import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
|
|
5
6
|
import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerImageAdapter.js";
|
|
@@ -570,6 +571,21 @@ const PREPROCESSED_FILES = new WeakMap();
|
|
|
570
571
|
* page cache the read usually hits — detection has just read the same bytes.
|
|
571
572
|
*/
|
|
572
573
|
const FILE_READ_TIMEOUT_MS = 30_000;
|
|
574
|
+
/**
|
|
575
|
+
* How many files may be detected and processed at once.
|
|
576
|
+
*
|
|
577
|
+
* A cap rather than unbounded `Promise.all`, and the cap is the point: this
|
|
578
|
+
* path admits files up to 100 MB, so N files in flight means N decoded buffers
|
|
579
|
+
* resident at once plus whatever each processor allocates on top. Turning a
|
|
580
|
+
* sequential loop into an unbounded fan-out trades a latency problem for a
|
|
581
|
+
* memory one, and on a large batch that is the worse of the two.
|
|
582
|
+
*
|
|
583
|
+
* Four is chosen to keep the worst case bounded rather than to saturate a
|
|
584
|
+
* disk: the win here is overlapping I/O wait, which most of it is, and the
|
|
585
|
+
* marginal gain past a handful of concurrent reads is small next to the
|
|
586
|
+
* marginal cost in resident bytes.
|
|
587
|
+
*/
|
|
588
|
+
const FILE_PROCESSING_CONCURRENCY = 4;
|
|
573
589
|
/**
|
|
574
590
|
* Read a file input's bytes, or null when they cannot be had.
|
|
575
591
|
*
|
|
@@ -835,6 +851,57 @@ function markFileProcessed(input, entry) {
|
|
|
835
851
|
processed.add(entry);
|
|
836
852
|
PREPROCESSED_FILES.set(input, processed);
|
|
837
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Detect and process one entry of the unified `files` array.
|
|
856
|
+
*
|
|
857
|
+
* Extracted so the same call can be made from the concurrent pass and from the
|
|
858
|
+
* sequential fall-through, rather than duplicated between them.
|
|
859
|
+
*/
|
|
860
|
+
async function detectFileForUnifiedArray(file, options, provider, genericFileMaxSize) {
|
|
861
|
+
const rawFileInput = isFileWithMetadata(file) ? file.buffer : file;
|
|
862
|
+
// Forward the caller's mimetype hint (Slack/Curator-style
|
|
863
|
+
// extension-less buffers) so the eager path classifies correctly
|
|
864
|
+
// for tiny files — the lazy registry path has its own hint wiring.
|
|
865
|
+
const fileMimetypeHint = isFileWithMetadata(file) ? file.mimetype : undefined;
|
|
866
|
+
// The name has to travel the same way, and for the same reason: the
|
|
867
|
+
// line above unwraps the object to its buffer, so by the time
|
|
868
|
+
// detection resolves an extension there is no name left to read one
|
|
869
|
+
// from. Without this a `.tar` supplied as bytes-plus-name is
|
|
870
|
+
// unidentifiable — its "ustar" marker sits at byte 257, not at
|
|
871
|
+
// offset 0 — and reports "Could not extract content" for an archive
|
|
872
|
+
// that extracts perfectly when handed its filename.
|
|
873
|
+
const fileFilenameHint = isFileWithMetadata(file) ? file.filename : undefined;
|
|
874
|
+
return FileDetector.detectAndProcess(rawFileInput, {
|
|
875
|
+
maxSize: genericFileMaxSize,
|
|
876
|
+
allowedTypes: [
|
|
877
|
+
"csv",
|
|
878
|
+
"image",
|
|
879
|
+
"pdf",
|
|
880
|
+
"svg",
|
|
881
|
+
"video",
|
|
882
|
+
"audio",
|
|
883
|
+
"archive",
|
|
884
|
+
"xlsx",
|
|
885
|
+
"docx",
|
|
886
|
+
"pptx",
|
|
887
|
+
"text",
|
|
888
|
+
"unknown",
|
|
889
|
+
],
|
|
890
|
+
csvOptions: options.csvOptions,
|
|
891
|
+
// #478: videos arrive through this unified `files` path, so this is
|
|
892
|
+
// where the CLI's frame/quality/format request has to be handed on.
|
|
893
|
+
videoOptions: options.videoOptions
|
|
894
|
+
? {
|
|
895
|
+
frames: options.videoOptions.frames,
|
|
896
|
+
quality: options.videoOptions.quality,
|
|
897
|
+
format: options.videoOptions.format,
|
|
898
|
+
}
|
|
899
|
+
: undefined,
|
|
900
|
+
provider: provider,
|
|
901
|
+
mimetypeHint: fileMimetypeHint,
|
|
902
|
+
filenameHint: fileFilenameHint,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
838
905
|
export async function processUnifiedFilesArray(options, maxSize, provider) {
|
|
839
906
|
options.input ??= {};
|
|
840
907
|
if (!options.input.files || options.input.files.length === 0) {
|
|
@@ -882,15 +949,45 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
|
|
|
882
949
|
inp2.text = inp2.text || "";
|
|
883
950
|
let includedCount = 0;
|
|
884
951
|
const fileRegistry = options.fileRegistry;
|
|
952
|
+
// Only detection runs concurrently, and only for files that will not
|
|
953
|
+
// take the lazy-registration branch.
|
|
954
|
+
//
|
|
955
|
+
// Everything else in this loop is order-dependent in a way that is not
|
|
956
|
+
// obvious from its shape. `appendDetectedFileResult` appends to
|
|
957
|
+
// `inp2.text`, `inp2.images` and `inp2.pdfFiles`, so running it out of
|
|
958
|
+
// order would reorder the prompt and the attachments against the files
|
|
959
|
+
// the caller supplied. `tryRegisterFileReference` mutates the shared
|
|
960
|
+
// registry and assigns reference ids from it. Both therefore stay in the
|
|
961
|
+
// sequential pass below; what moves off the critical path is
|
|
962
|
+
// `FileDetector.detectAndProcess`, which is where the read and the parse
|
|
963
|
+
// actually happen and which touches nothing shared.
|
|
964
|
+
//
|
|
965
|
+
// Lazy candidates are decided here — the predicate is synchronous — but
|
|
966
|
+
// are not detected up front, because registration usually means the
|
|
967
|
+
// bytes are never processed at all. Pre-detecting them would do exactly
|
|
968
|
+
// the work the lazy path exists to avoid. The rare fall-through, where
|
|
969
|
+
// registration is attempted and declines, detects inline below.
|
|
970
|
+
const isLazyCandidate = files.map((file) => Boolean(fileRegistry) &&
|
|
971
|
+
getFileSize(file) > SIZE_TIER_THRESHOLDS.TINY_MAX &&
|
|
972
|
+
!isEagerMultimodalFile(file));
|
|
973
|
+
const genericFileMaxSize = Math.max(maxSize, 100 * 1024 * 1024);
|
|
974
|
+
const detectLimit = pLimit(FILE_PROCESSING_CONCURRENCY);
|
|
975
|
+
// allSettled, not all: with `all` the rejection that surfaces is the one
|
|
976
|
+
// that happened FIRST IN TIME, so which file is blamed for a batch
|
|
977
|
+
// failure would depend on disk scheduling. The sequential loop reported
|
|
978
|
+
// the first failure BY INDEX, and the ordered walk below preserves that.
|
|
979
|
+
// It also means a second failure cannot become an unhandled rejection
|
|
980
|
+
// after the first has already thrown.
|
|
981
|
+
const detected = await Promise.allSettled(files.map((file, fileIdx) => isLazyCandidate[fileIdx]
|
|
982
|
+
? Promise.resolve(undefined)
|
|
983
|
+
: detectLimit(() => detectFileForUnifiedArray(file, options, provider, genericFileMaxSize))));
|
|
885
984
|
for (let fileIdx = 0; fileIdx < files.length; fileIdx++) {
|
|
886
985
|
const file = files[fileIdx];
|
|
887
986
|
const filename = extractFilename(file, fileIdx);
|
|
888
987
|
try {
|
|
889
988
|
// ─── Lazy file registration path ──────────────────────────────
|
|
890
989
|
const fileSize = fileRegistry ? getFileSize(file) : 0;
|
|
891
|
-
if (fileRegistry &&
|
|
892
|
-
fileSize > SIZE_TIER_THRESHOLDS.TINY_MAX &&
|
|
893
|
-
!isEagerMultimodalFile(file)) {
|
|
990
|
+
if (fileRegistry && isLazyCandidate[fileIdx]) {
|
|
894
991
|
const registered = await tryRegisterFileReference(file, fileSize, fileRegistry, fileIdx);
|
|
895
992
|
if (registered) {
|
|
896
993
|
logger.info(`[NEUROLINK] File lazily registered: ${filename} (${fileSize} bytes) — deferred processing`);
|
|
@@ -900,54 +997,16 @@ export async function processUnifiedFilesArray(options, maxSize, provider) {
|
|
|
900
997
|
}
|
|
901
998
|
}
|
|
902
999
|
// ─── Full processing path (current behavior) ──────────────────
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
// detection resolves an extension there is no name left to read one
|
|
914
|
-
// from. Without this a `.tar` supplied as bytes-plus-name is
|
|
915
|
-
// unidentifiable — its "ustar" marker sits at byte 257, not at
|
|
916
|
-
// offset 0 — and reports "Could not extract content" for an archive
|
|
917
|
-
// that extracts perfectly when handed its filename.
|
|
918
|
-
const fileFilenameHint = isFileWithMetadata(file)
|
|
919
|
-
? file.filename
|
|
920
|
-
: undefined;
|
|
921
|
-
const result = await FileDetector.detectAndProcess(rawFileInput, {
|
|
922
|
-
maxSize: genericFileMaxSize,
|
|
923
|
-
allowedTypes: [
|
|
924
|
-
"csv",
|
|
925
|
-
"image",
|
|
926
|
-
"pdf",
|
|
927
|
-
"svg",
|
|
928
|
-
"video",
|
|
929
|
-
"audio",
|
|
930
|
-
"archive",
|
|
931
|
-
"xlsx",
|
|
932
|
-
"docx",
|
|
933
|
-
"pptx",
|
|
934
|
-
"text",
|
|
935
|
-
"unknown",
|
|
936
|
-
],
|
|
937
|
-
csvOptions: options.csvOptions,
|
|
938
|
-
// #478: videos arrive through this unified `files` path, so this is
|
|
939
|
-
// where the CLI's frame/quality/format request has to be handed on.
|
|
940
|
-
videoOptions: options.videoOptions
|
|
941
|
-
? {
|
|
942
|
-
frames: options.videoOptions.frames,
|
|
943
|
-
quality: options.videoOptions.quality,
|
|
944
|
-
format: options.videoOptions.format,
|
|
945
|
-
}
|
|
946
|
-
: undefined,
|
|
947
|
-
provider: provider,
|
|
948
|
-
mimetypeHint: fileMimetypeHint,
|
|
949
|
-
filenameHint: fileFilenameHint,
|
|
950
|
-
});
|
|
1000
|
+
// Normally already resolved by the concurrent pass above. The inline
|
|
1001
|
+
// call is the fall-through: this file was a lazy candidate, so it was
|
|
1002
|
+
// deliberately not detected up front, and registration then declined
|
|
1003
|
+
// it. Detection has to happen somewhere, and here it is sequential.
|
|
1004
|
+
const outcome = detected[fileIdx];
|
|
1005
|
+
if (outcome.status === "rejected") {
|
|
1006
|
+
throw outcome.reason;
|
|
1007
|
+
}
|
|
1008
|
+
const result = outcome.value ??
|
|
1009
|
+
(await detectFileForUnifiedArray(file, options, provider, genericFileMaxSize));
|
|
951
1010
|
await appendDetectedFileResult(result, file, options);
|
|
952
1011
|
includedCount++;
|
|
953
1012
|
// Log what content type was added to the message
|
|
@@ -1023,31 +1082,22 @@ async function processExplicitCsvFiles(options) {
|
|
|
1023
1082
|
}
|
|
1024
1083
|
logger.info(`[CSV] Processing ${options.input.csvFiles.length} explicit CSV file(s)`);
|
|
1025
1084
|
options.input.text = options.input.text || "";
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
// Put the actual CSV content BEFORE the tool instructions —
|
|
1043
|
-
// buildCSVToolInstructions references "the CSV data shown above"
|
|
1044
|
-
// and the trailing position keeps that reference accurate.
|
|
1045
|
-
csvSection += result.content;
|
|
1046
|
-
csvSection += buildCSVToolInstructions(filePath);
|
|
1047
|
-
options.input.text += csvSection;
|
|
1048
|
-
logger.info(`[CSV] ✅ Processed: ${filename}`);
|
|
1049
|
-
}
|
|
1050
|
-
catch (error) {
|
|
1085
|
+
const csvFiles = options.input.csvFiles;
|
|
1086
|
+
// Detection is the expensive half — a read plus a parse per file, and
|
|
1087
|
+
// previously each one waited for the last. It is also the only half that is
|
|
1088
|
+
// independent per file, so it is the only half that runs concurrently: the
|
|
1089
|
+
// sections below are appended to a single `text` string, so they stay in a
|
|
1090
|
+
// sequential index-ordered pass and the prompt reads identically.
|
|
1091
|
+
const limit = pLimit(FILE_PROCESSING_CONCURRENCY);
|
|
1092
|
+
const settled = await Promise.allSettled(csvFiles.map((csvFile) => limit(() => FileDetector.detectAndProcess(csvFile, {
|
|
1093
|
+
allowedTypes: ["csv"],
|
|
1094
|
+
csvOptions: options.csvOptions,
|
|
1095
|
+
}))));
|
|
1096
|
+
for (let i = 0; i < csvFiles.length; i++) {
|
|
1097
|
+
const csvFile = csvFiles[i];
|
|
1098
|
+
const outcome = settled[i];
|
|
1099
|
+
if (outcome.status === "rejected") {
|
|
1100
|
+
const error = outcome.reason;
|
|
1051
1101
|
const filename = extractFilename(csvFile, i);
|
|
1052
1102
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1053
1103
|
// #273: fail loud instead of embedding the error into the prompt text
|
|
@@ -1055,6 +1105,23 @@ async function processExplicitCsvFiles(options) {
|
|
|
1055
1105
|
logger.error(`[CSV] ❌ Failed to process ${filename}: ${errMsg}`);
|
|
1056
1106
|
throw ErrorFactory.csvProcessingFailed(filename, error instanceof Error ? error : new Error(errMsg));
|
|
1057
1107
|
}
|
|
1108
|
+
const result = outcome.value;
|
|
1109
|
+
const filename = extractFilename(csvFile, i);
|
|
1110
|
+
const filePath = typeof csvFile === "string" ? csvFile : filename;
|
|
1111
|
+
let csvSection = `\n\n## CSV Data from "${filename}":\n`;
|
|
1112
|
+
if (result.metadata) {
|
|
1113
|
+
const metadataText = formatCSVMetadata(result.metadata);
|
|
1114
|
+
if (metadataText) {
|
|
1115
|
+
csvSection += metadataText + `\n\n`;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Put the actual CSV content BEFORE the tool instructions —
|
|
1119
|
+
// buildCSVToolInstructions references "the CSV data shown above"
|
|
1120
|
+
// and the trailing position keeps that reference accurate.
|
|
1121
|
+
csvSection += result.content;
|
|
1122
|
+
csvSection += buildCSVToolInstructions(filePath);
|
|
1123
|
+
options.input.text += csvSection;
|
|
1124
|
+
logger.info(`[CSV] ✅ Processed: ${filename}`);
|
|
1058
1125
|
}
|
|
1059
1126
|
}
|
|
1060
1127
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.15.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"test:media": "pnpm exec tsx test/continuous-test-suite-media-gen.ts",
|
|
93
93
|
"test:media-registry-collisions": "pnpm exec tsx test/continuous-test-suite-media-registry-collisions.ts",
|
|
94
94
|
"test:memory": "pnpm exec tsx test/continuous-test-suite-memory.ts",
|
|
95
|
+
"test:multimodal-rag": "pnpm exec tsx test/continuous-test-suite-multimodal-rag.ts",
|
|
95
96
|
"test:proxy-sharing": "pnpm run build && pnpm exec tsx test/continuous-test-suite-proxy-sharing.ts",
|
|
96
97
|
"test:openai-compat-streaming-retry": "pnpm exec tsx test/continuous-test-suite-openai-compat-streaming-retry.ts",
|
|
97
98
|
"test:anthropic-streaming-retry": "pnpm exec tsx test/continuous-test-suite-anthropic-streaming-retry.ts",
|