@juspay/neurolink 9.94.1 → 9.94.3
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 +12 -0
- package/dist/agent/directTools.js +3 -1
- package/dist/browser/neurolink.min.js +404 -403
- package/dist/cli/factories/commandFactory.d.ts +6 -0
- package/dist/cli/factories/commandFactory.js +36 -8
- package/dist/lib/agent/directTools.js +3 -1
- package/dist/lib/neurolink.js +5 -0
- package/dist/lib/rag/document/loaders.d.ts +12 -1
- package/dist/lib/rag/document/loaders.js +64 -34
- package/dist/lib/types/file.d.ts +43 -0
- package/dist/lib/types/generate.d.ts +3 -11
- package/dist/lib/types/rag.d.ts +7 -0
- package/dist/lib/types/stream.d.ts +2 -6
- package/dist/lib/utils/csvProcessor.d.ts +121 -5
- package/dist/lib/utils/csvProcessor.js +563 -114
- package/dist/lib/utils/csvUtils.d.ts +26 -0
- package/dist/lib/utils/csvUtils.js +85 -0
- package/dist/lib/utils/errorHandling.d.ts +23 -0
- package/dist/lib/utils/errorHandling.js +65 -0
- package/dist/lib/utils/multimodalOptionsBuilder.d.ts +1 -5
- package/dist/lib/utils/textEncoding.d.ts +25 -0
- package/dist/lib/utils/textEncoding.js +141 -0
- package/dist/neurolink.js +5 -0
- package/dist/rag/document/loaders.d.ts +12 -1
- package/dist/rag/document/loaders.js +64 -34
- package/dist/types/file.d.ts +43 -0
- package/dist/types/generate.d.ts +3 -11
- package/dist/types/rag.d.ts +7 -0
- package/dist/types/stream.d.ts +2 -6
- package/dist/utils/csvProcessor.d.ts +121 -5
- package/dist/utils/csvProcessor.js +562 -113
- package/dist/utils/csvUtils.d.ts +26 -0
- package/dist/utils/csvUtils.js +84 -0
- package/dist/utils/errorHandling.d.ts +23 -0
- package/dist/utils/errorHandling.js +65 -0
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -5
- package/dist/utils/textEncoding.d.ts +25 -0
- package/dist/utils/textEncoding.js +140 -0
- package/package.json +3 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, RFC-4180-aware delimiter-separated-value helpers.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates quote-aware field counting/splitting that was previously
|
|
5
|
+
* duplicated across csvProcessor.ts and fileDetector.ts (#359), and adds
|
|
6
|
+
* delimiter detection so TSV / semicolon / pipe files parse correctly (#361).
|
|
7
|
+
* Keeping these in one module prevents the same quoting edge case from having
|
|
8
|
+
* to be fixed in three places and drifting out of sync.
|
|
9
|
+
*/
|
|
10
|
+
/** Delimiter candidates, in tie-break preference order (comma first). */
|
|
11
|
+
export declare const CANDIDATE_DELIMITERS: readonly [",", "\t", ";", "|"];
|
|
12
|
+
/**
|
|
13
|
+
* Split a single line into fields, respecting RFC-4180 quoting: a delimiter
|
|
14
|
+
* inside a `"…"` quoted field does not split, and `""` is an escaped quote.
|
|
15
|
+
*/
|
|
16
|
+
export declare function splitCSVFields(line: string, delimiter?: string): string[];
|
|
17
|
+
/** Count fields in a single line, respecting RFC-4180 quoting. */
|
|
18
|
+
export declare function countCSVColumns(line: string, delimiter?: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* Detect the delimiter of DSV content by testing comma/tab/semicolon/pipe
|
|
21
|
+
* against up to 5 sampled non-empty lines and keeping the candidate with the
|
|
22
|
+
* most consistent (>1) column count. Comma wins ties and the "nothing splits"
|
|
23
|
+
* case, so plain CSVs are never reclassified. An optional extension hint
|
|
24
|
+
* (`tsv` → tab, `psv` → pipe) breaks ambiguity in the file's favor.
|
|
25
|
+
*/
|
|
26
|
+
export declare function detectDelimiter(lines: string[], extensionHint?: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, RFC-4180-aware delimiter-separated-value helpers.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates quote-aware field counting/splitting that was previously
|
|
5
|
+
* duplicated across csvProcessor.ts and fileDetector.ts (#359), and adds
|
|
6
|
+
* delimiter detection so TSV / semicolon / pipe files parse correctly (#361).
|
|
7
|
+
* Keeping these in one module prevents the same quoting edge case from having
|
|
8
|
+
* to be fixed in three places and drifting out of sync.
|
|
9
|
+
*/
|
|
10
|
+
/** Delimiter candidates, in tie-break preference order (comma first). */
|
|
11
|
+
export const CANDIDATE_DELIMITERS = [",", "\t", ";", "|"];
|
|
12
|
+
/**
|
|
13
|
+
* Split a single line into fields, respecting RFC-4180 quoting: a delimiter
|
|
14
|
+
* inside a `"…"` quoted field does not split, and `""` is an escaped quote.
|
|
15
|
+
*/
|
|
16
|
+
export function splitCSVFields(line, delimiter = ",") {
|
|
17
|
+
const fields = [];
|
|
18
|
+
let field = "";
|
|
19
|
+
let inQuotes = false;
|
|
20
|
+
for (let i = 0; i < line.length; i++) {
|
|
21
|
+
const ch = line[i];
|
|
22
|
+
if (ch === '"') {
|
|
23
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
24
|
+
field += '"';
|
|
25
|
+
i++; // consume the escaped quote
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
inQuotes = !inQuotes;
|
|
29
|
+
}
|
|
30
|
+
else if (ch === delimiter && !inQuotes) {
|
|
31
|
+
fields.push(field);
|
|
32
|
+
field = "";
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
field += ch;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
fields.push(field);
|
|
39
|
+
return fields;
|
|
40
|
+
}
|
|
41
|
+
/** Count fields in a single line, respecting RFC-4180 quoting. */
|
|
42
|
+
export function countCSVColumns(line, delimiter = ",") {
|
|
43
|
+
return splitCSVFields(line, delimiter).length;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Detect the delimiter of DSV content by testing comma/tab/semicolon/pipe
|
|
47
|
+
* against up to 5 sampled non-empty lines and keeping the candidate with the
|
|
48
|
+
* most consistent (>1) column count. Comma wins ties and the "nothing splits"
|
|
49
|
+
* case, so plain CSVs are never reclassified. An optional extension hint
|
|
50
|
+
* (`tsv` → tab, `psv` → pipe) breaks ambiguity in the file's favor.
|
|
51
|
+
*/
|
|
52
|
+
export function detectDelimiter(lines, extensionHint) {
|
|
53
|
+
const sample = lines.filter((l) => l.trim() !== "").slice(0, 5);
|
|
54
|
+
if (sample.length === 0) {
|
|
55
|
+
return ",";
|
|
56
|
+
}
|
|
57
|
+
const hint = extensionHint?.toLowerCase().replace(/^\./, "");
|
|
58
|
+
const hinted = hint === "tsv" ? "\t" : hint === "psv" ? "|" : undefined;
|
|
59
|
+
let best = ",";
|
|
60
|
+
let bestScore = -1;
|
|
61
|
+
for (const delim of CANDIDATE_DELIMITERS) {
|
|
62
|
+
const counts = sample.map((l) => countCSVColumns(l, delim));
|
|
63
|
+
const headerCols = counts[0];
|
|
64
|
+
if (headerCols <= 1) {
|
|
65
|
+
continue; // this delimiter doesn't split the header — not the delimiter
|
|
66
|
+
}
|
|
67
|
+
const consistent = counts.every((c) => c === headerCols);
|
|
68
|
+
// Prefer: a matching extension hint, then consistency, then more columns,
|
|
69
|
+
// with a small comma bias so comma stays the default on ties.
|
|
70
|
+
const score = (delim === hinted ? 1000 : 0) +
|
|
71
|
+
(consistent ? 100 : 0) +
|
|
72
|
+
headerCols +
|
|
73
|
+
(delim === "," ? 0.5 : 0);
|
|
74
|
+
if (score > bestScore) {
|
|
75
|
+
bestScore = score;
|
|
76
|
+
best = delim;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Nothing split multi-column, but the extension says otherwise — honor it.
|
|
80
|
+
if (bestScore < 0 && hinted) {
|
|
81
|
+
return hinted;
|
|
82
|
+
}
|
|
83
|
+
return best;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=csvUtils.js.map
|
|
@@ -51,6 +51,10 @@ export declare const ERROR_CODES: {
|
|
|
51
51
|
readonly INVALID_PPT_LOGO_PATH: "INVALID_PPT_LOGO_PATH";
|
|
52
52
|
readonly INVALID_PPT_MODE: "INVALID_PPT_MODE";
|
|
53
53
|
readonly INVALID_PPT_PROMPT: "INVALID_PPT_PROMPT";
|
|
54
|
+
readonly CSV_INVALID_INPUT: "CSV_INVALID_INPUT";
|
|
55
|
+
readonly CSV_ROW_INVALID: "CSV_ROW_INVALID";
|
|
56
|
+
readonly CSV_FILE_ACCESS_FAILED: "CSV_FILE_ACCESS_FAILED";
|
|
57
|
+
readonly CSV_PARSE_FAILED: "CSV_PARSE_FAILED";
|
|
54
58
|
};
|
|
55
59
|
/**
|
|
56
60
|
* Enhanced error class with structured information
|
|
@@ -128,6 +132,25 @@ export declare class ErrorFactory {
|
|
|
128
132
|
* Create an invalid configuration error (e.g., NaN for numeric values)
|
|
129
133
|
*/
|
|
130
134
|
static invalidConfiguration(configName: string, reason: string, context?: Record<string, unknown>): NeuroLinkError;
|
|
135
|
+
/**
|
|
136
|
+
* Create an invalid CSV input error (e.g. an empty filePath/csvString argument).
|
|
137
|
+
*/
|
|
138
|
+
static csvInvalidInput(message: string): NeuroLinkError;
|
|
139
|
+
/**
|
|
140
|
+
* Create a CSV row-shape violation error (#384) — a parsed row that isn't a
|
|
141
|
+
* string-keyed object with string values.
|
|
142
|
+
*/
|
|
143
|
+
static csvRowInvalid(message: string, rowNumber: number): NeuroLinkError;
|
|
144
|
+
/**
|
|
145
|
+
* Create a CSV file access error (bad path, permissions, ENOENT/EACCES) (#375).
|
|
146
|
+
*/
|
|
147
|
+
static csvFileAccessFailed(message: string, filePath: string, originalError?: Error): NeuroLinkError;
|
|
148
|
+
/**
|
|
149
|
+
* Create a CSV read/parse failure error (#375) — source stream errors and
|
|
150
|
+
* csv-parser errors both route through this, carrying the enriched
|
|
151
|
+
* `buildCsvParseErrorMessage` context in `message`.
|
|
152
|
+
*/
|
|
153
|
+
static csvParseFailed(message: string, context: Record<string, unknown>, originalError?: Error): NeuroLinkError;
|
|
131
154
|
/**
|
|
132
155
|
* Create an invalid video resolution error
|
|
133
156
|
*/
|
|
@@ -66,6 +66,11 @@ export const ERROR_CODES = {
|
|
|
66
66
|
INVALID_PPT_LOGO_PATH: "INVALID_PPT_LOGO_PATH",
|
|
67
67
|
INVALID_PPT_MODE: "INVALID_PPT_MODE",
|
|
68
68
|
INVALID_PPT_PROMPT: "INVALID_PPT_PROMPT",
|
|
69
|
+
// CSV validation/parsing errors (#1199)
|
|
70
|
+
CSV_INVALID_INPUT: "CSV_INVALID_INPUT",
|
|
71
|
+
CSV_ROW_INVALID: "CSV_ROW_INVALID",
|
|
72
|
+
CSV_FILE_ACCESS_FAILED: "CSV_FILE_ACCESS_FAILED",
|
|
73
|
+
CSV_PARSE_FAILED: "CSV_PARSE_FAILED",
|
|
69
74
|
};
|
|
70
75
|
/**
|
|
71
76
|
* Enhanced error class with structured information
|
|
@@ -259,6 +264,66 @@ export class ErrorFactory {
|
|
|
259
264
|
});
|
|
260
265
|
}
|
|
261
266
|
// ============================================================================
|
|
267
|
+
// CSV VALIDATION/PARSING ERRORS (#1199)
|
|
268
|
+
// ============================================================================
|
|
269
|
+
/**
|
|
270
|
+
* Create an invalid CSV input error (e.g. an empty filePath/csvString argument).
|
|
271
|
+
*/
|
|
272
|
+
static csvInvalidInput(message) {
|
|
273
|
+
return new NeuroLinkError({
|
|
274
|
+
code: ERROR_CODES.CSV_INVALID_INPUT,
|
|
275
|
+
message,
|
|
276
|
+
category: ErrorCategory.VALIDATION,
|
|
277
|
+
severity: ErrorSeverity.MEDIUM,
|
|
278
|
+
retriable: false,
|
|
279
|
+
context: {},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Create a CSV row-shape violation error (#384) — a parsed row that isn't a
|
|
284
|
+
* string-keyed object with string values.
|
|
285
|
+
*/
|
|
286
|
+
static csvRowInvalid(message, rowNumber) {
|
|
287
|
+
return new NeuroLinkError({
|
|
288
|
+
code: ERROR_CODES.CSV_ROW_INVALID,
|
|
289
|
+
message,
|
|
290
|
+
category: ErrorCategory.VALIDATION,
|
|
291
|
+
severity: ErrorSeverity.MEDIUM,
|
|
292
|
+
retriable: false,
|
|
293
|
+
context: { rowNumber },
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Create a CSV file access error (bad path, permissions, ENOENT/EACCES) (#375).
|
|
298
|
+
*/
|
|
299
|
+
static csvFileAccessFailed(message, filePath, originalError) {
|
|
300
|
+
return new NeuroLinkError({
|
|
301
|
+
code: ERROR_CODES.CSV_FILE_ACCESS_FAILED,
|
|
302
|
+
message,
|
|
303
|
+
category: ErrorCategory.RESOURCE,
|
|
304
|
+
severity: ErrorSeverity.HIGH,
|
|
305
|
+
retriable: false,
|
|
306
|
+
context: { filePath },
|
|
307
|
+
originalError,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Create a CSV read/parse failure error (#375) — source stream errors and
|
|
312
|
+
* csv-parser errors both route through this, carrying the enriched
|
|
313
|
+
* `buildCsvParseErrorMessage` context in `message`.
|
|
314
|
+
*/
|
|
315
|
+
static csvParseFailed(message, context, originalError) {
|
|
316
|
+
return new NeuroLinkError({
|
|
317
|
+
code: ERROR_CODES.CSV_PARSE_FAILED,
|
|
318
|
+
message,
|
|
319
|
+
category: ErrorCategory.EXECUTION,
|
|
320
|
+
severity: ErrorSeverity.MEDIUM,
|
|
321
|
+
retriable: false,
|
|
322
|
+
context,
|
|
323
|
+
originalError,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// ============================================================================
|
|
262
327
|
// VIDEO VALIDATION ERRORS
|
|
263
328
|
// ============================================================================
|
|
264
329
|
/**
|
|
@@ -50,11 +50,7 @@ export declare function buildMultimodalOptions(options: StreamOptions, providerN
|
|
|
50
50
|
csvFiles: (string | Buffer<ArrayBufferLike>)[] | undefined;
|
|
51
51
|
pdfFiles: (string | Buffer<ArrayBufferLike>)[] | undefined;
|
|
52
52
|
};
|
|
53
|
-
csvOptions:
|
|
54
|
-
maxRows?: number;
|
|
55
|
-
formatStyle?: "raw" | "markdown" | "json";
|
|
56
|
-
includeHeaders?: boolean;
|
|
57
|
-
} | undefined;
|
|
53
|
+
csvOptions: import("../types/file.js").CSVProcessorOptions | undefined;
|
|
58
54
|
systemPrompt: string | undefined;
|
|
59
55
|
conversationHistory: import("../types/conversation.js").ChatMessage[] | undefined;
|
|
60
56
|
provider: string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffer → string decoding with encoding detection (#362).
|
|
3
|
+
*
|
|
4
|
+
* Order of resolution:
|
|
5
|
+
* 1. Explicit override (any iconv-lite label).
|
|
6
|
+
* 2. Deterministic BOM sniff (UTF-8 / UTF-16 LE / UTF-16 BE).
|
|
7
|
+
* 3. `chardet` statistical detection, mapped to an iconv-lite label.
|
|
8
|
+
* 4. Fallback to UTF-8 (preserves the historical hard-coded default).
|
|
9
|
+
*
|
|
10
|
+
* Decoding always goes through `iconv-lite`, and any residual BOM is stripped
|
|
11
|
+
* from the decoded string so it can't glue onto the first token.
|
|
12
|
+
*/
|
|
13
|
+
import type { DecodedBuffer } from "../types/index.js";
|
|
14
|
+
/** Strip a leading BOM (U+FEFF) from an already-decoded string. */
|
|
15
|
+
export declare function stripBomString(text: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Decode a buffer to text, detecting the encoding unless overridden.
|
|
18
|
+
*
|
|
19
|
+
* @param isCompleteBuffer - Whether `buffer` is the entire source (default
|
|
20
|
+
* `true`). Pass `false` when `buffer` is only a peeked prefix (e.g. a
|
|
21
|
+
* streaming head-sniff) — the ASCII fast path still commits to "utf-8" for
|
|
22
|
+
* the peeked bytes (unchanged behavior), but reports a lower confidence
|
|
23
|
+
* since later bytes not yet seen could still be non-ASCII (#1199).
|
|
24
|
+
*/
|
|
25
|
+
export declare function decodeBuffer(buffer: Buffer, override?: string, isCompleteBuffer?: boolean): DecodedBuffer;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffer → string decoding with encoding detection (#362).
|
|
3
|
+
*
|
|
4
|
+
* Order of resolution:
|
|
5
|
+
* 1. Explicit override (any iconv-lite label).
|
|
6
|
+
* 2. Deterministic BOM sniff (UTF-8 / UTF-16 LE / UTF-16 BE).
|
|
7
|
+
* 3. `chardet` statistical detection, mapped to an iconv-lite label.
|
|
8
|
+
* 4. Fallback to UTF-8 (preserves the historical hard-coded default).
|
|
9
|
+
*
|
|
10
|
+
* Decoding always goes through `iconv-lite`, and any residual BOM is stripped
|
|
11
|
+
* from the decoded string so it can't glue onto the first token.
|
|
12
|
+
*/
|
|
13
|
+
import chardet from "chardet";
|
|
14
|
+
import iconv from "iconv-lite";
|
|
15
|
+
import { logger } from "./logger.js";
|
|
16
|
+
/** Strip a leading BOM (U+FEFF) from an already-decoded string. */
|
|
17
|
+
export function stripBomString(text) {
|
|
18
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
19
|
+
}
|
|
20
|
+
/** Deterministic BOM detection from the raw bytes. Returns null when absent. */
|
|
21
|
+
function sniffBom(buffer) {
|
|
22
|
+
if (buffer.length >= 3 &&
|
|
23
|
+
buffer[0] === 0xef &&
|
|
24
|
+
buffer[1] === 0xbb &&
|
|
25
|
+
buffer[2] === 0xbf) {
|
|
26
|
+
return "utf-8";
|
|
27
|
+
}
|
|
28
|
+
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
29
|
+
return "utf-16le";
|
|
30
|
+
}
|
|
31
|
+
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
32
|
+
return "utf-16be";
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Map a chardet label to an iconv-lite-supported label. Returns null when the
|
|
38
|
+
* label is unknown/unsupported so the caller falls back to UTF-8.
|
|
39
|
+
*/
|
|
40
|
+
function toIconvLabel(detected) {
|
|
41
|
+
if (!detected) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const normalized = detected.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
45
|
+
const map = {
|
|
46
|
+
utf8: "utf-8",
|
|
47
|
+
utf16le: "utf-16le",
|
|
48
|
+
utf16be: "utf-16be",
|
|
49
|
+
ascii: "utf-8", // ASCII is a strict UTF-8 subset
|
|
50
|
+
windows1252: "windows-1252",
|
|
51
|
+
iso88591: "latin1",
|
|
52
|
+
latin1: "latin1",
|
|
53
|
+
iso88592: "iso-8859-2",
|
|
54
|
+
windows1251: "windows-1251",
|
|
55
|
+
koi8r: "koi8-r",
|
|
56
|
+
gb18030: "gb18030",
|
|
57
|
+
big5: "big5",
|
|
58
|
+
shiftjis: "shift_jis",
|
|
59
|
+
euckr: "euc-kr",
|
|
60
|
+
eucjp: "euc-jp",
|
|
61
|
+
};
|
|
62
|
+
const label = map[normalized] ?? detected;
|
|
63
|
+
return iconv.encodingExists(label) ? label : null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Decode a buffer to text, detecting the encoding unless overridden.
|
|
67
|
+
*
|
|
68
|
+
* @param isCompleteBuffer - Whether `buffer` is the entire source (default
|
|
69
|
+
* `true`). Pass `false` when `buffer` is only a peeked prefix (e.g. a
|
|
70
|
+
* streaming head-sniff) — the ASCII fast path still commits to "utf-8" for
|
|
71
|
+
* the peeked bytes (unchanged behavior), but reports a lower confidence
|
|
72
|
+
* since later bytes not yet seen could still be non-ASCII (#1199).
|
|
73
|
+
*/
|
|
74
|
+
export function decodeBuffer(buffer, override, isCompleteBuffer = true) {
|
|
75
|
+
// 1. Explicit override wins outright.
|
|
76
|
+
if (override && iconv.encodingExists(override)) {
|
|
77
|
+
return {
|
|
78
|
+
text: stripBomString(iconv.decode(buffer, override)),
|
|
79
|
+
encoding: override,
|
|
80
|
+
confidence: 100,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (override && !iconv.encodingExists(override)) {
|
|
84
|
+
logger.warn(`[textEncoding] Unsupported encoding override "${override}"; falling back to detection.`);
|
|
85
|
+
}
|
|
86
|
+
// 2. BOM is authoritative when present.
|
|
87
|
+
const bom = sniffBom(buffer);
|
|
88
|
+
if (bom) {
|
|
89
|
+
return {
|
|
90
|
+
text: stripBomString(iconv.decode(buffer, bom)),
|
|
91
|
+
encoding: bom,
|
|
92
|
+
confidence: 100,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// 2b. Pure-ASCII fast path — chardet often reports ASCII as ISO-8859-1, but
|
|
96
|
+
// ASCII is a strict UTF-8 subset and decodes identically, so report "utf-8"
|
|
97
|
+
// to preserve the historical default for plain-ASCII files.
|
|
98
|
+
let ascii = true;
|
|
99
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
100
|
+
if (buffer[i] >= 0x80) {
|
|
101
|
+
ascii = false;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (ascii) {
|
|
106
|
+
return {
|
|
107
|
+
text: stripBomString(iconv.decode(buffer, "utf-8")),
|
|
108
|
+
encoding: "utf-8",
|
|
109
|
+
// A partial peek being pure-ASCII only proves the peeked prefix is
|
|
110
|
+
// ASCII, not the whole file — a later chunk could still switch to a
|
|
111
|
+
// legacy encoding. Only report full certainty when `buffer` is known
|
|
112
|
+
// to be the complete source.
|
|
113
|
+
confidence: isCompleteBuffer ? 100 : 60,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// 3. Statistical detection.
|
|
117
|
+
let detected = null;
|
|
118
|
+
try {
|
|
119
|
+
detected = chardet.detect(buffer);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger.debug(`[textEncoding] chardet failed: ${String(error)}`);
|
|
123
|
+
}
|
|
124
|
+
const label = toIconvLabel(detected);
|
|
125
|
+
if (label) {
|
|
126
|
+
return {
|
|
127
|
+
text: stripBomString(iconv.decode(buffer, label)),
|
|
128
|
+
encoding: label,
|
|
129
|
+
// chardet gives a winner but not a numeric score via detect(); treat a
|
|
130
|
+
// confident non-UTF-8 hit as reasonably (not fully) certain.
|
|
131
|
+
confidence: label === "utf-8" ? 100 : 80,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// 4. Fallback: historical UTF-8 default.
|
|
135
|
+
return {
|
|
136
|
+
text: stripBomString(iconv.decode(buffer, "utf-8")),
|
|
137
|
+
encoding: "utf-8",
|
|
138
|
+
confidence: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=textEncoding.js.map
|
package/dist/neurolink.js
CHANGED
|
@@ -3820,6 +3820,11 @@ Current user's request: ${currentInput}`;
|
|
|
3820
3820
|
evaluationDomain: options.evaluationDomain,
|
|
3821
3821
|
toolUsageContext: options.toolUsageContext,
|
|
3822
3822
|
input: options.input,
|
|
3823
|
+
// CSV processing options must survive the reconstruction into
|
|
3824
|
+
// TextGenerationOptions — the message builder reads them (encoding,
|
|
3825
|
+
// formatting, sanitization, parse timeout). Omitting them silently
|
|
3826
|
+
// dropped every csvOptions field (incl. the CLI --csv-* flags).
|
|
3827
|
+
csvOptions: options.csvOptions,
|
|
3823
3828
|
region: options.region,
|
|
3824
3829
|
tts: options.tts,
|
|
3825
3830
|
stt: options.stt,
|
|
@@ -61,9 +61,20 @@ export declare class JSONLoader extends TextLoader {
|
|
|
61
61
|
export declare class CSVLoader extends TextLoader {
|
|
62
62
|
load(source: string, options?: CSVLoaderOptions): Promise<MDocument>;
|
|
63
63
|
canHandle(source: string): boolean;
|
|
64
|
-
private parseCSVLine;
|
|
65
64
|
private toMarkdownTable;
|
|
66
65
|
private toTextTable;
|
|
66
|
+
/**
|
|
67
|
+
* Collapse embedded newlines to a single space so one logical CSV row
|
|
68
|
+
* renders as exactly one line in the text/markdown table output.
|
|
69
|
+
*
|
|
70
|
+
* The shared quote-aware parser (`splitCsvLines` + `splitCSVFields`)
|
|
71
|
+
* preserves newlines embedded inside a quoted field verbatim — correct
|
|
72
|
+
* for the `json` output format (where `JSON.stringify` escapes them), but
|
|
73
|
+
* left as-is here it would split a single record across multiple
|
|
74
|
+
* physical table rows. Mirrors the same normalization `CSVProcessor`
|
|
75
|
+
* already applies in its own markdown formatter.
|
|
76
|
+
*/
|
|
77
|
+
private collapseEmbeddedNewlines;
|
|
67
78
|
}
|
|
68
79
|
/**
|
|
69
80
|
* PDF file loader
|
|
@@ -27,6 +27,8 @@ import { existsSync } from "fs";
|
|
|
27
27
|
import { readFile } from "fs/promises";
|
|
28
28
|
import { basename, extname } from "path";
|
|
29
29
|
import { logger } from "../../utils/logger.js";
|
|
30
|
+
import { CSVProcessor, stripBom, stripMetadataLine, splitCsvLines, dedupeColumnNames, sanitizeColumnName, } from "../../utils/csvProcessor.js";
|
|
31
|
+
import { splitCSVFields } from "../../utils/csvUtils.js";
|
|
30
32
|
import { MDocument } from "./MDocument.js";
|
|
31
33
|
/**
|
|
32
34
|
* Text file loader
|
|
@@ -115,17 +117,48 @@ export class JSONLoader extends TextLoader {
|
|
|
115
117
|
export class CSVLoader extends TextLoader {
|
|
116
118
|
async load(source, options) {
|
|
117
119
|
const content = await this.loadContent(source, options?.encoding);
|
|
118
|
-
const { delimiter
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
const { delimiter, hasHeader = true, columns, outputFormat = "text", sanitizeColumnNames = false, columnNameCase = "snake_case", } = options || {};
|
|
121
|
+
// #361: an explicit delimiter always wins; otherwise detect it from the
|
|
122
|
+
// content + extension so a .tsv fed through RAG isn't collapsed into one
|
|
123
|
+
// comma-column (CSVLoader.canHandle already accepts .tsv).
|
|
124
|
+
const effectiveDelimiter = delimiter ?? CSVProcessor.detectDelimiter(content, extname(source));
|
|
125
|
+
// Route header/row parsing through the same shared, quote-aware path
|
|
126
|
+
// detectDelimiter() above already uses internally: a quote-aware logical
|
|
127
|
+
// row split (splitCsvLines — a newline inside a quoted field stays part
|
|
128
|
+
// of that field instead of breaking the row) with a leading Excel `sep=`
|
|
129
|
+
// preamble line stripped (stripMetadataLine), so it can't be parsed as
|
|
130
|
+
// the header. Field-level splitting also goes through the shared
|
|
131
|
+
// RFC-4180-aware splitCSVFields() instead of the loader's own naive
|
|
132
|
+
// backslash-escape parser, which didn't handle doubled-quote (`""`)
|
|
133
|
+
// escaping or quoted delimiters in the headerless-columns branch.
|
|
134
|
+
const { dataLines: strippedLines } = stripMetadataLine(splitCsvLines(stripBom(content)));
|
|
135
|
+
const lines = strippedLines.filter((line) => line.trim());
|
|
136
|
+
const rawHeaders = hasHeader
|
|
137
|
+
? splitCSVFields(lines[0] ?? "", effectiveDelimiter).map((h) => h.trim())
|
|
138
|
+
: columns ||
|
|
139
|
+
splitCSVFields(lines[0] ?? "", effectiveDelimiter).map((_, i) => `col${i + 1}`);
|
|
140
|
+
// #378: apply the same opt-in header sanitization CSVProcessor offers, so
|
|
141
|
+
// the RAG ingestion path doesn't corrupt "Price ($)"-style headers as keys.
|
|
142
|
+
const headers = sanitizeColumnNames
|
|
143
|
+
? dedupeColumnNames(rawHeaders.map((h, i) => sanitizeColumnName(h, columnNameCase, i)))
|
|
144
|
+
: rawHeaders;
|
|
123
145
|
const dataLines = hasHeader ? lines.slice(1) : lines;
|
|
124
|
-
const rows = dataLines.map((line) =>
|
|
146
|
+
const rows = dataLines.map((line) => splitCSVFields(line, effectiveDelimiter).map((field) => field.trim()));
|
|
125
147
|
let formattedContent;
|
|
126
148
|
switch (outputFormat) {
|
|
127
149
|
case "json":
|
|
128
|
-
formattedContent = JSON.stringify(rows.map((row) =>
|
|
150
|
+
formattedContent = JSON.stringify(rows.map((row) => {
|
|
151
|
+
// Defense-in-depth against a caller-controlled header literally
|
|
152
|
+
// named "__proto__"/"constructor"/"prototype" (#1199);
|
|
153
|
+
// Object.fromEntries can't actually pollute Object.prototype here
|
|
154
|
+
// (spec semantics), but a null-prototype target avoids relying on
|
|
155
|
+
// that for future readers.
|
|
156
|
+
const record = Object.create(null);
|
|
157
|
+
headers.forEach((h, i) => {
|
|
158
|
+
record[h] = row[i];
|
|
159
|
+
});
|
|
160
|
+
return record;
|
|
161
|
+
}), null, 2);
|
|
129
162
|
break;
|
|
130
163
|
case "markdown":
|
|
131
164
|
formattedContent = this.toMarkdownTable(headers, rows);
|
|
@@ -145,42 +178,39 @@ export class CSVLoader extends TextLoader {
|
|
|
145
178
|
const ext = extname(source).toLowerCase();
|
|
146
179
|
return ext === ".csv" || ext === ".tsv";
|
|
147
180
|
}
|
|
148
|
-
parseCSVLine(line, delimiter) {
|
|
149
|
-
const result = [];
|
|
150
|
-
let current = "";
|
|
151
|
-
let inQuotes = false;
|
|
152
|
-
for (let i = 0; i < line.length; i++) {
|
|
153
|
-
const char = line[i];
|
|
154
|
-
if (char === '"' && (i === 0 || line[i - 1] !== "\\")) {
|
|
155
|
-
inQuotes = !inQuotes;
|
|
156
|
-
}
|
|
157
|
-
else if (char === delimiter && !inQuotes) {
|
|
158
|
-
result.push(current.trim());
|
|
159
|
-
current = "";
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
current += char;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
result.push(current.trim());
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
181
|
toMarkdownTable(headers, rows) {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
182
|
+
const safeHeaders = headers.map((h) => this.collapseEmbeddedNewlines(h));
|
|
183
|
+
const headerRow = `| ${safeHeaders.join(" | ")} |`;
|
|
184
|
+
const separator = `| ${safeHeaders.map(() => "---").join(" | ")} |`;
|
|
185
|
+
const dataRows = rows.map((row) => `| ${row.map((cell) => this.collapseEmbeddedNewlines(cell)).join(" | ")} |`);
|
|
172
186
|
return [headerRow, separator, ...dataRows].join("\n");
|
|
173
187
|
}
|
|
174
188
|
toTextTable(headers, rows) {
|
|
175
|
-
const
|
|
176
|
-
const
|
|
189
|
+
const safeHeaders = headers.map((h) => this.collapseEmbeddedNewlines(h));
|
|
190
|
+
const safeRows = rows.map((row) => row.map((cell) => this.collapseEmbeddedNewlines(cell)));
|
|
191
|
+
const allRows = [safeHeaders, ...safeRows];
|
|
192
|
+
const colWidths = safeHeaders.map((_, i) => Math.max(...allRows.map((row) => (row[i] || "").length)));
|
|
177
193
|
const formatRow = (row) => row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" | ");
|
|
178
194
|
return [
|
|
179
|
-
formatRow(
|
|
195
|
+
formatRow(safeHeaders),
|
|
180
196
|
colWidths.map((w) => "-".repeat(w)).join("-+-"),
|
|
181
|
-
...
|
|
197
|
+
...safeRows.map(formatRow),
|
|
182
198
|
].join("\n");
|
|
183
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Collapse embedded newlines to a single space so one logical CSV row
|
|
202
|
+
* renders as exactly one line in the text/markdown table output.
|
|
203
|
+
*
|
|
204
|
+
* The shared quote-aware parser (`splitCsvLines` + `splitCSVFields`)
|
|
205
|
+
* preserves newlines embedded inside a quoted field verbatim — correct
|
|
206
|
+
* for the `json` output format (where `JSON.stringify` escapes them), but
|
|
207
|
+
* left as-is here it would split a single record across multiple
|
|
208
|
+
* physical table rows. Mirrors the same normalization `CSVProcessor`
|
|
209
|
+
* already applies in its own markdown formatter.
|
|
210
|
+
*/
|
|
211
|
+
collapseEmbeddedNewlines(cell) {
|
|
212
|
+
return cell.replace(/\r\n|\n|\r/g, " ");
|
|
213
|
+
}
|
|
184
214
|
}
|
|
185
215
|
/**
|
|
186
216
|
* PDF file loader
|
package/dist/types/file.d.ts
CHANGED
|
@@ -80,6 +80,17 @@ export type FileProcessingResult = {
|
|
|
80
80
|
hasHeaders?: boolean;
|
|
81
81
|
/** Detected delimiter */
|
|
82
82
|
detectedDelimiter?: string;
|
|
83
|
+
/** Detected (or overridden) character encoding used to decode the CSV (#362) */
|
|
84
|
+
detectedEncoding?: string;
|
|
85
|
+
/** Confidence (0-100) of the detected encoding (#362) */
|
|
86
|
+
encodingConfidence?: number;
|
|
87
|
+
/** Original→sanitized column-name mapping when sanitizeColumnNames is on (#378) */
|
|
88
|
+
columnNameMapping?: Array<{
|
|
89
|
+
original: string;
|
|
90
|
+
sanitized: string;
|
|
91
|
+
}>;
|
|
92
|
+
/** True when the parse hit its time budget and returned partial rows (#379) */
|
|
93
|
+
parseTimedOut?: boolean;
|
|
83
94
|
version?: string;
|
|
84
95
|
estimatedPages?: number | null;
|
|
85
96
|
provider?: string;
|
|
@@ -125,6 +136,8 @@ export type CSVDataQualityWarning = {
|
|
|
125
136
|
*/
|
|
126
137
|
export type CSVColumnMetadata = {
|
|
127
138
|
name: string;
|
|
139
|
+
/** Original header text before sanitization, when sanitizeColumnNames rewrote it (#378) */
|
|
140
|
+
originalName?: string;
|
|
128
141
|
index: number;
|
|
129
142
|
detectedType: CSVColumnDataType;
|
|
130
143
|
/** Confidence of type detection (0-100) */
|
|
@@ -146,6 +159,17 @@ export type CSVColumnMetadata = {
|
|
|
146
159
|
/** Column name validation issues */
|
|
147
160
|
nameIssues?: string[];
|
|
148
161
|
};
|
|
162
|
+
/** A parsed CSV row: string-keyed with string (or missing) cell values (#384). */
|
|
163
|
+
export type CSVRow = Record<string, string | undefined>;
|
|
164
|
+
/** Result of decoding a buffer with encoding detection (#362). */
|
|
165
|
+
export type DecodedBuffer = {
|
|
166
|
+
/** Decoded text with any BOM removed. */
|
|
167
|
+
text: string;
|
|
168
|
+
/** iconv-lite label actually used to decode. */
|
|
169
|
+
encoding: string;
|
|
170
|
+
/** Detection confidence 0-100 (100 for BOM/override, 0 for the UTF-8 fallback). */
|
|
171
|
+
confidence: number;
|
|
172
|
+
};
|
|
149
173
|
/**
|
|
150
174
|
* CSV processor options
|
|
151
175
|
*/
|
|
@@ -155,6 +179,25 @@ export type CSVProcessorOptions = {
|
|
|
155
179
|
includeHeaders?: boolean;
|
|
156
180
|
sampleDataFormat?: SampleDataFormat;
|
|
157
181
|
extension?: string | null;
|
|
182
|
+
/**
|
|
183
|
+
* Character encoding override (#362). When omitted, the encoding is detected
|
|
184
|
+
* from a BOM then `chardet`, falling back to UTF-8. Accepts any label
|
|
185
|
+
* `iconv-lite` supports (e.g. "utf-8", "utf-16le", "windows-1252", "latin1").
|
|
186
|
+
*/
|
|
187
|
+
encoding?: string;
|
|
188
|
+
/**
|
|
189
|
+
* Rewrite column headers into valid identifiers (#378). Opt-in; default false
|
|
190
|
+
* preserves the raw header strings as object keys.
|
|
191
|
+
*/
|
|
192
|
+
sanitizeColumnNames?: boolean;
|
|
193
|
+
/** Case style used when `sanitizeColumnNames` is on (#378). Default "snake_case". */
|
|
194
|
+
columnNameCase?: "camelCase" | "snake_case";
|
|
195
|
+
/**
|
|
196
|
+
* Wall-clock cap for the streaming parse in milliseconds (#379). On timeout the
|
|
197
|
+
* parse returns the rows collected so far and flags `metadata.parseTimedOut`,
|
|
198
|
+
* rather than hanging forever. Defaults: 30s for strings, 5min for files.
|
|
199
|
+
*/
|
|
200
|
+
parseTimeoutMs?: number;
|
|
158
201
|
};
|
|
159
202
|
/**
|
|
160
203
|
* PDF API types for different providers
|