@juspay/neurolink 12.30.1 → 12.31.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 +399 -382
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/core/baseProvider.js +1 -0
- package/dist/core/modules/MessageBuilder.js +10 -0
- package/dist/neurolink.js +1 -0
- package/dist/processors/document/ExcelProcessor.d.ts +13 -1
- package/dist/processors/document/ExcelProcessor.js +118 -0
- package/dist/processors/document/WordProcessor.js +21 -0
- package/dist/types/file.d.ts +15 -0
- package/dist/types/generate.d.ts +11 -1
- package/dist/types/processor.d.ts +11 -0
- package/dist/types/stream.d.ts +6 -1
- package/dist/types/utilities.d.ts +19 -0
- package/dist/utils/fileDetector.js +31 -36
- package/dist/utils/htmlToMarkdown.d.ts +23 -0
- package/dist/utils/htmlToMarkdown.js +442 -0
- package/dist/utils/messageBuilder.js +3 -0
- package/dist/utils/multimodalOptionsBuilder.d.ts +3 -0
- package/dist/utils/multimodalOptionsBuilder.js +6 -0
- package/docs-site/static/search-index.json +18 -3
- package/package.json +4 -2
|
@@ -4,4 +4,4 @@ import type { OptionSchema, TextGenerationOptions } from "../../types/index.js";
|
|
|
4
4
|
* This object provides metadata for validation and help text in the CLI loop.
|
|
5
5
|
* It is derived from the main TextGenerationOptions interface to ensure consistency.
|
|
6
6
|
*/
|
|
7
|
-
export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "imageOptions" | "videoOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation" | "toolExecutionCapture" | "toolExecutionRecorder" | "toolRoots">, OptionSchema>;
|
|
7
|
+
export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "imageOptions" | "videoOptions" | "officeOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation" | "toolExecutionCapture" | "toolExecutionRecorder" | "toolRoots">, OptionSchema>;
|
|
@@ -937,6 +937,7 @@ export class BaseProvider {
|
|
|
937
937
|
csvOptions: options.csvOptions,
|
|
938
938
|
pdfOptions: options.pdfOptions,
|
|
939
939
|
videoOptions: options.videoOptions,
|
|
940
|
+
officeOptions: options.officeOptions,
|
|
940
941
|
// Forward abort, tool filtering, and timeout options to prevent
|
|
941
942
|
// silent bypass when falling back from real streaming to fake streaming
|
|
942
943
|
abortSignal: options.abortSignal,
|
|
@@ -109,6 +109,11 @@ export class MessageBuilder {
|
|
|
109
109
|
// transcription flag are read downstream by the detector, and
|
|
110
110
|
// a reconstruction that omits them silently restores defaults.
|
|
111
111
|
videoOptions: options.videoOptions,
|
|
112
|
+
// This object is rebuilt field by field, so anything not named
|
|
113
|
+
// here never reaches the multimodal builder — the same way
|
|
114
|
+
// audioFiles/videoFiles went missing in #284. Office options
|
|
115
|
+
// steer XLSX sheet selection and rendering.
|
|
116
|
+
officeOptions: options.officeOptions,
|
|
112
117
|
provider: options.provider,
|
|
113
118
|
model: options.model,
|
|
114
119
|
temperature: options.temperature,
|
|
@@ -244,6 +249,11 @@ export class MessageBuilder {
|
|
|
244
249
|
// transcription flag are read downstream by the detector, and
|
|
245
250
|
// a reconstruction that omits them silently restores defaults.
|
|
246
251
|
videoOptions: options.videoOptions,
|
|
252
|
+
// This object is rebuilt field by field, so anything not named
|
|
253
|
+
// here never reaches the multimodal builder — the same way
|
|
254
|
+
// audioFiles/videoFiles went missing in #284. Office options
|
|
255
|
+
// steer XLSX sheet selection and rendering.
|
|
256
|
+
officeOptions: options.officeOptions,
|
|
247
257
|
provider: options.provider,
|
|
248
258
|
model: options.model,
|
|
249
259
|
temperature: options.temperature,
|
package/dist/neurolink.js
CHANGED
|
@@ -4284,6 +4284,7 @@ Current user's request: ${currentInput}`;
|
|
|
4284
4284
|
// caller's video settings have ever reached a provider -- frames and
|
|
4285
4285
|
// quality included, not just the native-video flags added alongside.
|
|
4286
4286
|
videoOptions: options.videoOptions,
|
|
4287
|
+
officeOptions: options.officeOptions,
|
|
4287
4288
|
region: options.region,
|
|
4288
4289
|
tts: options.tts,
|
|
4289
4290
|
stt: options.stt,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
38
|
import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
|
|
39
|
-
import type { FileInfo, ProcessOptions, ProcessedExcel, ProcessorFileProcessingResult } from "../../types/index.js";
|
|
39
|
+
import type { ExcelWorksheet, FileInfo, OfficeProcessorOptions, ProcessOptions, ProcessedExcel, ProcessorFileProcessingResult } from "../../types/index.js";
|
|
40
40
|
/**
|
|
41
41
|
* Excel Processor - handles .xlsx and .xls files.
|
|
42
42
|
* Uses exceljs library for parsing with support for large files.
|
|
@@ -136,6 +136,18 @@ export declare class ExcelProcessor extends BaseFileProcessor<ProcessedExcel> {
|
|
|
136
136
|
*/
|
|
137
137
|
extractSheetRange(buffer: Buffer, sheet?: string | number, rowStart?: number, rowEnd?: number, columns?: string[]): Promise<string>;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Render processed worksheets into the text handed to the model.
|
|
141
|
+
*
|
|
142
|
+
* @param worksheets - Worksheets extracted from the workbook
|
|
143
|
+
* @param totalRows - Total row count across all extracted worksheets
|
|
144
|
+
* @param options - Office options; `sheetName` selects a single sheet and
|
|
145
|
+
* `formatStyle` chooses the rendering (default `raw`, the tab-separated
|
|
146
|
+
* preview that predates the option)
|
|
147
|
+
* @returns Prompt-ready text, or a message naming the available sheets when
|
|
148
|
+
* `sheetName` matches none of them
|
|
149
|
+
*/
|
|
150
|
+
export declare function formatWorksheetsForPrompt(worksheets: ExcelWorksheet[], totalRows: number, options?: OfficeProcessorOptions): string;
|
|
139
151
|
/**
|
|
140
152
|
* Singleton Excel processor instance.
|
|
141
153
|
* Use this for standard Excel processing operations.
|
|
@@ -39,6 +39,7 @@ import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
|
|
|
39
39
|
import { SIZE_LIMITS } from "../config/index.js";
|
|
40
40
|
import { FileErrorCode } from "../errors/index.js";
|
|
41
41
|
import { tryImport } from "../../utils/tryImport.js";
|
|
42
|
+
import { dedupeColumnNames } from "../../utils/csvProcessor.js";
|
|
42
43
|
let _exceljs = null;
|
|
43
44
|
async function loadExcelJS() {
|
|
44
45
|
if (_exceljs) {
|
|
@@ -482,6 +483,123 @@ export class ExcelProcessor extends BaseFileProcessor {
|
|
|
482
483
|
}
|
|
483
484
|
}
|
|
484
485
|
// =============================================================================
|
|
486
|
+
// PROMPT FORMATTING
|
|
487
|
+
// =============================================================================
|
|
488
|
+
/** Rows beyond this per sheet are summarised rather than listed. */
|
|
489
|
+
const PROMPT_PREVIEW_ROWS = 20;
|
|
490
|
+
/** RFC 4180: quote a field that contains a comma, quote or newline. */
|
|
491
|
+
function csvEscape(value) {
|
|
492
|
+
return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
493
|
+
}
|
|
494
|
+
/** A pipe inside a cell would otherwise split it into two columns. */
|
|
495
|
+
function markdownEscape(value) {
|
|
496
|
+
return value
|
|
497
|
+
.replace(/\\/g, "\\\\")
|
|
498
|
+
.replace(/\|/g, "\\|")
|
|
499
|
+
.replace(/\r?\n/g, " ");
|
|
500
|
+
}
|
|
501
|
+
function cellText(value) {
|
|
502
|
+
return String(value ?? "");
|
|
503
|
+
}
|
|
504
|
+
function formatSheetRows(sheet, formatStyle) {
|
|
505
|
+
const previewRows = sheet.rows.slice(0, PROMPT_PREVIEW_ROWS);
|
|
506
|
+
if (previewRows.length === 0) {
|
|
507
|
+
return "";
|
|
508
|
+
}
|
|
509
|
+
switch (formatStyle) {
|
|
510
|
+
case "markdown": {
|
|
511
|
+
// `rows[0]` IS the header row (that is where `headers` came from), so
|
|
512
|
+
// the body always starts at index 1 — emitting row 0 again would
|
|
513
|
+
// duplicate the header underneath the separator.
|
|
514
|
+
const header = sheet.headers.length > 0 ? sheet.headers : previewRows[0].map(cellText);
|
|
515
|
+
const body = previewRows.slice(1);
|
|
516
|
+
const columnCount = Math.max(header.length, ...body.map((row) => row.length), 1);
|
|
517
|
+
const pad = (cells) => Array.from({ length: columnCount }, (_, i) => markdownEscape(cells[i] ?? ""));
|
|
518
|
+
return [
|
|
519
|
+
`| ${pad(header).join(" | ")} |`,
|
|
520
|
+
`| ${Array.from({ length: columnCount }, () => "---").join(" | ")} |`,
|
|
521
|
+
...body.map((row) => `| ${pad(row.map(cellText)).join(" | ")} |`),
|
|
522
|
+
].join("\n");
|
|
523
|
+
}
|
|
524
|
+
case "csv":
|
|
525
|
+
return previewRows
|
|
526
|
+
.map((row) => row.map((cell) => csvEscape(cellText(cell))).join(","))
|
|
527
|
+
.join("\n");
|
|
528
|
+
case "json": {
|
|
529
|
+
const rawHeader = sheet.headers.length > 0 ? sheet.headers : previewRows[0].map(cellText);
|
|
530
|
+
const body = previewRows.slice(1);
|
|
531
|
+
// A data row's own populated-cell count can exceed the header's — row
|
|
532
|
+
// extraction sizes each row on its own last populated cell, not a
|
|
533
|
+
// workbook-wide column count, so a header truncated by a merged cell
|
|
534
|
+
// (or a value landing in a column the header left blank) is a normal
|
|
535
|
+
// spreadsheet shape. "markdown" already sizes on the widest row for
|
|
536
|
+
// exactly this reason; do the same here instead of zipping strictly to
|
|
537
|
+
// header.length, which silently dropped every cell past it.
|
|
538
|
+
const columnCount = Math.max(rawHeader.length, ...body.map((row) => row.length), 1);
|
|
539
|
+
const extendedHeader = Array.from({ length: columnCount }, (_, i) => rawHeader[i] ?? `Column ${i + 1}`);
|
|
540
|
+
// Object.fromEntries silently drops a column when two header cells are
|
|
541
|
+
// literally the same string (a copy-pasted header, a merged-and-split
|
|
542
|
+
// sheet) — the later column's value overwrites the earlier one under
|
|
543
|
+
// the shared key. Dedupe first so every column keeps its own key.
|
|
544
|
+
const header = dedupeColumnNames(extendedHeader);
|
|
545
|
+
const objects = body.map((row) => Object.fromEntries(header.map((key, i) => [key, row[i] ?? null])));
|
|
546
|
+
return JSON.stringify(objects, null, 2);
|
|
547
|
+
}
|
|
548
|
+
case "raw":
|
|
549
|
+
default:
|
|
550
|
+
// Bug-for-bug with the pre-option behaviour: the header line is printed
|
|
551
|
+
// and then `rows` is printed in full, so row 1 appears twice. Callers
|
|
552
|
+
// that want a clean table ask for "markdown" or "csv".
|
|
553
|
+
return [
|
|
554
|
+
sheet.headers.join("\t"),
|
|
555
|
+
...previewRows.map((row) => row.map(cellText).join("\t")),
|
|
556
|
+
].join("\n");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Render processed worksheets into the text handed to the model.
|
|
561
|
+
*
|
|
562
|
+
* @param worksheets - Worksheets extracted from the workbook
|
|
563
|
+
* @param totalRows - Total row count across all extracted worksheets
|
|
564
|
+
* @param options - Office options; `sheetName` selects a single sheet and
|
|
565
|
+
* `formatStyle` chooses the rendering (default `raw`, the tab-separated
|
|
566
|
+
* preview that predates the option)
|
|
567
|
+
* @returns Prompt-ready text, or a message naming the available sheets when
|
|
568
|
+
* `sheetName` matches none of them
|
|
569
|
+
*/
|
|
570
|
+
export function formatWorksheetsForPrompt(worksheets, totalRows, options) {
|
|
571
|
+
const requested = options?.sheetName;
|
|
572
|
+
let selected = worksheets;
|
|
573
|
+
if (requested !== undefined) {
|
|
574
|
+
selected = worksheets.filter((sheet) => sheet.name === requested);
|
|
575
|
+
if (selected.length === 0) {
|
|
576
|
+
const available = worksheets.map((sheet) => sheet.name).join(", ");
|
|
577
|
+
return `Sheet "${requested}" not found. Available sheets: ${available || "(none)"}`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const formatStyle = options?.formatStyle ?? "raw";
|
|
581
|
+
const selectedRows = selected.reduce((sum, sheet) => sum + sheet.rowCount, 0);
|
|
582
|
+
const scope = requested !== undefined
|
|
583
|
+
? `Spreadsheet: sheet "${requested}" of ${worksheets.length}, ${selectedRows} rows`
|
|
584
|
+
: `Spreadsheet: ${worksheets.length} sheet(s), ${totalRows} total rows`;
|
|
585
|
+
let text = `${scope}\n`;
|
|
586
|
+
for (const sheet of selected) {
|
|
587
|
+
text += `\n### Sheet: ${sheet.name}\n`;
|
|
588
|
+
text += `Columns (${sheet.columnCount}): ${sheet.headers.join(", ")}\n`;
|
|
589
|
+
text += `Rows: ${sheet.rowCount}\n`;
|
|
590
|
+
const rendered = formatSheetRows(sheet, formatStyle);
|
|
591
|
+
if (!rendered) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
text += `\nData:\n${rendered}\n`;
|
|
595
|
+
const remaining = sheet.rowCount - PROMPT_PREVIEW_ROWS;
|
|
596
|
+
if (remaining > 0) {
|
|
597
|
+
text += `... (${remaining} more rows)\n`;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return text;
|
|
601
|
+
}
|
|
602
|
+
// =============================================================================
|
|
485
603
|
// SINGLETON INSTANCE
|
|
486
604
|
// =============================================================================
|
|
487
605
|
/**
|
|
@@ -35,6 +35,7 @@ import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
|
|
|
35
35
|
import { SIZE_LIMITS } from "../config/index.js";
|
|
36
36
|
import { FileErrorCode } from "../errors/index.js";
|
|
37
37
|
import { tryImport } from "../../utils/tryImport.js";
|
|
38
|
+
import { htmlToMarkdown } from "../../utils/htmlToMarkdown.js";
|
|
38
39
|
let _mammoth = null;
|
|
39
40
|
async function loadMammoth() {
|
|
40
41
|
if (_mammoth) {
|
|
@@ -64,6 +65,22 @@ const SUPPORTED_WORD_EXTENSIONS = [".docx", ".doc"];
|
|
|
64
65
|
* Word documents can be larger due to embedded images and complex formatting
|
|
65
66
|
*/
|
|
66
67
|
const WORD_TIMEOUT_MS = 60000;
|
|
68
|
+
/**
|
|
69
|
+
* Derive the document metrics from the extracted plain text.
|
|
70
|
+
*
|
|
71
|
+
* Counted off `textContent` rather than the markdown so the numbers describe
|
|
72
|
+
* the document's prose, not the syntax added to represent it.
|
|
73
|
+
*/
|
|
74
|
+
function measureText(textContent) {
|
|
75
|
+
const trimmed = textContent.trim();
|
|
76
|
+
return {
|
|
77
|
+
wordCount: trimmed ? trimmed.split(/\s+/).length : 0,
|
|
78
|
+
paragraphCount: textContent
|
|
79
|
+
.split(/\n+/)
|
|
80
|
+
.filter((line) => line.trim().length > 0).length,
|
|
81
|
+
characterCount: textContent.length,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
67
84
|
// =============================================================================
|
|
68
85
|
// WORD PROCESSOR CLASS
|
|
69
86
|
// =============================================================================
|
|
@@ -144,7 +161,9 @@ export class WordProcessor extends BaseFileProcessor {
|
|
|
144
161
|
return {
|
|
145
162
|
textContent: "",
|
|
146
163
|
htmlContent: "",
|
|
164
|
+
markdownContent: "",
|
|
147
165
|
warnings: [],
|
|
166
|
+
...measureText(""),
|
|
148
167
|
buffer,
|
|
149
168
|
mimetype: fileInfo.mimetype ||
|
|
150
169
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
@@ -267,7 +286,9 @@ export class WordProcessor extends BaseFileProcessor {
|
|
|
267
286
|
filename: this.getFilename(fileInfo),
|
|
268
287
|
textContent,
|
|
269
288
|
htmlContent,
|
|
289
|
+
markdownContent: htmlToMarkdown(htmlContent),
|
|
270
290
|
warnings,
|
|
291
|
+
...measureText(textContent),
|
|
271
292
|
},
|
|
272
293
|
};
|
|
273
294
|
}
|
package/dist/types/file.d.ts
CHANGED
|
@@ -611,6 +611,21 @@ export type OfficeProcessorOptions = {
|
|
|
611
611
|
processAllSheets?: boolean;
|
|
612
612
|
/** For presentations (pptx only): whether to include slide notes */
|
|
613
613
|
includeSlideNotes?: boolean;
|
|
614
|
+
/**
|
|
615
|
+
* For spreadsheets (xlsx only): restrict processing to the named sheet.
|
|
616
|
+
* When the workbook has no sheet by that name the result says so and lists
|
|
617
|
+
* the names it does have, rather than silently falling back to every sheet.
|
|
618
|
+
* Omit to process every sheet (the default).
|
|
619
|
+
*/
|
|
620
|
+
sheetName?: string;
|
|
621
|
+
/**
|
|
622
|
+
* For spreadsheets (xlsx only): how sheet data is rendered into the prompt.
|
|
623
|
+
* - `raw` (default): tab-separated preview — the long-standing behaviour
|
|
624
|
+
* - `csv`: comma-separated values with RFC 4180 quoting
|
|
625
|
+
* - `markdown`: a markdown table per sheet
|
|
626
|
+
* - `json`: an array of row objects keyed by the header row
|
|
627
|
+
*/
|
|
628
|
+
formatStyle?: "raw" | "markdown" | "json" | "csv";
|
|
614
629
|
};
|
|
615
630
|
/**
|
|
616
631
|
* File detector options
|
package/dist/types/generate.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { AvatarOptions, AvatarResult } from "./avatar.js";
|
|
|
18
18
|
import type { MusicOptions, MusicResult } from "./music.js";
|
|
19
19
|
import type { StandardRecord, ValidationSchema, ZodUnknownSchema } from "./aliases.js";
|
|
20
20
|
import type { NeurolinkCredentials } from "./providers.js";
|
|
21
|
-
import type { CSVProcessorOptions, VideoProcessorOptions, FileWithMetadata, MultimodalAudioEntry, MultimodalVideoEntry, VisionImageOutputFormat } from "./file.js";
|
|
21
|
+
import type { CSVProcessorOptions, VideoProcessorOptions, OfficeProcessorOptions, FileWithMetadata, MultimodalAudioEntry, MultimodalVideoEntry, VisionImageOutputFormat } from "./file.js";
|
|
22
22
|
import type { WorkflowConfig } from "./workflow.js";
|
|
23
23
|
import type { Schema, Tool, ToolChoice } from "./tools.js";
|
|
24
24
|
import type { StepResult, LanguageModel } from "./providers.js";
|
|
@@ -150,6 +150,11 @@ export type GenerateOptions = {
|
|
|
150
150
|
music?: MusicOptions;
|
|
151
151
|
};
|
|
152
152
|
csvOptions?: CSVProcessorOptions;
|
|
153
|
+
/**
|
|
154
|
+
* Office document processing options. Currently consumed by the XLSX path
|
|
155
|
+
* (`sheetName`, `formatStyle`); see OfficeProcessorOptions.
|
|
156
|
+
*/
|
|
157
|
+
officeOptions?: OfficeProcessorOptions;
|
|
153
158
|
/** PDF processing options (#258). */
|
|
154
159
|
pdfOptions?: {
|
|
155
160
|
/** Password for an encrypted PDF (image-conversion fallback path). */
|
|
@@ -1437,6 +1442,11 @@ export type TextGenerationOptions = {
|
|
|
1437
1442
|
* saying so.
|
|
1438
1443
|
*/
|
|
1439
1444
|
videoOptions?: VideoProcessorOptions;
|
|
1445
|
+
/**
|
|
1446
|
+
* Office document processing options. Currently consumed by the XLSX path
|
|
1447
|
+
* (`sheetName`, `formatStyle`); see OfficeProcessorOptions.
|
|
1448
|
+
*/
|
|
1449
|
+
officeOptions?: OfficeProcessorOptions;
|
|
1440
1450
|
/** PDF processing options (#258). */
|
|
1441
1451
|
pdfOptions?: {
|
|
1442
1452
|
/** Password for an encrypted PDF (image-conversion fallback path). */
|
|
@@ -639,8 +639,19 @@ export type ProcessedWord = ProcessedFileBase & {
|
|
|
639
639
|
textContent: string;
|
|
640
640
|
/** HTML representation of the Word document */
|
|
641
641
|
htmlContent: string;
|
|
642
|
+
/**
|
|
643
|
+
* Markdown rendering of `htmlContent`, preserving the structure that plain
|
|
644
|
+
* text loses: headings, ordered/unordered lists and tables.
|
|
645
|
+
*/
|
|
646
|
+
markdownContent: string;
|
|
642
647
|
/** Warnings from mammoth extraction (e.g., unsupported elements) */
|
|
643
648
|
warnings: string[];
|
|
649
|
+
/** Whitespace-delimited word count of `textContent`. */
|
|
650
|
+
wordCount: number;
|
|
651
|
+
/** Count of non-empty text blocks in `textContent`. */
|
|
652
|
+
paragraphCount: number;
|
|
653
|
+
/** Character count of `textContent`, including whitespace. */
|
|
654
|
+
characterCount: number;
|
|
644
655
|
};
|
|
645
656
|
/**
|
|
646
657
|
* Processed RTF document result.
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { AIModelProviderConfig, NeurolinkCredentials } from "./providers.js
|
|
|
16
16
|
import type { TTSChunk, TTSOptions, TTSResult } from "./tts.js";
|
|
17
17
|
import type { STTOptions, STTResult } from "./stt.js";
|
|
18
18
|
import type { StandardRecord, ValidationSchema } from "./aliases.js";
|
|
19
|
-
import type { CSVProcessorOptions, FileWithMetadata, VideoProcessorOptions, VisionImageOutputFormat } from "./file.js";
|
|
19
|
+
import type { CSVProcessorOptions, FileWithMetadata, VideoProcessorOptions, VisionImageOutputFormat, OfficeProcessorOptions } from "./file.js";
|
|
20
20
|
import type { WorkflowConfig } from "./workflow.js";
|
|
21
21
|
import type { LanguageModel, StepResult } from "./providers.js";
|
|
22
22
|
import type { Tool, ToolChoice } from "./tools.js";
|
|
@@ -366,6 +366,11 @@ export type StreamOptions = {
|
|
|
366
366
|
* saying so.
|
|
367
367
|
*/
|
|
368
368
|
videoOptions?: VideoProcessorOptions;
|
|
369
|
+
/**
|
|
370
|
+
* Office document processing options. Currently consumed by the XLSX path
|
|
371
|
+
* (`sheetName`, `formatStyle`); see OfficeProcessorOptions.
|
|
372
|
+
*/
|
|
373
|
+
officeOptions?: OfficeProcessorOptions;
|
|
369
374
|
/** PDF processing options (#258). */
|
|
370
375
|
pdfOptions?: {
|
|
371
376
|
/** Password for an encrypted PDF (image-conversion fallback path). */
|
|
@@ -321,3 +321,22 @@ export type ScalarRecoveryDecision = {
|
|
|
321
321
|
} | {
|
|
322
322
|
kind: "not-json";
|
|
323
323
|
};
|
|
324
|
+
/**
|
|
325
|
+
* An element in the parsed HTML tree used for Markdown conversion.
|
|
326
|
+
*/
|
|
327
|
+
export type MarkdownHtmlElementNode = {
|
|
328
|
+
kind: "element";
|
|
329
|
+
/** Lower-cased tag name. */
|
|
330
|
+
tag: string;
|
|
331
|
+
/** Lower-cased attribute names mapped to their decoded values. */
|
|
332
|
+
attrs: Record<string, string>;
|
|
333
|
+
children: MarkdownHtmlNode[];
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* A node in the parsed HTML tree used for Markdown conversion: either a text
|
|
337
|
+
* run or an element.
|
|
338
|
+
*/
|
|
339
|
+
export type MarkdownHtmlNode = {
|
|
340
|
+
kind: "text";
|
|
341
|
+
value: string;
|
|
342
|
+
} | MarkdownHtmlElementNode;
|
|
@@ -341,14 +341,14 @@ export class FileDetector {
|
|
|
341
341
|
logger.warn(`[FileDetector] All fallback parsing failed for type "${detection.type}". ` +
|
|
342
342
|
`Attempted: ${options.allowedTypes.join(", ")}. Falling through to universal handler.`);
|
|
343
343
|
const csvOptions = options?.csvOptions;
|
|
344
|
-
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions);
|
|
344
|
+
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions, options?.officeOptions);
|
|
345
345
|
FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
|
|
346
346
|
return result;
|
|
347
347
|
}
|
|
348
348
|
const content = loadedUrl?.content ??
|
|
349
349
|
(await FileDetector.loadContent(input, detection, options));
|
|
350
350
|
const csvOptions = options?.csvOptions;
|
|
351
|
-
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions);
|
|
351
|
+
const result = await FileDetector.processFile(content, detection, csvOptions, options?.provider, options?.videoOptions, options?.officeOptions);
|
|
352
352
|
FileDetector.setFileResultSpanAttributes(span, result, inputFilename, detection.type);
|
|
353
353
|
return result;
|
|
354
354
|
});
|
|
@@ -1063,7 +1063,7 @@ export class FileDetector {
|
|
|
1063
1063
|
/**
|
|
1064
1064
|
* Route to appropriate processor
|
|
1065
1065
|
*/
|
|
1066
|
-
static async processFile(content, detection, options, provider, videoOptions) {
|
|
1066
|
+
static async processFile(content, detection, options, provider, videoOptions, officeOptions) {
|
|
1067
1067
|
switch (detection.type) {
|
|
1068
1068
|
case "csv":
|
|
1069
1069
|
// Pass original extension through to CSV processor; if detection has none,
|
|
@@ -1087,7 +1087,7 @@ export class FileDetector {
|
|
|
1087
1087
|
case "archive":
|
|
1088
1088
|
return await FileDetector.processArchiveFile(content, detection);
|
|
1089
1089
|
case "xlsx":
|
|
1090
|
-
return await FileDetector.processXlsxFile(content, detection);
|
|
1090
|
+
return await FileDetector.processXlsxFile(content, detection, officeOptions);
|
|
1091
1091
|
case "docx":
|
|
1092
1092
|
return await FileDetector.processDocxFile(content, detection);
|
|
1093
1093
|
case "pptx":
|
|
@@ -1283,7 +1283,7 @@ export class FileDetector {
|
|
|
1283
1283
|
/**
|
|
1284
1284
|
* Process Excel/OpenDocument spreadsheet file via ExcelProcessor or OpenDocumentProcessor
|
|
1285
1285
|
*/
|
|
1286
|
-
static async processXlsxFile(content, detection) {
|
|
1286
|
+
static async processXlsxFile(content, detection, officeOptions) {
|
|
1287
1287
|
const xlsxFilename = detection.metadata.filename || "spreadsheet";
|
|
1288
1288
|
try {
|
|
1289
1289
|
const ext = detection.extension?.toLowerCase();
|
|
@@ -1308,7 +1308,7 @@ export class FileDetector {
|
|
|
1308
1308
|
}
|
|
1309
1309
|
}
|
|
1310
1310
|
else {
|
|
1311
|
-
const { excelProcessor } = await import("../processors/document/ExcelProcessor.js");
|
|
1311
|
+
const { excelProcessor, formatWorksheetsForPrompt } = await import("../processors/document/ExcelProcessor.js");
|
|
1312
1312
|
const xlsxResult = await excelProcessor.processFile({
|
|
1313
1313
|
id: xlsxFilename,
|
|
1314
1314
|
name: xlsxFilename,
|
|
@@ -1318,30 +1318,9 @@ export class FileDetector {
|
|
|
1318
1318
|
buffer: content,
|
|
1319
1319
|
});
|
|
1320
1320
|
if (xlsxResult.success && xlsxResult.data) {
|
|
1321
|
-
// Build text content from worksheets
|
|
1322
|
-
const sheets = xlsxResult.data.worksheets || [];
|
|
1323
|
-
let textContent = `Spreadsheet: ${sheets.length} sheet(s), ${xlsxResult.data.totalRows} total rows\n`;
|
|
1324
|
-
for (const sheet of sheets) {
|
|
1325
|
-
textContent += `\n### Sheet: ${sheet.name}\n`;
|
|
1326
|
-
textContent += `Columns (${sheet.columnCount}): ${sheet.headers.join(", ")}\n`;
|
|
1327
|
-
textContent += `Rows: ${sheet.rowCount}\n`;
|
|
1328
|
-
// Include first rows as sample data
|
|
1329
|
-
const sampleRows = sheet.rows.slice(0, 20);
|
|
1330
|
-
const rowText = sampleRows
|
|
1331
|
-
.map((row) => row.map((c) => String(c ?? "")).join("\t"))
|
|
1332
|
-
.join("\n");
|
|
1333
|
-
if (!rowText) {
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
textContent += `\nData:\n${sheet.headers.join("\t")}\n${rowText}\n`;
|
|
1337
|
-
const remaining = sheet.rowCount - 20;
|
|
1338
|
-
if (remaining > 0) {
|
|
1339
|
-
textContent += `... (${remaining} more rows)\n`;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
1321
|
return {
|
|
1343
1322
|
type: "xlsx",
|
|
1344
|
-
content:
|
|
1323
|
+
content: formatWorksheetsForPrompt(xlsxResult.data.worksheets || [], xlsxResult.data.totalRows, officeOptions),
|
|
1345
1324
|
mimeType: detection.mimeType,
|
|
1346
1325
|
metadata: detection.metadata,
|
|
1347
1326
|
};
|
|
@@ -1374,13 +1353,18 @@ export class FileDetector {
|
|
|
1374
1353
|
try {
|
|
1375
1354
|
if (ext === "odt") {
|
|
1376
1355
|
const { openDocumentProcessor } = await import("../processors/document/OpenDocumentProcessor.js");
|
|
1377
|
-
|
|
1356
|
+
// Bounded per the project's async-timeout guideline, matching the ODP
|
|
1357
|
+
// branch of processPptxFile below: this unzips and parses
|
|
1358
|
+
// attacker-supplied bytes, and a stalled parse would otherwise hold
|
|
1359
|
+
// the request open with no ceiling. On timeout the throw lands in
|
|
1360
|
+
// this block's existing catch, which degrades to the placeholder.
|
|
1361
|
+
const odtResult = await withTimeout(openDocumentProcessor.processFile({
|
|
1378
1362
|
id: docxFilename,
|
|
1379
1363
|
name: docxFilename,
|
|
1380
1364
|
mimetype: detection.mimeType || "application/vnd.oasis.opendocument.text",
|
|
1381
1365
|
size: content.length,
|
|
1382
1366
|
buffer: content,
|
|
1383
|
-
});
|
|
1367
|
+
}), FileDetector.DEFAULT_DOCUMENT_TIMEOUT);
|
|
1384
1368
|
if (odtResult.success && odtResult.data) {
|
|
1385
1369
|
return {
|
|
1386
1370
|
type: "docx",
|
|
@@ -1393,13 +1377,14 @@ export class FileDetector {
|
|
|
1393
1377
|
}
|
|
1394
1378
|
else if (ext === "rtf") {
|
|
1395
1379
|
const { rtfProcessor } = await import("../processors/document/RtfProcessor.js");
|
|
1396
|
-
|
|
1380
|
+
// Bounded for the same reason as the odt branch above.
|
|
1381
|
+
const rtfResult = await withTimeout(rtfProcessor.processFile({
|
|
1397
1382
|
id: docxFilename,
|
|
1398
1383
|
name: docxFilename,
|
|
1399
1384
|
mimetype: detection.mimeType || "application/rtf",
|
|
1400
1385
|
size: content.length,
|
|
1401
1386
|
buffer: content,
|
|
1402
|
-
});
|
|
1387
|
+
}), FileDetector.DEFAULT_DOCUMENT_TIMEOUT);
|
|
1403
1388
|
if (rtfResult.success && rtfResult.data) {
|
|
1404
1389
|
return {
|
|
1405
1390
|
type: "docx",
|
|
@@ -1412,19 +1397,29 @@ export class FileDetector {
|
|
|
1412
1397
|
}
|
|
1413
1398
|
else {
|
|
1414
1399
|
const { wordProcessor } = await import("../processors/document/WordProcessor.js");
|
|
1415
|
-
|
|
1400
|
+
// Bounded for the same reason as the odt branch above. This call now
|
|
1401
|
+
// also runs htmlToMarkdown's recursive HTML-to-markdown rendering
|
|
1402
|
+
// over mammoth's output, so it is real additional work inside what
|
|
1403
|
+
// was already an unbounded call.
|
|
1404
|
+
const docxResult = await withTimeout(wordProcessor.processFile({
|
|
1416
1405
|
id: docxFilename,
|
|
1417
1406
|
name: docxFilename,
|
|
1418
1407
|
mimetype: detection.mimeType ||
|
|
1419
1408
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1420
1409
|
size: content.length,
|
|
1421
1410
|
buffer: content,
|
|
1422
|
-
});
|
|
1411
|
+
}), FileDetector.DEFAULT_DOCUMENT_TIMEOUT);
|
|
1423
1412
|
if (docxResult.success && docxResult.data) {
|
|
1413
|
+
// Prefer the markdown rendering: it keeps the headings, lists and
|
|
1414
|
+
// tables that the plain-text extraction flattens away. Plain text
|
|
1415
|
+
// remains the fallback for a document with no convertible structure.
|
|
1416
|
+
const { markdownContent, textContent, wordCount, paragraphCount, characterCount, } = docxResult.data;
|
|
1417
|
+
const body = markdownContent || textContent;
|
|
1424
1418
|
return {
|
|
1425
1419
|
type: "docx",
|
|
1426
|
-
content:
|
|
1427
|
-
|
|
1420
|
+
content: body
|
|
1421
|
+
? `Document: ${wordCount} words, ${paragraphCount} paragraphs, ${characterCount} characters\n\n${body}`
|
|
1422
|
+
: FileDetector.formatInformativePlaceholder("Document", docxFilename, content, detection),
|
|
1428
1423
|
mimeType: detection.mimeType,
|
|
1429
1424
|
metadata: detection.metadata,
|
|
1430
1425
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML to Markdown conversion for document processors.
|
|
3
|
+
*
|
|
4
|
+
* Scoped deliberately to the small, well-formed HTML subset that document
|
|
5
|
+
* converters emit (mammoth for DOCX): headings, paragraphs, emphasis, links,
|
|
6
|
+
* images, lists, block quotes, preformatted text and tables. It is not a
|
|
7
|
+
* general-purpose web-page converter, which is why the repository gains no
|
|
8
|
+
* dependency for it.
|
|
9
|
+
*
|
|
10
|
+
* @module utils/htmlToMarkdown
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Convert an HTML fragment to Markdown.
|
|
14
|
+
*
|
|
15
|
+
* @param html - HTML produced by a document converter
|
|
16
|
+
* @returns Markdown text, or an empty string when the fragment has no content
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* htmlToMarkdown("<h1>Title</h1><p>Body</p>"); // "# Title\n\nBody"
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function htmlToMarkdown(html: string): string;
|