diff --git a/workspaces/intelligent-assistant/.changeset/fuzzy-peaches-shake.md b/workspaces/intelligent-assistant/.changeset/fuzzy-peaches-shake.md new file mode 100644 index 00000000000..b5452a9a0d2 --- /dev/null +++ b/workspaces/intelligent-assistant/.changeset/fuzzy-peaches-shake.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed-backend': minor +--- + +bugfix - Notebooks routes 404 passthrough error resolved diff --git a/workspaces/intelligent-assistant/.changeset/six-chairs-turn.md b/workspaces/intelligent-assistant/.changeset/six-chairs-turn.md new file mode 100644 index 00000000000..878976acd90 --- /dev/null +++ b/workspaces/intelligent-assistant/.changeset/six-chairs-turn.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend': minor +--- + +AI Notebooks will use markitdown to clean up data before vectorizing documents diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/package.json b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/package.json index e8a42558ff9..6a64c8c0f3b 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/package.json +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/package.json @@ -54,12 +54,10 @@ "express": "^4.21.1", "express-rate-limit": "^8.2.2", "form-data": "^4.0.6", - "htmlparser2": "^9.1.0", "http-proxy-middleware": "^3.0.7", - "js-yaml": "^4.1.1", "knex": "^3.1.0", - "multer": "^2.2.0", - "pdfjs-dist": "^4.10.38" + "markitdown-ts": "^0.0.10", + "multer": "^2.2.0" }, "devDependencies": { "@backstage/backend-test-utils": "^1.11.4", @@ -67,7 +65,6 @@ "@ianvs/prettier-plugin-sort-imports": "^4.4.0", "@spotify/prettier-config": "^15.0.0", "@types/express": "4.17.25", - "@types/js-yaml": "^4", "@types/multer": "^1.4.12", "@types/supertest": "2.0.16", "msw": "2.14.6", diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentHelpers.test.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentHelpers.test.ts index 792d06de276..b8d491ba5c1 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentHelpers.test.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentHelpers.test.ts @@ -14,22 +14,10 @@ * limitations under the License. */ -import { mockServices } from '@backstage/backend-test-utils'; - import { DEFAULT_MAX_FILE_SIZE_MB } from '../../constant'; -import { - isValidFileSize, - isValidFileType, - parseFileContent, -} from './documentHelpers'; +import { isValidFileSize, isValidFileType } from './documentHelpers'; describe('documentHelpers', () => { - const logger = mockServices.logger.mock(); - - afterEach(() => { - jest.clearAllMocks(); - }); - describe('isValidFileSize', () => { const MB = 1024 * 1024; @@ -93,38 +81,4 @@ describe('documentHelpers', () => { expect(isValidFileType('.')).toBe(false); }); }); - - describe('parseFileContent', () => { - it('should throw error when no file uploaded for non-URL type', async () => { - await expect(parseFileContent(logger, 'txt', undefined)).rejects.toThrow( - 'No file uploaded', - ); - }); - - it('should throw error when file size exceeds limit', async () => { - const largeFile = { - buffer: Buffer.from('test'), - originalname: 'large.txt', - size: 21 * 1024 * 1024, // 21MB - } as Express.Multer.File; - - await expect(parseFileContent(logger, 'txt', largeFile)).rejects.toThrow( - `File size exceeds ${DEFAULT_MAX_FILE_SIZE_MB / 1024 / 1024}MB limit`, - ); - }); - - it('should parse valid file successfully', async () => { - const file = { - buffer: Buffer.from('test content'), - originalname: 'test.txt', - size: 1024, - } as Express.Multer.File; - - const result = await parseFileContent(logger, 'txt', file); - - expect(result.content).toBe('test content'); - expect(result.metadata.fileName).toBe('test.txt'); - expect(result.metadata.fileType).toBe('txt'); - }); - }); }); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentHelpers.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentHelpers.ts index 880054b81d9..495f57be8ae 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentHelpers.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentHelpers.ts @@ -13,25 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { LoggerService } from '@backstage/backend-plugin-api'; import { InputError } from '@backstage/errors'; -import { Parser } from 'htmlparser2'; - import dns from 'dns/promises'; import { isIP } from 'net'; import { DEFAULT_MAX_FILE_SIZE_MB, FILTERED_CONTENT_MARKER, - HTML_BLOCK_TAGS, - HTML_IGNORED_TAGS, MAX_CONSECUTIVE_NEWLINES, PROMPT_INJECTION_PATTERNS, SSRF_BLOCKED_HOSTNAMES, SupportedFileType, } from '../../constant'; -import { parseFile } from './fileParser'; // ============================================================================== // URL Support Functions (Reserved for Future URL File Type Support) @@ -78,30 +72,6 @@ export const isValidFileType = (fileType: string): boolean => { ); }; -/** - * Parse file from upload - * @param logger - Logger service - * @param fileType - File type - * @param file - File to parse - * @returns Parsed file - */ -export const parseFileContent = async ( - logger: LoggerService, - fileType: string, - file?: Express.Multer.File, -) => { - if (!file) { - throw new InputError('No file uploaded'); - } - if (!isValidFileSize(file.size)) { - throw new InputError( - `File size exceeds ${DEFAULT_MAX_FILE_SIZE_MB / 1024 / 1024}MB limit`, - ); - } - logger.info(`Parsing file ${file.originalname} for fileType ${fileType}`); - return await parseFile(file.buffer, file.originalname, fileType); -}; - /** * Check if an IP address is private, internal, or a metadata endpoint * Blocks SSRF attacks to internal networks @@ -243,39 +213,6 @@ export const validateURLForSSRF = async (urlString: string): Promise => { } }; -/** - * Strip HTML tags and extract readable text from HTML content - * @reserved Reserved for future URL file type support - */ -export const stripHtmlTags = (html: string): string => { - let text = ''; - let ignoring = false; - - const parser = new Parser( - { - onopentag(name) { - if (HTML_IGNORED_TAGS.has(name)) ignoring = true; - }, - onclosetag(name) { - if (HTML_IGNORED_TAGS.has(name)) ignoring = false; - if (HTML_BLOCK_TAGS.has(name)) text += '\n'; - }, - ontext(data) { - if (!ignoring) text += data; - }, - }, - { decodeEntities: true }, - ); - - parser.write(html); - parser.end(); - - return text - .replace(/\n\s*\n/g, '\n\n') - .replace(/[ \t]+/g, ' ') - .trim(); -}; - /** * Sanitize content to prevent prompt injection attacks * Detects and filters common prompt injection patterns diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentService.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentService.ts index 0816d885898..81164083949 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentService.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/documentService.ts @@ -25,7 +25,6 @@ import { } from '../../constant'; import { SessionDocument, UpsertResult } from '../types/notebooksTypes'; import { VectorStoresOperator } from '../VectorStoresOperator'; -import { toFile } from './fileParser'; /** * Service for managing documents within notebook sessions using File-Based API @@ -92,21 +91,22 @@ export class DocumentService { } /** - * Upload a file to the Files API - * @param content - File content as string + * Upload markdown content to the Files API + * @param content - Markdown content string * @param title - File title/name * @returns File ID from the Files API * @throws Error if upload fails */ async uploadFile(content: string, title: string): Promise { try { - // Determine MIME type from file type or default to text/plain - const mimeType = 'text/plain'; - const txtFilename = `${title.replace(/\.[^.]+$/, '')}.txt`; + const mdFilename = `${title.replace(/\.[^.]+$/, '')}.md`; + const file = await this.client.files.create({ - file: await toFile(Buffer.from(content, 'utf-8'), txtFilename, { - type: mimeType, - }), + file: { + name: mdFilename, + buffer: Buffer.from(content, 'utf-8'), + type: 'text/markdown', + }, purpose: 'assistants', }); @@ -115,11 +115,9 @@ export class DocumentService { ); return file.id; } catch (error) { - // Preserve the original error type and message if (error instanceof Error) { throw error; } - // For non-Error objects, wrap with context throw new Error(`Failed to upload file: ${String(error)}`); } } diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/fileParser.test.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/fileParser.test.ts deleted file mode 100644 index 1264c16c996..00000000000 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/fileParser.test.ts +++ /dev/null @@ -1,541 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { InputError } from '@backstage/errors'; - -import * as pdfjsLib from 'pdfjs-dist'; - -import { parseFile } from './fileParser'; - -// Mock pdfjs-dist -jest.mock('pdfjs-dist'); - -describe('fileParser', () => { - const mockGetDocument = pdfjsLib.getDocument as jest.MockedFunction< - typeof pdfjsLib.getDocument - >; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('parseFile - Text Files (md, txt, log)', () => { - it('should parse markdown files', async () => { - const buffer = Buffer.from('# Heading\n\nParagraph'); - const result = await parseFile(buffer, 'test.md', 'md'); - - expect(result.content).toBe('# Heading\n\nParagraph'); - expect(result.metadata.fileName).toBe('test.md'); - expect(result.metadata.fileType).toBe('md'); - expect(result.metadata.parseTimestamp).toBeDefined(); - }); - - it('should parse text files', async () => { - const buffer = Buffer.from('Plain text content'); - const result = await parseFile(buffer, 'test.txt', 'txt'); - - expect(result.content).toBe('Plain text content'); - expect(result.metadata.fileName).toBe('test.txt'); - expect(result.metadata.fileType).toBe('txt'); - }); - - it('should parse log files', async () => { - const logContent = - '[2024-01-01] ERROR: Something went wrong\n[2024-01-01] INFO: Process started'; - const buffer = Buffer.from(logContent); - const result = await parseFile(buffer, 'app.log', 'log'); - - expect(result.content).toBe(logContent); - expect(result.metadata.fileType).toBe('log'); - }); - - it('should handle empty text files', async () => { - const buffer = Buffer.from(''); - const result = await parseFile(buffer, 'empty.txt', 'txt'); - - expect(result.content).toBe(''); - expect(result.metadata.fileName).toBe('empty.txt'); - }); - - it('should handle files with special characters', async () => { - const buffer = Buffer.from('Content with émojis 🎉 and spëcial chars'); - const result = await parseFile(buffer, 'special.txt', 'txt'); - - expect(result.content).toBe('Content with émojis 🎉 and spëcial chars'); - }); - - it('should preserve line breaks', async () => { - const buffer = Buffer.from('Line 1\nLine 2\r\nLine 3'); - const result = await parseFile(buffer, 'lines.txt', 'txt'); - - expect(result.content).toBe('Line 1\nLine 2\r\nLine 3'); - }); - }); - - describe('parseFile - JSON Files', () => { - it('should parse valid JSON', async () => { - const jsonContent = JSON.stringify({ key: 'value', number: 42 }, null, 2); - const buffer = Buffer.from(jsonContent); - const result = await parseFile(buffer, 'data.json', 'json'); - - expect(result.content).toBe(jsonContent); - expect(result.metadata.fileName).toBe('data.json'); - expect(result.metadata.fileType).toBe('json'); - }); - - it('should parse compact JSON', async () => { - const jsonContent = '{"key":"value","array":[1,2,3]}'; - const buffer = Buffer.from(jsonContent); - const result = await parseFile(buffer, 'compact.json', 'json'); - - expect(result.content).toBe(jsonContent); - }); - - it('should parse nested JSON', async () => { - const jsonContent = JSON.stringify({ - nested: { deep: { value: 'test' } }, - array: [{ id: 1 }, { id: 2 }], - }); - const buffer = Buffer.from(jsonContent); - const result = await parseFile(buffer, 'nested.json', 'json'); - - expect(result.content).toBe(jsonContent); - }); - - it('should throw error for invalid JSON', async () => { - const buffer = Buffer.from('{ invalid json }'); - await expect(parseFile(buffer, 'bad.json', 'json')).rejects.toThrow( - InputError, - ); - await expect(parseFile(buffer, 'bad.json', 'json')).rejects.toThrow( - /Invalid JSON file/, - ); - }); - - it('should throw error for malformed JSON', async () => { - const buffer = Buffer.from('{"key": "value",}'); // Trailing comma - await expect(parseFile(buffer, 'bad.json', 'json')).rejects.toThrow( - InputError, - ); - }); - - it('should throw error for incomplete JSON', async () => { - const buffer = Buffer.from('{"key": "value"'); // Missing closing brace - await expect(parseFile(buffer, 'bad.json', 'json')).rejects.toThrow( - InputError, - ); - }); - - it('should handle empty JSON object', async () => { - const buffer = Buffer.from('{}'); - const result = await parseFile(buffer, 'empty.json', 'json'); - expect(result.content).toBe('{}'); - }); - - it('should handle empty JSON array', async () => { - const buffer = Buffer.from('[]'); - const result = await parseFile(buffer, 'empty.json', 'json'); - expect(result.content).toBe('[]'); - }); - }); - - describe('parseFile - YAML Files', () => { - it('should parse valid YAML', async () => { - const yamlContent = ` -key: value -number: 42 -array: - - item1 - - item2 -`; - const buffer = Buffer.from(yamlContent); - const result = await parseFile(buffer, 'config.yaml', 'yaml'); - - expect(result.content).toBe(yamlContent); - expect(result.metadata.fileName).toBe('config.yaml'); - expect(result.metadata.fileType).toBe('yaml'); - }); - - it('should parse .yml extension', async () => { - const yamlContent = 'key: value'; - const buffer = Buffer.from(yamlContent); - const result = await parseFile(buffer, 'config.yml', 'yml'); - - expect(result.content).toBe(yamlContent); - expect(result.metadata.fileType).toBe('yml'); - }); - - it('should preserve YAML comments', async () => { - const yamlContent = ` -# This is a comment -key: value # inline comment -`; - const buffer = Buffer.from(yamlContent); - const result = await parseFile(buffer, 'config.yaml', 'yaml'); - - expect(result.content).toBe(yamlContent); - }); - - it('should handle nested YAML structures', async () => { - const yamlContent = ` -parent: - child: - grandchild: value - array: - - item1 - - item2 -`; - const buffer = Buffer.from(yamlContent); - const result = await parseFile(buffer, 'nested.yaml', 'yaml'); - - expect(result.content).toBe(yamlContent); - }); - - it('should throw error for invalid YAML', async () => { - const buffer = Buffer.from('key: [unclosed\n array'); - await expect(parseFile(buffer, 'bad.yaml', 'yaml')).rejects.toThrow( - InputError, - ); - await expect(parseFile(buffer, 'bad.yaml', 'yaml')).rejects.toThrow( - /Invalid YAML file/, - ); - }); - - it('should throw error for malformed YAML', async () => { - const buffer = Buffer.from('key: [unclosed'); - await expect(parseFile(buffer, 'bad.yaml', 'yaml')).rejects.toThrow( - InputError, - ); - }); - - it('should handle empty YAML', async () => { - const buffer = Buffer.from(''); - const result = await parseFile(buffer, 'empty.yaml', 'yaml'); - expect(result.content).toBe(''); - }); - - it('should handle YAML with special characters', async () => { - const yamlContent = ` -message: "Hello, world! 🎉" -special: "Line 1\nLine 2" -`; - const buffer = Buffer.from(yamlContent); - const result = await parseFile(buffer, 'special.yaml', 'yaml'); - - expect(result.content).toBe(yamlContent); - }); - }); - - describe('parseFile - PDF Files', () => { - it('should parse valid PDF with single page', async () => { - const mockPage = { - getTextContent: jest.fn().mockResolvedValue({ - items: [ - { str: 'Hello' }, - { str: 'World' }, - { str: 'PDF' }, - { str: 'Content' }, - ], - }), - }; - - const mockPdf = { - numPages: 1, - getPage: jest.fn().mockResolvedValue(mockPage), - }; - - mockGetDocument.mockReturnValue({ - promise: Promise.resolve(mockPdf as any), - } as any); - - const buffer = Buffer.from('fake pdf data'); - const result = await parseFile(buffer, 'test.pdf', 'pdf'); - - expect(result.content).toContain('--- Page 1 ---'); - expect(result.content).toContain('Hello World PDF Content'); - expect(result.metadata.fileName).toBe('test.pdf'); - expect(result.metadata.fileType).toBe('pdf'); - expect(result.metadata.pageCount).toBe(1); - }); - - it('should parse multi-page PDF', async () => { - const mockPage1 = { - getTextContent: jest.fn().mockResolvedValue({ - items: [{ str: 'Page 1 content' }], - }), - }; - - const mockPage2 = { - getTextContent: jest.fn().mockResolvedValue({ - items: [{ str: 'Page 2 content' }], - }), - }; - - const mockPdf = { - numPages: 2, - getPage: jest - .fn() - .mockResolvedValueOnce(mockPage1) - .mockResolvedValueOnce(mockPage2), - }; - - mockGetDocument.mockReturnValue({ - promise: Promise.resolve(mockPdf as any), - } as any); - - const buffer = Buffer.from('fake pdf data'); - const result = await parseFile(buffer, 'multi.pdf', 'pdf'); - - expect(result.content).toContain('--- Page 1 ---'); - expect(result.content).toContain('Page 1 content'); - expect(result.content).toContain('--- Page 2 ---'); - expect(result.content).toContain('Page 2 content'); - expect(result.metadata.pageCount).toBe(2); - }); - - it('should handle PDF with empty pages', async () => { - const mockPage1 = { - getTextContent: jest.fn().mockResolvedValue({ - items: [{ str: 'Content' }], - }), - }; - - const mockPage2 = { - getTextContent: jest.fn().mockResolvedValue({ - items: [], - }), - }; - - const mockPdf = { - numPages: 2, - getPage: jest - .fn() - .mockResolvedValueOnce(mockPage1) - .mockResolvedValueOnce(mockPage2), - }; - - mockGetDocument.mockReturnValue({ - promise: Promise.resolve(mockPdf as any), - } as any); - - const buffer = Buffer.from('fake pdf data'); - const result = await parseFile(buffer, 'test.pdf', 'pdf'); - - expect(result.content).toContain('--- Page 1 ---'); - expect(result.content).toContain('Content'); - expect(result.content).not.toContain('--- Page 2 ---'); - }); - - it('should filter out empty text items', async () => { - const mockPage = { - getTextContent: jest.fn().mockResolvedValue({ - items: [ - { str: 'Valid' }, - { str: ' ' }, // Whitespace only - { str: '' }, // Empty - { str: 'Text' }, - ], - }), - }; - - const mockPdf = { - numPages: 1, - getPage: jest.fn().mockResolvedValue(mockPage), - }; - - mockGetDocument.mockReturnValue({ - promise: Promise.resolve(mockPdf as any), - } as any); - - const buffer = Buffer.from('fake pdf data'); - const result = await parseFile(buffer, 'test.pdf', 'pdf'); - - expect(result.content).toContain('Valid Text'); - expect( - result.content.split(' ').filter(s => s.trim() === '').length, - ).toBe(0); - }); - - it('should handle items without str property', async () => { - const mockPage = { - getTextContent: jest.fn().mockResolvedValue({ - items: [ - { str: 'Valid' }, - { notStr: 'Invalid' }, // Missing str property - { str: 'Text' }, - ], - }), - }; - - const mockPdf = { - numPages: 1, - getPage: jest.fn().mockResolvedValue(mockPage), - }; - - mockGetDocument.mockReturnValue({ - promise: Promise.resolve(mockPdf as any), - } as any); - - const buffer = Buffer.from('fake pdf data'); - const result = await parseFile(buffer, 'test.pdf', 'pdf'); - - expect(result.content).toContain('Valid Text'); - expect(result.content).not.toContain('Invalid'); - }); - - it('should throw error for corrupted PDF', async () => { - mockGetDocument.mockReturnValue({ - promise: Promise.reject(new Error('Invalid PDF structure')), - } as any); - - const buffer = Buffer.from('corrupted pdf data'); - await expect(parseFile(buffer, 'corrupt.pdf', 'pdf')).rejects.toThrow( - /Error parsing PDF/, - ); - }); - - it('should throw error when PDF loading fails', async () => { - mockGetDocument.mockReturnValue({ - promise: Promise.reject(new Error('Failed to load PDF')), - } as any); - - const buffer = Buffer.from('bad data'); - await expect(parseFile(buffer, 'bad.pdf', 'pdf')).rejects.toThrow( - /Error parsing PDF/, - ); - }); - }); - - describe('parseFile - Unsupported Types', () => { - it('should throw error for unsupported file type', async () => { - const buffer = Buffer.from('content'); - await expect(parseFile(buffer, 'file.exe', 'exe')).rejects.toThrow( - InputError, - ); - await expect(parseFile(buffer, 'file.exe', 'exe')).rejects.toThrow( - /Unsupported file type: exe/, - ); - }); - - it('should throw error for empty file type', async () => { - const buffer = Buffer.from('content'); - await expect(parseFile(buffer, 'file', '')).rejects.toThrow(InputError); - }); - - it('should throw error for unknown extensions', async () => { - const buffer = Buffer.from('content'); - await expect(parseFile(buffer, 'file.xyz', 'xyz')).rejects.toThrow( - InputError, - ); - }); - }); - - describe('parseFile - File Type Normalization', () => { - it('should normalize file type to lowercase', async () => { - const buffer = Buffer.from('Content'); - const result = await parseFile(buffer, 'TEST.TXT', 'TXT'); - - expect(result.metadata.fileType).toBe('TXT'); - }); - - it('should handle file type with leading dot', async () => { - const buffer = Buffer.from('Content'); - const result = await parseFile(buffer, 'test.txt', '.txt'); - - expect(result.content).toBe('Content'); - }); - - it('should treat md, txt, and log the same way', async () => { - const buffer = Buffer.from('Same content'); - - const mdResult = await parseFile(buffer, 'file.md', 'md'); - const txtResult = await parseFile(buffer, 'file.txt', 'txt'); - const logResult = await parseFile(buffer, 'file.log', 'log'); - - expect(mdResult.content).toBe('Same content'); - expect(txtResult.content).toBe('Same content'); - expect(logResult.content).toBe('Same content'); - }); - - it('should treat yaml and yml the same way', async () => { - const buffer = Buffer.from('key: value'); - - const yamlResult = await parseFile(buffer, 'file.yaml', 'yaml'); - const ymlResult = await parseFile(buffer, 'file.yml', 'yml'); - - expect(yamlResult.content).toBe(ymlResult.content); - }); - }); - - describe('parseFile - Metadata', () => { - it('should include parseTimestamp in metadata', async () => { - const buffer = Buffer.from('Content'); - const beforeTime = new Date().toISOString(); - const result = await parseFile(buffer, 'test.txt', 'txt'); - const afterTime = new Date().toISOString(); - - expect(result.metadata.parseTimestamp).toBeDefined(); - expect(result.metadata.parseTimestamp).toMatch( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, - ); - expect(result.metadata.parseTimestamp >= beforeTime).toBe(true); - expect(result.metadata.parseTimestamp <= afterTime).toBe(true); - }); - - it('should include correct fileName in metadata', async () => { - const buffer = Buffer.from('Content'); - const result = await parseFile(buffer, 'my-file.txt', 'txt'); - - expect(result.metadata.fileName).toBe('my-file.txt'); - }); - - it('should include correct fileType in metadata', async () => { - const buffer = Buffer.from('{"key": "value"}'); - const result = await parseFile(buffer, 'test.json', 'json'); - - expect(result.metadata.fileType).toBe('json'); - }); - - it('should include pageCount for PDF files', async () => { - const mockPage = { - getTextContent: jest.fn().mockResolvedValue({ - items: [{ str: 'Content' }], - }), - }; - - const mockPdf = { - numPages: 5, - getPage: jest.fn().mockResolvedValue(mockPage), - }; - - mockGetDocument.mockReturnValue({ - promise: Promise.resolve(mockPdf as any), - } as any); - - const buffer = Buffer.from('pdf data'); - const result = await parseFile(buffer, 'test.pdf', 'pdf'); - - expect(result.metadata.pageCount).toBe(5); - }); - - it('should not include pageCount for non-PDF files', async () => { - const buffer = Buffer.from('Content'); - const result = await parseFile(buffer, 'test.txt', 'txt'); - - expect(result.metadata.pageCount).toBeUndefined(); - }); - }); -}); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/fileParser.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/fileParser.ts deleted file mode 100644 index 0a28405b56a..00000000000 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/fileParser.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { InputError } from '@backstage/errors'; - -import * as yaml from 'js-yaml'; -import * as pdfjsLib from 'pdfjs-dist'; - -import { Readable } from 'stream'; - -import { SupportedFileType } from '../../constant'; - -export interface ParsedDocument { - content: string; - metadata: { - fileName: string; - fileType: string; - pageCount?: number; - parseTimestamp: string; - }; -} - -/** - * Parse text-based files (md, txt, log) - */ -function parseTextFile( - buffer: Buffer, - fileName: string, - fileType: string, -): ParsedDocument { - const content = buffer.toString('utf-8'); - - return { - content, - metadata: { - fileName, - fileType, - parseTimestamp: new Date().toISOString(), - }, - }; -} - -/** - * Parse JSON files - */ -function parseJSONFile( - buffer: Buffer, - fileName: string, - fileType: string, -): ParsedDocument { - try { - const content = buffer.toString('utf-8'); - // Validate JSON but keep original formatting - JSON.parse(content); - - return { - content, - metadata: { - fileName, - fileType, - parseTimestamp: new Date().toISOString(), - }, - }; - } catch (error) { - throw new InputError(`Invalid JSON file: ${error}`); - } -} - -/** - * Parse YAML files - */ -function parseYAMLFile( - buffer: Buffer, - fileName: string, - fileType: string, -): ParsedDocument { - try { - const content = buffer.toString('utf-8'); - // Validate YAML but keep original formatting and comments - yaml.load(content); - - return { - content, - metadata: { - fileName, - fileType, - parseTimestamp: new Date().toISOString(), - }, - }; - } catch (error) { - throw new InputError(`Invalid YAML file: ${error}`); - } -} - -/** - * Parse PDF files - * Extracts text content from PDF using PDF.js - */ -async function parsePDFFile( - buffer: Buffer, - fileName: string, - fileType: string, -): Promise { - try { - const loadingTask = pdfjsLib.getDocument({ - data: new Uint8Array(buffer), - useSystemFonts: true, - }); - - const pdf = await loadingTask.promise; - const textParts: string[] = []; - - // Extract text from each page - for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { - const page = await pdf.getPage(pageNum); - const textContent = await page.getTextContent(); - - const pageText = textContent.items - .map((item: any) => { - if ('str' in item) { - return item.str; - } - return ''; - }) - .filter((str: string) => str.trim().length > 0) - .join(' '); - - if (pageText.trim().length > 0) { - textParts.push(`--- Page ${pageNum} ---\n${pageText}`); - } - } - - const content = textParts.join('\n\n'); - - return { - content, - metadata: { - fileName, - fileType, - pageCount: pdf.numPages, - parseTimestamp: new Date().toISOString(), - }, - }; - } catch (error) { - throw new InputError(`Error parsing PDF: ${error}`); - } -} - -/** - * Parse file based on its type - * @param buffer - File buffer - * @param fileName - File name - * @param fileType - File type (md, txt, pdf, json, yaml, yml, log) - * @returns Parsed document with content and metadata - */ -export async function parseFile( - buffer: Buffer, - fileName: string, - fileType: string, -): Promise { - // Normalize file type: lowercase and remove leading dot - const normalizedType = fileType.toLowerCase().replace(/^\./, ''); - - switch (normalizedType) { - case SupportedFileType.MARKDOWN: - case SupportedFileType.TEXT: - case SupportedFileType.LOG: - return parseTextFile(buffer, fileName, fileType); - - case SupportedFileType.JSON: - return parseJSONFile(buffer, fileName, fileType); - - case SupportedFileType.YAML: - case SupportedFileType.YML: - return parseYAMLFile(buffer, fileName, fileType); - - case SupportedFileType.PDF: - return parsePDFFile(buffer, fileName, fileType); - - default: - throw new InputError(`Unsupported file type: ${fileType}`); - } -} - -/** - * File-like object interface matching what toFile() returns - */ -export interface FileObject { - name: string; - stream: Readable; - buffer: Buffer; - type: string; -} - -/** - * Convert Buffer to File-like object for upload - */ -export async function toFile( - buffer: Buffer, - filename: string, - options?: { type?: string }, -): Promise { - const stream = Readable.from(buffer); - - return { - name: filename, - stream, - buffer, - type: options?.type || 'application/octet-stream', - }; -} diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/markitdownClient.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/markitdownClient.ts new file mode 100644 index 00000000000..04c223a9442 --- /dev/null +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/documents/markitdownClient.ts @@ -0,0 +1,53 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; + +import { MarkItDown } from 'markitdown-ts'; + +const markitdown = new MarkItDown(); + +const PLAINTEXT_EXTENSIONS = new Set(['.json', '.yaml', '.yml', '.log']); + +/** + * Convert a document buffer to markdown using markitdown-ts. + * Plain-text formats (json, yaml, log) are passed through as-is. + */ +export async function convertToMarkdown( + buffer: Buffer, + originalName: string, +): Promise { + const rawExt = originalName.includes('.') + ? originalName.split('.').pop()!.toLowerCase() + : ''; + const ext = rawExt ? `.${rawExt}` : ''; + + if (PLAINTEXT_EXTENSIONS.has(ext)) { + return buffer.toString('utf-8'); + } + + const result = await markitdown.convertBuffer(buffer, { + file_extension: ext, + }); + + if (!result?.markdown) { + throw new InputError( + `Markdown conversion produced no output for ${originalName}`, + ); + } + + return result.markdown; +} diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/notebooksRouters.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/notebooksRouters.ts index 4d0f5864a66..695e51505de 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/notebooksRouters.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/src/service/notebooks/notebooksRouters.ts @@ -45,8 +45,9 @@ import { getIdentity, } from '../middleware/getIdentity'; import { userPermissionAuthorization } from '../permission'; -import { isValidFileType, parseFileContent } from './documents/documentHelpers'; +import { isValidFileType } from './documents/documentHelpers'; import { DocumentService } from './documents/documentService'; +import { convertToMarkdown } from './documents/markitdownClient'; import { SessionService } from './sessions/sessionService'; import { createDocumentListResponse, @@ -398,11 +399,17 @@ export async function createNotebooksRouter( handleError(logger, res, 'Session not found'); return; } - const parsedDocument = await parseFileContent(logger, fileType, req.file); - const fileId = await documentService.uploadFile( - parsedDocument.content, - title, + + if (!req.file) { + handleError(logger, res, 'No file uploaded'); + return; + } + + const markdown = await convertToMarkdown( + req.file.buffer, + req.file.originalname, ); + const fileId = await documentService.uploadFile(markdown, title); res.status(HTTP_STATUS_ACCEPTED).json({ status: 'processing', @@ -411,7 +418,7 @@ export async function createNotebooksRouter( message: 'Document upload started', }); - // Upload document to vector store in background + // Attach file to vector store in background const docName = newTitle || title; documentService .upsertDocument(sessionId, title, fileType, fileId, newTitle) diff --git a/workspaces/intelligent-assistant/yarn.lock b/workspaces/intelligent-assistant/yarn.lock index 9cc453eef83..6ae1c3f29c2 100644 --- a/workspaces/intelligent-assistant/yarn.lock +++ b/workspaces/intelligent-assistant/yarn.lock @@ -62,6 +62,41 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/gateway@npm:3.0.158": + version: 3.0.158 + resolution: "@ai-sdk/gateway@npm:3.0.158" + dependencies: + "@ai-sdk/provider": "npm:3.0.14" + "@ai-sdk/provider-utils": "npm:4.0.40" + "@vercel/oidc": "npm:3.2.0" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/833ead720f821e2ef28843895eeb46d2849b8f4d6eeb38201001addf9ba5cf379b3d1924e393f909763e233095a2ff7ce84f36b841d75f9a85008b90fe78ecdc + languageName: node + linkType: hard + +"@ai-sdk/provider-utils@npm:4.0.40": + version: 4.0.40 + resolution: "@ai-sdk/provider-utils@npm:4.0.40" + dependencies: + "@ai-sdk/provider": "npm:3.0.14" + "@standard-schema/spec": "npm:^1.1.0" + eventsource-parser: "npm:^3.0.8" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/fcf8863864866ac7131e191f1e6cf42a064e697eaa4d0a28c4256ff7796a2ed58e8783ac2d572d5612c5761f5b6b437f67390b049771d9be87c0d642b7cd31f3 + languageName: node + linkType: hard + +"@ai-sdk/provider@npm:3.0.14": + version: 3.0.14 + resolution: "@ai-sdk/provider@npm:3.0.14" + dependencies: + json-schema: "npm:^0.4.0" + checksum: 10c0/4a533f0c7e98c3f7a3b75119b3f1855fd1ae759d696f7ee9c07b9bd4feaed57716f3e0d81586333e621a501dc0b99d3a103def3e063a45970a653ab027a8f163 + languageName: node + linkType: hard + "@apidevtools/json-schema-ref-parser@npm:9.0.6": version: 9.0.6 resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6" @@ -6354,6 +6389,13 @@ __metadata: languageName: node linkType: hard +"@joplin/turndown-plugin-gfm@npm:^1.0.60": + version: 1.0.67 + resolution: "@joplin/turndown-plugin-gfm@npm:1.0.67" + checksum: 10c0/aa2d566df3d493029efe0c257d6aa8acf33e488307582e228dbbe070053e395fbd225b79f5f41d51c98ce083f25cc30c4115d6ec2cd85b0d2d55b73e8f1c442a + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" @@ -7020,6 +7062,13 @@ __metadata: languageName: node linkType: hard +"@mixmark-io/domino@npm:^2.2.0": + version: 2.2.0 + resolution: "@mixmark-io/domino@npm:2.2.0" + checksum: 10c0/aa468a15f9217d425220fe6a4b3f9416cbe8e566ee14efc191c6d5cc04fe39338b16a90bbac190f28d44e69465db5f2cf95f479c621ce38060ca6b2a3d346e9d + languageName: node + linkType: hard + "@module-federation/bridge-react-webpack-plugin@npm:2.7.0": version: 2.7.0 resolution: "@module-federation/bridge-react-webpack-plugin@npm:2.7.0" @@ -7588,98 +7637,207 @@ __metadata: languageName: node linkType: hard -"@napi-rs/canvas-android-arm64@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-android-arm64@npm:0.1.97" +"@napi-rs/canvas-android-arm64@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-android-arm64@npm:0.1.100" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@napi-rs/canvas-darwin-arm64@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-darwin-arm64@npm:0.1.97" +"@napi-rs/canvas-android-arm64@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-android-arm64@npm:0.1.80" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/canvas-darwin-arm64@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-darwin-arm64@npm:0.1.100" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@napi-rs/canvas-darwin-x64@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-darwin-x64@npm:0.1.97" +"@napi-rs/canvas-darwin-arm64@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-darwin-arm64@npm:0.1.80" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@napi-rs/canvas-darwin-x64@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-darwin-x64@npm:0.1.100" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@napi-rs/canvas-linux-arm-gnueabihf@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-linux-arm-gnueabihf@npm:0.1.97" +"@napi-rs/canvas-darwin-x64@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-darwin-x64@npm:0.1.80" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm-gnueabihf@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-linux-arm-gnueabihf@npm:0.1.100" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm-gnueabihf@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-linux-arm-gnueabihf@npm:0.1.80" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@napi-rs/canvas-linux-arm64-gnu@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-linux-arm64-gnu@npm:0.1.97" +"@napi-rs/canvas-linux-arm64-gnu@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-linux-arm64-gnu@npm:0.1.100" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-arm64-gnu@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-linux-arm64-gnu@npm:0.1.80" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@napi-rs/canvas-linux-arm64-musl@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-linux-arm64-musl@npm:0.1.97" +"@napi-rs/canvas-linux-arm64-musl@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-linux-arm64-musl@npm:0.1.100" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@napi-rs/canvas-linux-riscv64-gnu@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-linux-riscv64-gnu@npm:0.1.97" +"@napi-rs/canvas-linux-arm64-musl@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-linux-arm64-musl@npm:0.1.80" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-riscv64-gnu@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-linux-riscv64-gnu@npm:0.1.100" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@napi-rs/canvas-linux-x64-gnu@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-linux-x64-gnu@npm:0.1.97" +"@napi-rs/canvas-linux-riscv64-gnu@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-linux-riscv64-gnu@npm:0.1.80" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-x64-gnu@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-linux-x64-gnu@npm:0.1.100" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@napi-rs/canvas-linux-x64-gnu@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-linux-x64-gnu@npm:0.1.80" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@napi-rs/canvas-linux-x64-musl@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-linux-x64-musl@npm:0.1.97" +"@napi-rs/canvas-linux-x64-musl@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-linux-x64-musl@npm:0.1.100" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@napi-rs/canvas-win32-arm64-msvc@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-win32-arm64-msvc@npm:0.1.97" +"@napi-rs/canvas-linux-x64-musl@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-linux-x64-musl@npm:0.1.80" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@napi-rs/canvas-win32-arm64-msvc@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-win32-arm64-msvc@npm:0.1.100" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@napi-rs/canvas-win32-x64-msvc@npm:0.1.97": - version: 0.1.97 - resolution: "@napi-rs/canvas-win32-x64-msvc@npm:0.1.97" +"@napi-rs/canvas-win32-x64-msvc@npm:0.1.100": + version: 0.1.100 + resolution: "@napi-rs/canvas-win32-x64-msvc@npm:0.1.100" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@napi-rs/canvas-win32-x64-msvc@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas-win32-x64-msvc@npm:0.1.80" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@napi-rs/canvas@npm:^0.1.65": - version: 0.1.97 - resolution: "@napi-rs/canvas@npm:0.1.97" +"@napi-rs/canvas@npm:0.1.80": + version: 0.1.80 + resolution: "@napi-rs/canvas@npm:0.1.80" + dependencies: + "@napi-rs/canvas-android-arm64": "npm:0.1.80" + "@napi-rs/canvas-darwin-arm64": "npm:0.1.80" + "@napi-rs/canvas-darwin-x64": "npm:0.1.80" + "@napi-rs/canvas-linux-arm-gnueabihf": "npm:0.1.80" + "@napi-rs/canvas-linux-arm64-gnu": "npm:0.1.80" + "@napi-rs/canvas-linux-arm64-musl": "npm:0.1.80" + "@napi-rs/canvas-linux-riscv64-gnu": "npm:0.1.80" + "@napi-rs/canvas-linux-x64-gnu": "npm:0.1.80" + "@napi-rs/canvas-linux-x64-musl": "npm:0.1.80" + "@napi-rs/canvas-win32-x64-msvc": "npm:0.1.80" + dependenciesMeta: + "@napi-rs/canvas-android-arm64": + optional: true + "@napi-rs/canvas-darwin-arm64": + optional: true + "@napi-rs/canvas-darwin-x64": + optional: true + "@napi-rs/canvas-linux-arm-gnueabihf": + optional: true + "@napi-rs/canvas-linux-arm64-gnu": + optional: true + "@napi-rs/canvas-linux-arm64-musl": + optional: true + "@napi-rs/canvas-linux-riscv64-gnu": + optional: true + "@napi-rs/canvas-linux-x64-gnu": + optional: true + "@napi-rs/canvas-linux-x64-musl": + optional: true + "@napi-rs/canvas-win32-x64-msvc": + optional: true + checksum: 10c0/41c60ee5cb96571561a6d72a5cebc2bebbc775096ccb0fb7cc06e0665f327d9396dc30740f1466bd91a10147fc5783979570a4dde7530be9e05d88d6fa6a73ae + languageName: node + linkType: hard + +"@napi-rs/canvas@npm:^0.1.80": + version: 0.1.100 + resolution: "@napi-rs/canvas@npm:0.1.100" dependencies: - "@napi-rs/canvas-android-arm64": "npm:0.1.97" - "@napi-rs/canvas-darwin-arm64": "npm:0.1.97" - "@napi-rs/canvas-darwin-x64": "npm:0.1.97" - "@napi-rs/canvas-linux-arm-gnueabihf": "npm:0.1.97" - "@napi-rs/canvas-linux-arm64-gnu": "npm:0.1.97" - "@napi-rs/canvas-linux-arm64-musl": "npm:0.1.97" - "@napi-rs/canvas-linux-riscv64-gnu": "npm:0.1.97" - "@napi-rs/canvas-linux-x64-gnu": "npm:0.1.97" - "@napi-rs/canvas-linux-x64-musl": "npm:0.1.97" - "@napi-rs/canvas-win32-arm64-msvc": "npm:0.1.97" - "@napi-rs/canvas-win32-x64-msvc": "npm:0.1.97" + "@napi-rs/canvas-android-arm64": "npm:0.1.100" + "@napi-rs/canvas-darwin-arm64": "npm:0.1.100" + "@napi-rs/canvas-darwin-x64": "npm:0.1.100" + "@napi-rs/canvas-linux-arm-gnueabihf": "npm:0.1.100" + "@napi-rs/canvas-linux-arm64-gnu": "npm:0.1.100" + "@napi-rs/canvas-linux-arm64-musl": "npm:0.1.100" + "@napi-rs/canvas-linux-riscv64-gnu": "npm:0.1.100" + "@napi-rs/canvas-linux-x64-gnu": "npm:0.1.100" + "@napi-rs/canvas-linux-x64-musl": "npm:0.1.100" + "@napi-rs/canvas-win32-arm64-msvc": "npm:0.1.100" + "@napi-rs/canvas-win32-x64-msvc": "npm:0.1.100" dependenciesMeta: "@napi-rs/canvas-android-arm64": optional: true @@ -7703,7 +7861,11 @@ __metadata: optional: true "@napi-rs/canvas-win32-x64-msvc": optional: true - checksum: 10c0/e201c7c547a1d882becd41943c0e0a09f7cd9a1cd1bce0d2b1442d2a1032b44585b1687a2d44068974a35dbcae9a62f1ae1057d460fe8e9485c3b1b1ea19e891 + canvas: + built: true + skia-canvas: + built: true + checksum: 10c0/d3dfca5620a41c34addd344bd4c448a5334d3f32f2562ec8390507b3897747ed1de9f08fcb9169cff6dd27111afb5b3eaf7e42cc1494c8fa4128e7d4235f6bea languageName: node linkType: hard @@ -9318,19 +9480,16 @@ __metadata: "@red-hat-developer-hub/backstage-plugin-intelligent-assistant-common": "workspace:^" "@spotify/prettier-config": "npm:^15.0.0" "@types/express": "npm:4.17.25" - "@types/js-yaml": "npm:^4" "@types/multer": "npm:^1.4.12" "@types/supertest": "npm:2.0.16" express: "npm:^4.21.1" express-rate-limit: "npm:^8.2.2" form-data: "npm:^4.0.6" - htmlparser2: "npm:^9.1.0" http-proxy-middleware: "npm:^3.0.7" - js-yaml: "npm:^4.1.1" knex: "npm:^3.1.0" + markitdown-ts: "npm:^0.0.10" msw: "npm:2.14.6" multer: "npm:^2.2.0" - pdfjs-dist: "npm:^4.10.38" prettier: "npm:3.9.5" supertest: "npm:6.3.4" languageName: unknown @@ -12718,7 +12877,7 @@ __metadata: languageName: node linkType: hard -"@types/js-yaml@npm:^4, @types/js-yaml@npm:^4.0.1": +"@types/js-yaml@npm:^4.0.1": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211 @@ -13711,6 +13870,13 @@ __metadata: languageName: node linkType: hard +"@vercel/oidc@npm:3.2.0": + version: 3.2.0 + resolution: "@vercel/oidc@npm:3.2.0" + checksum: 10c0/98318d3236f58c296616c8c2e1655b268c7bf58525bcd985adac7af6d900e05fc610f6f03ce2ff4bdcd3df7885a40c0ca44fdc761f122dcfe15a78c2756b0243 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1": version: 1.14.1 resolution: "@webassemblyjs/ast@npm:1.14.1" @@ -13869,6 +14035,20 @@ __metadata: languageName: node linkType: hard +"@xmldom/xmldom@npm:^0.8.6": + version: 0.8.13 + resolution: "@xmldom/xmldom@npm:0.8.13" + checksum: 10c0/06405ee6fffba631abf715a305ace338420ebcea8baf1317f19f2752f5c505952b7df45159908e7be8451a42faa54326b780616ab4d08242b20477b2973da24b + languageName: node + linkType: hard + +"@xmldom/xmldom@npm:^0.9.6": + version: 0.9.10 + resolution: "@xmldom/xmldom@npm:0.9.10" + checksum: 10c0/38a9c9b95450d7fccebc61371c8e0b90ceaae992886484108333d29ccbd2640e3555b6af012f15ce1beb5067aeb40486659b6183fad38af179e82d28028bfc5d + languageName: node + linkType: hard + "@xobotyi/scrollbar-width@npm:^1.9.5": version: 1.9.5 resolution: "@xobotyi/scrollbar-width@npm:1.9.5" @@ -14011,6 +14191,13 @@ __metadata: languageName: node linkType: hard +"adler-32@npm:~1.3.0": + version: 1.3.1 + resolution: "adler-32@npm:1.3.1" + checksum: 10c0/c1b7185526ee1bbe0eac8ed414d5226af4cd02a0540449a72ec1a75f198c5e93352ba4d7b9327231eea31fd83c2d080d13baf16d8ed5710fb183677beb85f612 + languageName: node + linkType: hard + "adm-zip@npm:0.5.10": version: 0.5.10 resolution: "adm-zip@npm:0.5.10" @@ -14053,6 +14240,20 @@ __metadata: languageName: node linkType: hard +"ai@npm:^6.0.6": + version: 6.0.236 + resolution: "ai@npm:6.0.236" + dependencies: + "@ai-sdk/gateway": "npm:3.0.158" + "@ai-sdk/provider": "npm:3.0.14" + "@ai-sdk/provider-utils": "npm:4.0.40" + "@opentelemetry/api": "npm:^1.9.0" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/cf311c9b1417ab18dc250812e94c924554dbf5b81af8dd486c5f0dabbe02706ade737eedee57139927db4152877ed4c630649ab8e3c0a1679d4589f5b66f8b7c + languageName: node + linkType: hard + "ajv-draft-04@npm:^1.0.0, ajv-draft-04@npm:~1.0.0": version: 1.0.0 resolution: "ajv-draft-04@npm:1.0.0" @@ -14477,7 +14678,7 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.10, argparse@npm:^1.0.7, argparse@npm:~1.0.9": +"argparse@npm:^1.0.10, argparse@npm:^1.0.7, argparse@npm:~1.0.3, argparse@npm:~1.0.9": version: 1.0.10 resolution: "argparse@npm:1.0.10" dependencies: @@ -15338,6 +15539,13 @@ __metadata: languageName: node linkType: hard +"bluebird@npm:~3.4.0": + version: 3.4.7 + resolution: "bluebird@npm:3.4.7" + checksum: 10c0/ac7e3df09a433b985a0ba61a0be4fc23e3874bf62440ffbca2f275a8498b00c11336f1f633631f38419b2c842515473985f9c4aaa9e4c9b36105535026d94144 + languageName: node + linkType: hard + "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.8, bn.js@npm:^4.11.9": version: 4.12.2 resolution: "bn.js@npm:4.12.2" @@ -15865,6 +16073,16 @@ __metadata: languageName: node linkType: hard +"cfb@npm:~1.2.1": + version: 1.2.2 + resolution: "cfb@npm:1.2.2" + dependencies: + adler-32: "npm:~1.3.0" + crc-32: "npm:~1.2.0" + checksum: 10c0/87f6d9c3878268896ed6ca29dfe32a2aa078b12d0f21d8405c95911b74ab6296823d7312bbf5e18326d00b16cc697f587e07a17018c5edf7a1ba31dd5bc6da36 + languageName: node + linkType: hard + "chalk@npm:2.4.2, chalk@npm:^2.3.2, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -16294,6 +16512,13 @@ __metadata: languageName: node linkType: hard +"codepage@npm:~1.15.0": + version: 1.15.0 + resolution: "codepage@npm:1.15.0" + checksum: 10c0/2455b482302cb784b46dea60a8ee83f0c23e794bdd979556bdb107abe681bba722af62a37f5c955ff4efd68fdb9688c3986e719b4fd536c0e06bb25bc82abea3 + languageName: node + linkType: hard + "collect-v8-coverage@npm:^1.0.2": version: 1.0.3 resolution: "collect-v8-coverage@npm:1.0.3" @@ -16869,7 +17094,7 @@ __metadata: languageName: node linkType: hard -"crc-32@npm:^1.2.0": +"crc-32@npm:^1.2.0, crc-32@npm:~1.2.0, crc-32@npm:~1.2.1": version: 1.2.2 resolution: "crc-32@npm:1.2.2" bin: @@ -17915,6 +18140,13 @@ __metadata: languageName: node linkType: hard +"dingbat-to-unicode@npm:^1.0.1": + version: 1.0.1 + resolution: "dingbat-to-unicode@npm:1.0.1" + checksum: 10c0/4def812dadd17122929ad31df574539e4066d85d07b0f824fc70533ff44ce75733e25cdd5817b80604d0c6e6091159a51f9b67487b926611518718c796354e26 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -18038,17 +18270,6 @@ __metadata: languageName: node linkType: hard -"dom-serializer@npm:^2.0.0": - version: 2.0.0 - resolution: "dom-serializer@npm:2.0.0" - dependencies: - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.2" - entities: "npm:^4.2.0" - checksum: 10c0/d5ae2b7110ca3746b3643d3ef60ef823f5f078667baf530cec096433f1627ec4b6fa8c072f09d079d7cda915fd2c7bc1b7b935681e9b09e591e1e15f4040b8e2 - languageName: node - linkType: hard - "domain-browser@npm:4.22.0": version: 4.22.0 resolution: "domain-browser@npm:4.22.0" @@ -18056,7 +18277,7 @@ __metadata: languageName: node linkType: hard -"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0": +"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0": version: 2.3.0 resolution: "domelementtype@npm:2.3.0" checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9 @@ -18072,15 +18293,6 @@ __metadata: languageName: node linkType: hard -"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": - version: 5.0.3 - resolution: "domhandler@npm:5.0.3" - dependencies: - domelementtype: "npm:^2.3.0" - checksum: 10c0/bba1e5932b3e196ad6862286d76adc89a0dbf0c773e5ced1eb01f9af930c50093a084eff14b8de5ea60b895c56a04d5de8bbc4930c5543d029091916770b2d2a - languageName: node - linkType: hard - "dompurify@npm:3.2.7": version: 3.2.7 resolution: "dompurify@npm:3.2.7" @@ -18128,17 +18340,6 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^3.1.0": - version: 3.2.2 - resolution: "domutils@npm:3.2.2" - dependencies: - dom-serializer: "npm:^2.0.0" - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.3" - checksum: 10c0/47938f473b987ea71cd59e59626eb8666d3aa8feba5266e45527f3b636c7883cca7e582d901531961f742c519d7514636b7973353b648762b2e3bedbf235fada - languageName: node - linkType: hard - "dot-case@npm:^3.0.4": version: 3.0.4 resolution: "dot-case@npm:3.0.4" @@ -18170,6 +18371,15 @@ __metadata: languageName: node linkType: hard +"duck@npm:^0.1.12": + version: 0.1.12 + resolution: "duck@npm:0.1.12" + dependencies: + underscore: "npm:^1.13.1" + checksum: 10c0/dfbe163481cae832c783016c5026f228e95e32bd5dfc9636607d981faf5b1e7aaa5ac27cf181ceefba6a01d42b54239908dee41ae3af2e7cea42e4fa925dbff3 + languageName: node + linkType: hard + "dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" @@ -18399,7 +18609,7 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.2.0, entities@npm:^4.5.0": +"entities@npm:^4.5.0": version: 4.5.0 resolution: "entities@npm:4.5.0" checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 @@ -19242,6 +19452,13 @@ __metadata: languageName: node linkType: hard +"eventsource-parser@npm:^3.0.8": + version: 3.1.0 + resolution: "eventsource-parser@npm:3.1.0" + checksum: 10c0/5ab4c6c9a2a042be0b387b6d03810eb580bac4ce90e299ede56458125a97ffe3af8145b2740089fc898a96cfa5aae792ee79f2a06257fba2776b0e7bce037071 + languageName: node + linkType: hard + "evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3": version: 1.0.3 resolution: "evp_bytestokey@npm:1.0.3" @@ -20018,6 +20235,13 @@ __metadata: languageName: node linkType: hard +"frac@npm:~1.1.2": + version: 1.1.2 + resolution: "frac@npm:1.1.2" + checksum: 10c0/640740eb58b590eb38c78c676955bee91cd22d854f5876241a15c49d4495fa53a84898779dcf7eca30aabfe1c1a4a705752b5f224934257c5dda55c545413ba7 + languageName: node + linkType: hard + "framer-motion@npm:^12.38.0": version: 12.38.0 resolution: "framer-motion@npm:12.38.0" @@ -21348,18 +21572,6 @@ __metadata: languageName: node linkType: hard -"htmlparser2@npm:^9.1.0": - version: 9.1.0 - resolution: "htmlparser2@npm:9.1.0" - dependencies: - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.3" - domutils: "npm:^3.1.0" - entities: "npm:^4.5.0" - checksum: 10c0/394f6323efc265bbc791d8c0d96bfe95984e0407565248521ab92e2dc7668e5ceeca7bc6ed18d408b9ee3b25032c5743368a4280d280332d782821d5d467ad8f - languageName: node - linkType: hard - "http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -23289,7 +23501,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": +"js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": version: 4.3.0 resolution: "js-yaml@npm:4.3.0" dependencies: @@ -23488,6 +23700,13 @@ __metadata: languageName: node linkType: hard +"json-schema@npm:^0.4.0": + version: 0.4.0 + resolution: "json-schema@npm:0.4.0" + checksum: 10c0/d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3 + languageName: node + linkType: hard + "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -23738,7 +23957,7 @@ __metadata: languageName: node linkType: hard -"jszip@npm:^3.10.1": +"jszip@npm:^3.10.1, jszip@npm:^3.7.1": version: 3.10.1 resolution: "jszip@npm:3.10.1" dependencies: @@ -24396,6 +24615,17 @@ __metadata: languageName: node linkType: hard +"lop@npm:^0.4.2": + version: 0.4.2 + resolution: "lop@npm:0.4.2" + dependencies: + duck: "npm:^0.1.12" + option: "npm:~0.2.1" + underscore: "npm:^1.13.1" + checksum: 10c0/f0d72f8a80fc12e5c1ac7a480dd5aa8820d8a390e1d5ea0a3a40f9a830885b149ee9c61384e40f1e5f23bc50469f87de8b78dd08549bbb4787409cf58373e04d + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -24597,6 +24827,26 @@ __metadata: languageName: node linkType: hard +"mammoth@npm:^1.8.0": + version: 1.12.0 + resolution: "mammoth@npm:1.12.0" + dependencies: + "@xmldom/xmldom": "npm:^0.8.6" + argparse: "npm:~1.0.3" + base64-js: "npm:^1.5.1" + bluebird: "npm:~3.4.0" + dingbat-to-unicode: "npm:^1.0.1" + jszip: "npm:^3.7.1" + lop: "npm:^0.4.2" + path-is-absolute: "npm:^1.0.0" + underscore: "npm:^1.13.1" + xmlbuilder: "npm:^10.0.0" + bin: + mammoth: bin/mammoth + checksum: 10c0/166af9c2b277ac3c509081c03db164e0eb7942d432e9c14299c1e7b4d73c57337f2fb4c4a1dd193ba093954a40ef6fbaa30f6ead95613963e385a19af454f1f6 + languageName: node + linkType: hard + "markdown-escape@npm:^2.0.0": version: 2.0.0 resolution: "markdown-escape@npm:2.0.0" @@ -24680,6 +24930,31 @@ __metadata: languageName: node linkType: hard +"markitdown-ts@npm:^0.0.10": + version: 0.0.10 + resolution: "markitdown-ts@npm:0.0.10" + dependencies: + "@joplin/turndown-plugin-gfm": "npm:^1.0.60" + "@xmldom/xmldom": "npm:^0.9.6" + ai: "npm:^6.0.6" + jsdom: "npm:^25.0.1" + mammoth: "npm:^1.8.0" + mime-types: "npm:^2.1.35" + pdf-parse: "npm:^2.4.5" + turndown: "npm:^7.2.0" + xlsx: "npm:^0.18.5" + peerDependencies: + unzipper: ^0.12.3 + youtube-transcript: ^1.2.1 + peerDependenciesMeta: + unzipper: + optional: true + youtube-transcript: + optional: true + checksum: 10c0/6fc9de87b8d0e9dda4ea00bc503d46dc186779ab2874e15310ed6b0bf1200da90ec86b338d1e325f4da9be3fbf386590e023d77931efe1361ce111d0fc48fb1e + languageName: node + linkType: hard + "matcher@npm:^3.0.0": version: 3.0.0 resolution: "matcher@npm:3.0.0" @@ -27346,6 +27621,13 @@ __metadata: languageName: node linkType: hard +"option@npm:~0.2.1": + version: 0.2.4 + resolution: "option@npm:0.2.4" + checksum: 10c0/b605e5f3f65b21e0a9ec49a4ead50acb953696678109bb0decd80cc4cc4b466691c14472b2e281866cef513fc63f0310a09677c2d4cedd1e0d9607be1ce25831 + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -27946,15 +28228,27 @@ __metadata: languageName: node linkType: hard -"pdfjs-dist@npm:^4.10.38": - version: 4.10.38 - resolution: "pdfjs-dist@npm:4.10.38" +"pdf-parse@npm:^2.4.5": + version: 2.4.5 + resolution: "pdf-parse@npm:2.4.5" + dependencies: + "@napi-rs/canvas": "npm:0.1.80" + pdfjs-dist: "npm:5.4.296" + bin: + pdf-parse: bin/cli.mjs + checksum: 10c0/0bf737c8405994a232e5ce11c6bd80016942cad291debcfe40afa9365e864de9f3f704ef2931e66a84512e28e8c3a9cff6537cc84d2981a39a6f0447a4389036 + languageName: node + linkType: hard + +"pdfjs-dist@npm:5.4.296": + version: 5.4.296 + resolution: "pdfjs-dist@npm:5.4.296" dependencies: - "@napi-rs/canvas": "npm:^0.1.65" + "@napi-rs/canvas": "npm:^0.1.80" dependenciesMeta: "@napi-rs/canvas": optional: true - checksum: 10c0/77b022109be7aac00372750a53decea3979409e6ef1cf93bf554351569cd4d1fafc70afae4a9a3e4b4de3facf59d3acd54d324b0fcff781374bcb00493d449ce + checksum: 10c0/a9e438f12771963f7c29934b4d5e58d508fa43954e4e4cbf951b040c6c89891d048e3d9d3799ed9ec6ceade677628a25f88062b300c5425777ce0b522e457532 languageName: node linkType: hard @@ -31874,6 +32168,15 @@ __metadata: languageName: node linkType: hard +"ssf@npm:~0.11.2": + version: 0.11.2 + resolution: "ssf@npm:0.11.2" + dependencies: + frac: "npm:~1.1.2" + checksum: 10c0/c3fd24a90dc37a9dc5c4154cb4121e27507c33ebfeee3532aaf03625756b2c006cf79c0a23db0ba16c4a6e88e1349455327867e03453fc9d54b32c546bc18ca6 + languageName: node + linkType: hard + "ssh-remote-port-forward@npm:^1.0.4": version: 1.0.4 resolution: "ssh-remote-port-forward@npm:1.0.4" @@ -33470,6 +33773,15 @@ __metadata: languageName: node linkType: hard +"turndown@npm:^7.2.0": + version: 7.2.4 + resolution: "turndown@npm:7.2.4" + dependencies: + "@mixmark-io/domino": "npm:^2.2.0" + checksum: 10c0/8e66e24bc9d9cdcf720f0ee2655a61d9b9e60ebe2763e97a7a8c3f689e9154717bbea7ed48cc5e355651f34182da698c6bc5fef3028e5600c1cadb7e117376d5 + languageName: node + linkType: hard + "tweetnacl@npm:^0.14.3": version: 0.14.5 resolution: "tweetnacl@npm:0.14.5" @@ -33850,7 +34162,7 @@ __metadata: languageName: node linkType: hard -"underscore@npm:^1.13.3": +"underscore@npm:^1.13.1, underscore@npm:^1.13.3": version: 1.13.8 resolution: "underscore@npm:1.13.8" checksum: 10c0/6677688daeda30484823e77c0b89ce4dcf29964a77d5a06f37299c007ab4bb1c66a0ff75e0d274620b62a1fe2a6ba29879f8214533ca611d71a1ae504f2bfc9b @@ -35098,6 +35410,13 @@ __metadata: languageName: node linkType: hard +"wmf@npm:~1.0.1": + version: 1.0.2 + resolution: "wmf@npm:1.0.2" + checksum: 10c0/3fa5806f382632cadfe65d4ef24f7a583b0c0720171edb00e645af5248ad0bb6784e8fcee1ccd9f475a1a12a7523e2512e9c063731fbbdae14dc469e1c033d93 + languageName: node + linkType: hard + "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -35105,6 +35424,13 @@ __metadata: languageName: node linkType: hard +"word@npm:~0.3.0": + version: 0.3.0 + resolution: "word@npm:0.3.0" + checksum: 10c0/c6da2a9f7a0d81a32fa6768a638d21b153da2be04f94f3964889c7cc1365d74b6ecb43b42256c3f926cd59512d8258206991c78c21000c3da96d42ff1238b840 + languageName: node + linkType: hard + "wordwrap@npm:^1.0.0": version: 1.0.0 resolution: "wordwrap@npm:1.0.0" @@ -35186,6 +35512,23 @@ __metadata: languageName: node linkType: hard +"xlsx@npm:^0.18.5": + version: 0.18.5 + resolution: "xlsx@npm:0.18.5" + dependencies: + adler-32: "npm:~1.3.0" + cfb: "npm:~1.2.1" + codepage: "npm:~1.15.0" + crc-32: "npm:~1.2.1" + ssf: "npm:~0.11.2" + wmf: "npm:~1.0.1" + word: "npm:~0.3.0" + bin: + xlsx: bin/xlsx.njs + checksum: 10c0/787cfa77034a3e86fdcde21572f1011c8976f87823a5e0ee5057f13b2f6e48f17a1710732a91b8ae15d7794945c7cba8a3ca904ea7150e028260b0ab8e1158c8 + languageName: node + linkType: hard + "xml-but-prettier@npm:^1.0.1": version: 1.0.1 resolution: "xml-but-prettier@npm:1.0.1" @@ -35216,6 +35559,13 @@ __metadata: languageName: node linkType: hard +"xmlbuilder@npm:^10.0.0": + version: 10.1.1 + resolution: "xmlbuilder@npm:10.1.1" + checksum: 10c0/26c465e8bd16b4e882d39c2e2a29bb277434d254717aa05df117dd0009041d92855426714b2d1a6a5f76983640349f4edb80073b6ae374e0e6c3d13029ea8237 + languageName: node + linkType: hard + "xmlchars@npm:^2.2.0": version: 2.2.0 resolution: "xmlchars@npm:2.2.0"