@red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend 3.2.0 → 4.1.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 +85 -0
- package/README.md +24 -22
- package/dist/plugin.cjs.js +0 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/service/notebooks/documents/documentHelpers.cjs.js +1 -21
- package/dist/service/notebooks/documents/documentHelpers.cjs.js.map +1 -1
- package/dist/service/notebooks/documents/documentService.cjs.js +85 -37
- package/dist/service/notebooks/documents/documentService.cjs.js.map +1 -1
- package/dist/service/notebooks/documents/markitdownClient.cjs.js +33 -0
- package/dist/service/notebooks/documents/markitdownClient.cjs.js.map +1 -0
- package/dist/service/notebooks/notebooksRouters.cjs.js +58 -22
- package/dist/service/notebooks/notebooksRouters.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +27 -32
- package/dist/service/router.cjs.js.map +1 -1
- package/package.json +4 -12
- package/dist/service/notebooks/documents/fileParser.cjs.js +0 -139
- package/dist/service/notebooks/documents/fileParser.cjs.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend
|
|
2
2
|
|
|
3
|
+
## 4.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6c8f76d: Removed public `iaSavedPromptsManagePermission` from the common package. Saved-prompts backend routes now require `intelligent-assistant.chat.use`. Operators should drop `intelligent-assistant.saved-prompts.manage` from RBAC CSVs; `chat.use` is enough.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- c0d97e4: Removed unused `@langchain/core` and `@langchain/openai` dependencies left over after the backend switched from an in-process LangChain client to the Lightspeed Core HTTP proxy.
|
|
12
|
+
- e5deac5: AI Notebooks will use markitdown to clean up data before vectorizing documents
|
|
13
|
+
- Updated dependencies [6c8f76d]
|
|
14
|
+
- @red-hat-developer-hub/backstage-plugin-intelligent-assistant-common@4.1.0
|
|
15
|
+
|
|
16
|
+
## 4.0.0
|
|
17
|
+
|
|
18
|
+
### Major Changes
|
|
19
|
+
|
|
20
|
+
- adb65c9: Breaking changes to MCP permissions using behavior-linked vocabulary rather than CRUD-linked vocabulary:
|
|
21
|
+
|
|
22
|
+
| Before (Lightspeed) | Before (Intelligent Assistant) | After |
|
|
23
|
+
| ----------------------- | ---------------------------------- | ------------------ |
|
|
24
|
+
| `lightspeed.mcp.read` | `intelligent-assistant.mcp.read` | `mcp.tools.use` |
|
|
25
|
+
| `lightspeed.mcp.manage` | `intelligent-assistant.mcp.manage` | `mcp.tools.manage` |
|
|
26
|
+
|
|
27
|
+
Removed permission CRUD action attributes; RBAC entries for these permission sets now use the generic `use` action.
|
|
28
|
+
|
|
29
|
+
Permission variable renamed from `iaMcpReadPermission` to `iaMcpUsePermission`; `iaMcpManagePermission` keeps its name.
|
|
30
|
+
|
|
31
|
+
Plugin documentation and example RBAC policy CSV updated to reflect the new MCP permission names.
|
|
32
|
+
|
|
33
|
+
- 0ed3adc: Breaking changes to the notebooks permissions model that uses behavior-linked vocabulary:
|
|
34
|
+
|
|
35
|
+
| Before (Lightspeed) | Before (Intelligent Assistant) | After |
|
|
36
|
+
| -------------------------- | ------------------------------------- | ---------------------------------------- |
|
|
37
|
+
| `lightspeed.notebooks.use` | `intelligent-assistant.notebooks.use` | `intelligent-assistant.notebooks.use` |
|
|
38
|
+
| | | `intelligent-assistant.notebooks.manage` |
|
|
39
|
+
- `notebooks.use` covers list/read/create session, upload document, and query endpoints
|
|
40
|
+
- `notebooks.manage` covers update/delete session and document endpoints
|
|
41
|
+
|
|
42
|
+
Removed permission CRUD action attributes; RBAC entries for notebooks permission sets now use the generic `use` action.
|
|
43
|
+
|
|
44
|
+
Hard-coded permission names were replaced by constants from the permission entities.
|
|
45
|
+
|
|
46
|
+
Plugin documentation and example RBAC policy CSV updated to reflect the notebooks permission model.
|
|
47
|
+
|
|
48
|
+
- f1dba9b: Breaking changes to the permissions model that uses behavior-linked vocabulary rather than CRUD-linked vocabulary:
|
|
49
|
+
|
|
50
|
+
| Before (Lightspeed) | Before (Intelligent Assistant) | After |
|
|
51
|
+
| ------------------------ | ----------------------------------- | ----------------------------------- |
|
|
52
|
+
| `lightspeed.chat.read` | `intelligent-assistant.chat.read` | `intelligent-assistant.chat.access` |
|
|
53
|
+
| `lightspeed.chat.create` | `intelligent-assistant.chat.create` | `intelligent-assistant.chat.use` |
|
|
54
|
+
| `lightspeed.chat.delete` | `intelligent-assistant.chat.delete` | `intelligent-assistant.chat.manage` |
|
|
55
|
+
| `lightspeed.chat.update` | `intelligent-assistant.chat.update` | `intelligent-assistant.chat.manage` |
|
|
56
|
+
|
|
57
|
+
Changes are applied to `lightspeed` (Frontend), `lightspeed-backend`, and `lightspeed-common` plugins.
|
|
58
|
+
|
|
59
|
+
Removed permission CRUD action attributes, rbac entries for permission sets now to generic 'use' action, allows `intelligent-assistant.chat.manage` to combine the update and delete actions.
|
|
60
|
+
|
|
61
|
+
Any hard-coded permission names have been replaced by constants, source references permission names from the permission entities.
|
|
62
|
+
|
|
63
|
+
`Trans.test.tsx` component test unit has permission names to reflect Intelligent Assistant for RHDH.
|
|
64
|
+
|
|
65
|
+
Additionally, hard-coded permission names have been replaced by local constants with the new names set.
|
|
66
|
+
|
|
67
|
+
Plugin documentation changes to revise information to permissions model changes to Intelligent Assistant for RHDH.
|
|
68
|
+
|
|
69
|
+
Changed permission variable 'lightspeed' prefix to 'ia' to use Intelligent Assistant rebranding.
|
|
70
|
+
|
|
71
|
+
Changes to example RBAC policy CSV file to reflect Intelligent Assistant for RHDH.
|
|
72
|
+
|
|
73
|
+
### Minor Changes
|
|
74
|
+
|
|
75
|
+
- e996529: Add inline rename for notebook resources with click or kebab menu, optimistic updates with rollback, frontend conflict validation, and backend PATCH endpoint with rollback on failure
|
|
76
|
+
- 0f32982: Deep context inteeligent-assistant model vision verification for 500 is not cached anymore
|
|
77
|
+
- 3d1d7d7: introduced new endpoint to proxy to LCORE /v1/skills endpoint, to be able to list available skills. also the backend endpoint will be gated by the new rbac permission: intelligent-assistant.skills.access
|
|
78
|
+
|
|
79
|
+
### Patch Changes
|
|
80
|
+
|
|
81
|
+
- 82742f0: Unify notebook terminology from "document" to "resource" across all UI strings, translations, and backend messages.
|
|
82
|
+
- Updated dependencies [3d1d7d7]
|
|
83
|
+
- Updated dependencies [adb65c9]
|
|
84
|
+
- Updated dependencies [0ed3adc]
|
|
85
|
+
- Updated dependencies [f1dba9b]
|
|
86
|
+
- @red-hat-developer-hub/backstage-plugin-intelligent-assistant-common@4.0.0
|
|
87
|
+
|
|
3
88
|
## 3.2.0
|
|
4
89
|
|
|
5
90
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -81,15 +81,16 @@ All nested keys (`servicePort`, `systemPrompt`, `prompts`, `mcpServers`, `notebo
|
|
|
81
81
|
|
|
82
82
|
Update permission names in your `rbac-policy.csv`:
|
|
83
83
|
|
|
84
|
-
| Before | After
|
|
85
|
-
| -------------------------- |
|
|
86
|
-
| `lightspeed.chat.read` | `intelligent-assistant.chat.
|
|
87
|
-
| `lightspeed.chat.create` | `intelligent-assistant.chat.
|
|
88
|
-
| `lightspeed.chat.delete` | `intelligent-assistant.chat.
|
|
89
|
-
| `lightspeed.chat.update` | `intelligent-assistant.chat.
|
|
90
|
-
| `lightspeed.notebooks.use` | `intelligent-assistant.notebooks.use`
|
|
91
|
-
|
|
|
92
|
-
| `lightspeed.mcp.
|
|
84
|
+
| Before | After |
|
|
85
|
+
| -------------------------- | ---------------------------------------- |
|
|
86
|
+
| `lightspeed.chat.read` | `intelligent-assistant.chat.access` |
|
|
87
|
+
| `lightspeed.chat.create` | `intelligent-assistant.chat.use` |
|
|
88
|
+
| `lightspeed.chat.delete` | `intelligent-assistant.chat.manage` |
|
|
89
|
+
| `lightspeed.chat.update` | `intelligent-assistant.chat.manage` |
|
|
90
|
+
| `lightspeed.notebooks.use` | `intelligent-assistant.notebooks.use` |
|
|
91
|
+
| | `intelligent-assistant.notebooks.manage` |
|
|
92
|
+
| `lightspeed.mcp.read` | `mcp.tools.use` |
|
|
93
|
+
| `lightspeed.mcp.manage` | `mcp.tools.manage` |
|
|
93
94
|
|
|
94
95
|
#### 5. OFS dynamic plugin configuration
|
|
95
96
|
|
|
@@ -332,20 +333,17 @@ The Intelligent Assistant Backend plugin has support for the permission framewor
|
|
|
332
333
|
- When [RBAC permission](https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-backend#installation) framework is enabled, for non-admin users to access intelligent-assistant backend API, the role associated with your user should have the following permission policies associated with it. Add the following in your permission policies configuration file named `rbac-policy.csv`:
|
|
333
334
|
|
|
334
335
|
```CSV
|
|
335
|
-
p, role:default/team_a, intelligent-assistant.chat.
|
|
336
|
-
p, role:default/team_a, intelligent-assistant.chat.
|
|
337
|
-
p, role:default/team_a, intelligent-assistant.chat.
|
|
338
|
-
p, role:default/team_a, intelligent-assistant.chat.update, update, allow
|
|
336
|
+
p, role:default/team_a, intelligent-assistant.chat.access, use, allow
|
|
337
|
+
p, role:default/team_a, intelligent-assistant.chat.use, use, allow
|
|
338
|
+
p, role:default/team_a, intelligent-assistant.chat.manage, use, allow
|
|
339
339
|
|
|
340
340
|
# Required for Notebooks feature (if enabled)
|
|
341
|
-
p, role:default/team_a, intelligent-assistant.notebooks.use,
|
|
341
|
+
p, role:default/team_a, intelligent-assistant.notebooks.use, use, allow
|
|
342
|
+
p, role:default/team_a, intelligent-assistant.notebooks.manage, use, allow
|
|
342
343
|
|
|
343
344
|
# Required for MCP server management (if configured)
|
|
344
|
-
p, role:default/team_a,
|
|
345
|
-
p, role:default/team_a,
|
|
346
|
-
|
|
347
|
-
# Required for saved prompts
|
|
348
|
-
p, role:default/team_a, intelligent-assistant.saved-prompts.manage, update, allow
|
|
345
|
+
p, role:default/team_a, mcp.tools.use, use, allow
|
|
346
|
+
p, role:default/team_a, mcp.tools.manage, use, allow
|
|
349
347
|
|
|
350
348
|
g, user:default/<your-user-name>, role:default/team_a
|
|
351
349
|
|
|
@@ -449,6 +447,7 @@ When enabled, Notebooks exposes the following REST API endpoints:
|
|
|
449
447
|
- `PUT /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents` - Upload or update a document (multipart/form-data)
|
|
450
448
|
- `GET /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents` - List all documents in a session
|
|
451
449
|
- `GET /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents/:documentId/status` - Get document processing status
|
|
450
|
+
- `PATCH /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents/:documentId` - Rename a document (JSON body: `{ "title": "new name" }`)
|
|
452
451
|
- `DELETE /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents/:documentId` - Delete a document
|
|
453
452
|
|
|
454
453
|
- **Queries**:
|
|
@@ -457,16 +456,19 @@ When enabled, Notebooks exposes the following REST API endpoints:
|
|
|
457
456
|
**Notes**:
|
|
458
457
|
|
|
459
458
|
- All endpoints require authentication (user context is automatically provided by Backstage)
|
|
460
|
-
- All `/v1/*` endpoints require
|
|
459
|
+
- All `/v1/*` endpoints require notebooks permissions:
|
|
460
|
+
- `intelligent-assistant.notebooks.use` for list/read/create session, upload document, and query endpoints
|
|
461
|
+
- `intelligent-assistant.notebooks.manage` for update/delete session and document endpoints
|
|
461
462
|
- Document endpoints verify session ownership before allowing operations
|
|
462
463
|
- `documentId` in paths is the document title (URL-encoded for special characters)
|
|
463
464
|
|
|
464
465
|
#### Permission Framework Support for Notebooks
|
|
465
466
|
|
|
466
|
-
When RBAC is enabled, users need the following
|
|
467
|
+
When RBAC is enabled, users need the following permissions to use Notebooks:
|
|
467
468
|
|
|
468
469
|
```CSV
|
|
469
|
-
p, role:default/team_a, intelligent-assistant.notebooks.use,
|
|
470
|
+
p, role:default/team_a, intelligent-assistant.notebooks.use, use, allow
|
|
471
|
+
p, role:default/team_a, intelligent-assistant.notebooks.manage, use, allow
|
|
470
472
|
|
|
471
473
|
g, user:default/<your-user-name>, role:default/team_a
|
|
472
474
|
```
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -5,9 +5,6 @@ var migration = require('./database/migration.cjs.js');
|
|
|
5
5
|
var notebooksRouters = require('./service/notebooks/notebooksRouters.cjs.js');
|
|
6
6
|
require('@backstage/errors');
|
|
7
7
|
require('./service/constant.cjs.js');
|
|
8
|
-
require('js-yaml');
|
|
9
|
-
require('pdfjs-dist');
|
|
10
|
-
require('stream');
|
|
11
8
|
var router = require('./service/router.cjs.js');
|
|
12
9
|
|
|
13
10
|
const intelligentAssistantPlugin = backendPluginApi.createBackendPlugin({
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\n\nimport { migrate } from './database/migration';\nimport { createNotebooksRouter } from './service/notebooks';\nimport { createRouter } from './service/router';\n\n/**\n * @public\n * The lightspeed backend plugin.\n */\nexport const intelligentAssistantPlugin = createBackendPlugin({\n pluginId: 'intelligent-assistant',\n register(env) {\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n http: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n auth: coreServices.auth,\n userInfo: coreServices.userInfo,\n permissions: coreServices.permissions,\n database: coreServices.database,\n },\n async init({\n logger,\n config,\n http,\n httpAuth,\n auth,\n userInfo,\n permissions,\n database,\n }) {\n await migrate(database);\n\n if (config.has('lightspeed')) {\n logger.warn(\n 'DEPRECATED: The \"lightspeed\" configuration key has been renamed to \"intelligent-assistant\". ' +\n 'Please update your app-config.yaml. The old \"lightspeed\" key is no longer read. ' +\n 'Migration guide: https://github.com/redhat-developer/rhdh-plugins/blob/main/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/README.md#migration-from-lightspeed-to-intelligent-assistant',\n );\n }\n\n const aiNotebooksEnabled =\n config.getOptionalBoolean(\n 'intelligent-assistant.notebooks.enabled',\n ) ?? false;\n\n if (aiNotebooksEnabled) {\n const queryModel = config.getOptionalString(\n 'intelligent-assistant.notebooks.queryDefaults.model',\n );\n const queryProvider = config.getOptionalString(\n 'intelligent-assistant.notebooks.queryDefaults.provider_id',\n );\n\n if (!queryModel || !queryProvider) {\n logger.warn(\n 'AI Notebooks feature is enabled but required configuration is missing. ' +\n 'Please configure intelligent-assistant.notebooks.queryDefaults.model and intelligent-assistant.notebooks.queryDefaults.provider_id. ' +\n 'Notebooks will not be available until these are set.',\n );\n } else {\n http.use(\n await createNotebooksRouter({\n config: config,\n logger: logger,\n httpAuth: httpAuth,\n userInfo: userInfo,\n permissions,\n }),\n );\n logger.info('AI Notebooks enabled');\n\n http.addAuthPolicy({\n path: '/notebooks/health',\n allow: 'unauthenticated',\n });\n }\n }\n\n http.use(\n await createRouter({\n config,\n logger,\n database,\n httpAuth,\n auth,\n userInfo,\n permissions,\n }),\n );\n\n // Configure authentication policies\n http.addAuthPolicy({\n path: '/health',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","migrate","createNotebooksRouter","createRouter"],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\n\nimport { migrate } from './database/migration';\nimport { createNotebooksRouter } from './service/notebooks';\nimport { createRouter } from './service/router';\n\n/**\n * @public\n * The lightspeed backend plugin.\n */\nexport const intelligentAssistantPlugin = createBackendPlugin({\n pluginId: 'intelligent-assistant',\n register(env) {\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n http: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n auth: coreServices.auth,\n userInfo: coreServices.userInfo,\n permissions: coreServices.permissions,\n database: coreServices.database,\n },\n async init({\n logger,\n config,\n http,\n httpAuth,\n auth,\n userInfo,\n permissions,\n database,\n }) {\n await migrate(database);\n\n if (config.has('lightspeed')) {\n logger.warn(\n 'DEPRECATED: The \"lightspeed\" configuration key has been renamed to \"intelligent-assistant\". ' +\n 'Please update your app-config.yaml. The old \"lightspeed\" key is no longer read. ' +\n 'Migration guide: https://github.com/redhat-developer/rhdh-plugins/blob/main/workspaces/intelligent-assistant/plugins/intelligent-assistant-backend/README.md#migration-from-lightspeed-to-intelligent-assistant',\n );\n }\n\n const aiNotebooksEnabled =\n config.getOptionalBoolean(\n 'intelligent-assistant.notebooks.enabled',\n ) ?? false;\n\n if (aiNotebooksEnabled) {\n const queryModel = config.getOptionalString(\n 'intelligent-assistant.notebooks.queryDefaults.model',\n );\n const queryProvider = config.getOptionalString(\n 'intelligent-assistant.notebooks.queryDefaults.provider_id',\n );\n\n if (!queryModel || !queryProvider) {\n logger.warn(\n 'AI Notebooks feature is enabled but required configuration is missing. ' +\n 'Please configure intelligent-assistant.notebooks.queryDefaults.model and intelligent-assistant.notebooks.queryDefaults.provider_id. ' +\n 'Notebooks will not be available until these are set.',\n );\n } else {\n http.use(\n await createNotebooksRouter({\n config: config,\n logger: logger,\n httpAuth: httpAuth,\n userInfo: userInfo,\n permissions,\n }),\n );\n logger.info('AI Notebooks enabled');\n\n http.addAuthPolicy({\n path: '/notebooks/health',\n allow: 'unauthenticated',\n });\n }\n }\n\n http.use(\n await createRouter({\n config,\n logger,\n database,\n httpAuth,\n auth,\n userInfo,\n permissions,\n }),\n );\n\n // Configure authentication policies\n http.addAuthPolicy({\n path: '/health',\n allow: 'unauthenticated',\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","migrate","createNotebooksRouter","createRouter"],"mappings":";;;;;;;;;AA6BO,MAAM,6BAA6BA,oCAAA,CAAoB;AAAA,EAC5D,QAAA,EAAU,uBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,QAAQC,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,MAAMA,6BAAA,CAAa,UAAA;AAAA,QACnB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,aAAaA,6BAAA,CAAa,WAAA;AAAA,QAC1B,UAAUA,6BAAA,CAAa;AAAA,OACzB;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,MAAA;AAAA,QACA,MAAA;AAAA,QACA,IAAA;AAAA,QACA,QAAA;AAAA,QACA,IAAA;AAAA,QACA,QAAA;AAAA,QACA,WAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAMC,kBAAQ,QAAQ,CAAA;AAEtB,QAAA,IAAI,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,EAAG;AAC5B,UAAA,MAAA,CAAO,IAAA;AAAA,YACL;AAAA,WAGF;AAAA,QACF;AAEA,QAAA,MAAM,qBACJ,MAAA,CAAO,kBAAA;AAAA,UACL;AAAA,SACF,IAAK,KAAA;AAEP,QAAA,IAAI,kBAAA,EAAoB;AACtB,UAAA,MAAM,aAAa,MAAA,CAAO,iBAAA;AAAA,YACxB;AAAA,WACF;AACA,UAAA,MAAM,gBAAgB,MAAA,CAAO,iBAAA;AAAA,YAC3B;AAAA,WACF;AAEA,UAAA,IAAI,CAAC,UAAA,IAAc,CAAC,aAAA,EAAe;AACjC,YAAA,MAAA,CAAO,IAAA;AAAA,cACL;AAAA,aAGF;AAAA,UACF,CAAA,MAAO;AACL,YAAA,IAAA,CAAK,GAAA;AAAA,cACH,MAAMC,sCAAA,CAAsB;AAAA,gBAC1B,MAAA;AAAA,gBACA,MAAA;AAAA,gBACA,QAAA;AAAA,gBACA,QAAA;AAAA,gBACA;AAAA,eACD;AAAA,aACH;AACA,YAAA,MAAA,CAAO,KAAK,sBAAsB,CAAA;AAElC,YAAA,IAAA,CAAK,aAAA,CAAc;AAAA,cACjB,IAAA,EAAM,mBAAA;AAAA,cACN,KAAA,EAAO;AAAA,aACR,CAAA;AAAA,UACH;AAAA,QACF;AAEA,QAAA,IAAA,CAAK,GAAA;AAAA,UACH,MAAMC,mBAAA,CAAa;AAAA,YACjB,MAAA;AAAA,YACA,MAAA;AAAA,YACA,QAAA;AAAA,YACA,QAAA;AAAA,YACA,IAAA;AAAA,YACA,QAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAGA,QAAA,IAAA,CAAK,aAAA,CAAc;AAAA,UACjB,IAAA,EAAM,SAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
|
|
@@ -1,36 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require('htmlparser2');
|
|
3
|
+
require('@backstage/errors');
|
|
5
4
|
require('dns/promises');
|
|
6
5
|
require('net');
|
|
7
6
|
var constant = require('../../constant.cjs.js');
|
|
8
|
-
var fileParser = require('./fileParser.cjs.js');
|
|
9
7
|
|
|
10
|
-
const isValidFileSize = (fileSize) => {
|
|
11
|
-
const maxSize = constant.DEFAULT_MAX_FILE_SIZE_MB;
|
|
12
|
-
return fileSize <= maxSize;
|
|
13
|
-
};
|
|
14
8
|
const isValidFileType = (fileType) => {
|
|
15
9
|
const normalizedType = fileType.toLowerCase().replace(/^\./, "");
|
|
16
10
|
return Object.values(constant.SupportedFileType).includes(
|
|
17
11
|
normalizedType
|
|
18
12
|
);
|
|
19
13
|
};
|
|
20
|
-
const parseFileContent = async (logger, fileType, file) => {
|
|
21
|
-
if (!file) {
|
|
22
|
-
throw new errors.InputError("No file uploaded");
|
|
23
|
-
}
|
|
24
|
-
if (!isValidFileSize(file.size)) {
|
|
25
|
-
throw new errors.InputError(
|
|
26
|
-
`File size exceeds ${constant.DEFAULT_MAX_FILE_SIZE_MB / 1024 / 1024}MB limit`
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
logger.info(`Parsing file ${file.originalname} for fileType ${fileType}`);
|
|
30
|
-
return await fileParser.parseFile(file.buffer, file.originalname, fileType);
|
|
31
|
-
};
|
|
32
14
|
|
|
33
|
-
exports.isValidFileSize = isValidFileSize;
|
|
34
15
|
exports.isValidFileType = isValidFileType;
|
|
35
|
-
exports.parseFileContent = parseFileContent;
|
|
36
16
|
//# sourceMappingURL=documentHelpers.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentHelpers.cjs.js","sources":["../../../../src/service/notebooks/documents/documentHelpers.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { InputError } from '@backstage/errors';\n\nimport { Parser } from 'htmlparser2';\n\nimport dns from 'dns/promises';\nimport { isIP } from 'net';\n\nimport {\n DEFAULT_MAX_FILE_SIZE_MB,\n FILTERED_CONTENT_MARKER,\n HTML_BLOCK_TAGS,\n HTML_IGNORED_TAGS,\n MAX_CONSECUTIVE_NEWLINES,\n PROMPT_INJECTION_PATTERNS,\n SSRF_BLOCKED_HOSTNAMES,\n SupportedFileType,\n} from '../../constant';\nimport { parseFile } from './fileParser';\n\n// ==============================================================================\n// URL Support Functions (Reserved for Future URL File Type Support)\n// ==============================================================================\n// The following functions are currently unused but reserved for future\n// implementation of URL file type parsing. They provide:\n// - URL validation and format checking\n// - SSRF (Server-Side Request Forgery) protection\n// - HTML content parsing and sanitization\n// - Prompt injection attack prevention\n//\n// These functions were part of the URL parsing implementation that was removed\n// but are kept here to facilitate re-implementation if needed in the future.\n// ==============================================================================\n\n/**\n * Validate URL format\n * @reserved Reserved for future URL file type support\n */\nexport const isValidURL = (urlString: string): boolean => {\n try {\n const url = new URL(urlString);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n};\n\n/**\n * Validate file size (max 20MB by default)\n */\nexport const isValidFileSize = (fileSize: number): boolean => {\n const maxSize = DEFAULT_MAX_FILE_SIZE_MB;\n return fileSize <= maxSize;\n};\n\n/**\n * Validate file type\n */\nexport const isValidFileType = (fileType: string): boolean => {\n const normalizedType = fileType.toLowerCase().replace(/^\\./, '');\n return Object.values(SupportedFileType).includes(\n normalizedType as SupportedFileType,\n );\n};\n\n/**\n * Parse file from upload\n * @param logger - Logger service\n * @param fileType - File type\n * @param file - File to parse\n * @returns Parsed file\n */\nexport const parseFileContent = async (\n logger: LoggerService,\n fileType: string,\n file?: Express.Multer.File,\n) => {\n if (!file) {\n throw new InputError('No file uploaded');\n }\n if (!isValidFileSize(file.size)) {\n throw new InputError(\n `File size exceeds ${DEFAULT_MAX_FILE_SIZE_MB / 1024 / 1024}MB limit`,\n );\n }\n logger.info(`Parsing file ${file.originalname} for fileType ${fileType}`);\n return await parseFile(file.buffer, file.originalname, fileType);\n};\n\n/**\n * Check if an IP address is private, internal, or a metadata endpoint\n * Blocks SSRF attacks to internal networks\n * @reserved Reserved for future URL file type support\n */\nconst isPrivateOrInternalIP = (ip: string): boolean => {\n // Check if it's a valid IP\n const ipVersion = isIP(ip);\n if (ipVersion === 0) {\n return false; // Not a valid IP\n }\n\n if (ipVersion === 4) {\n const parts = ip.split('.').map(Number);\n\n switch (parts[0]) {\n case 0: // 0.0.0.0/8 - Current network\n case 10: // 10.0.0.0/8 - Private\n case 127: // 127.0.0.0/8 - Loopback\n return true;\n case 100: // 100.64.0.0/10 - Carrier-grade NAT\n return parts[1] >= 64 && parts[1] <= 127;\n case 169: // 169.254.0.0/16 - Link-local (includes cloud metadata endpoint 169.254.169.254)\n return parts[1] === 254;\n case 172: // 172.16.0.0/12 - Private\n return parts[1] >= 16 && parts[1] <= 31;\n case 192: // 192.168.0.0/16 - Private\n return parts[1] === 168;\n default:\n // 224.0.0.0/4 - Multicast, 240.0.0.0/4 - Reserved\n return parts[0] >= 224;\n }\n } else if (ipVersion === 6) {\n const lower = ip.toLowerCase();\n\n // ::1 - Loopback\n if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') return true;\n\n // fc00::/7 - Unique local address\n if (lower.startsWith('fc') || lower.startsWith('fd')) return true;\n\n // fe80::/10 - Link-local\n if (\n lower.startsWith('fe8') ||\n lower.startsWith('fe9') ||\n lower.startsWith('fea') ||\n lower.startsWith('feb')\n )\n return true;\n\n // ::ffff:0:0/96 - IPv4-mapped IPv6\n if (lower.startsWith('::ffff:')) {\n const ipv4Part = ip.substring(7);\n // Check if it's in dotted-decimal format (e.g., ::ffff:127.0.0.1)\n if (ipv4Part.includes('.')) {\n return isPrivateOrInternalIP(ipv4Part);\n }\n // Handle hex format (e.g., ::ffff:7f00:1 which is 127.0.0.1)\n // Extract the hex parts and check against private ranges\n const parts = ipv4Part.split(':');\n if (parts.length >= 1) {\n const firstHex = parseInt(parts[0], 16);\n // Check for common private ranges in hex:\n // 127.x.x.x -> 0x7F00-0x7FFF (loopback)\n // 10.x.x.x -> 0x0A00-0x0AFF (private)\n // 192.168.x.x -> 0xC0A8 (private)\n // 172.16-31.x.x -> 0xAC10-0xAC1F (private)\n // 169.254.x.x -> 0xA9FE (link-local)\n if (\n (firstHex >= 0x7f00 && firstHex <= 0x7fff) || // 127.x.x.x\n (firstHex >= 0x0a00 && firstHex <= 0x0aff) || // 10.x.x.x\n firstHex === 0xc0a8 || // 192.168.x.x\n (firstHex >= 0xac10 && firstHex <= 0xac1f) || // 172.16-31.x.x\n firstHex === 0xa9fe // 169.254.x.x\n ) {\n return true;\n }\n }\n }\n }\n\n return false;\n};\n\n/**\n * Validate URL and check for SSRF (Server-Side Request Forgery) vulnerabilities\n *\n * This function protects against SSRF attacks by:\n * 1. Blocking access to private/internal IP ranges (RFC 1918, link-local, loopback)\n * 2. Blocking access to cloud metadata endpoints (AWS, GCP, Azure)\n * 3. Resolving hostnames to IPs to prevent DNS rebinding attacks\n *\n * @param urlString - URL to validate\n * @throws InputError if URL points to blocked hostname or private IP\n * @reserved Reserved for future URL file type support\n */\nexport const validateURLForSSRF = async (urlString: string): Promise<void> => {\n const url = new URL(urlString);\n\n // Strip brackets from IPv6 addresses (e.g., [::1] -> ::1)\n const hostname = url.hostname.replace(/^\\[|\\]$/g, '');\n\n // Check if hostname is already an IP address\n const ipVersion = isIP(hostname);\n if (ipVersion !== 0) {\n if (isPrivateOrInternalIP(hostname)) {\n throw new InputError(\n 'Access to private/internal IP addresses is not allowed',\n );\n }\n return;\n }\n\n // Block localhost and common internal hostnames used in SSRF attacks\n const lowerHostname = hostname.toLowerCase();\n if (SSRF_BLOCKED_HOSTNAMES.includes(lowerHostname as any)) {\n throw new InputError(`Access to ${lowerHostname} is not allowed`);\n }\n\n // Resolve hostname to IP addresses\n try {\n const addresses = await dns.resolve(hostname);\n\n // Check all resolved IPs\n for (const address of addresses) {\n if (isPrivateOrInternalIP(address)) {\n throw new InputError(\n `URL resolves to private/internal IP address (${address}), which is not allowed`,\n );\n }\n }\n } catch (error: any) {\n // Re-throw InputError from isPrivateOrInternalIP check\n if (error instanceof InputError) {\n throw error;\n }\n // DNS resolution failure is a user input issue (invalid hostname)\n throw new InputError(`Failed to resolve hostname: ${error.message}`);\n }\n};\n\n/**\n * Strip HTML tags and extract readable text from HTML content\n * @reserved Reserved for future URL file type support\n */\nexport const stripHtmlTags = (html: string): string => {\n let text = '';\n let ignoring = false;\n\n const parser = new Parser(\n {\n onopentag(name) {\n if (HTML_IGNORED_TAGS.has(name)) ignoring = true;\n },\n onclosetag(name) {\n if (HTML_IGNORED_TAGS.has(name)) ignoring = false;\n if (HTML_BLOCK_TAGS.has(name)) text += '\\n';\n },\n ontext(data) {\n if (!ignoring) text += data;\n },\n },\n { decodeEntities: true },\n );\n\n parser.write(html);\n parser.end();\n\n return text\n .replace(/\\n\\s*\\n/g, '\\n\\n')\n .replace(/[ \\t]+/g, ' ')\n .trim();\n};\n\n/**\n * Sanitize content to prevent prompt injection attacks\n * Detects and filters common prompt injection patterns\n * @param content - Raw content to sanitize\n * @returns Sanitized content with prompt injection patterns removed\n * @reserved Reserved for future URL file type support\n */\nexport const sanitizeContentForRAG = (content: string): string => {\n let sanitized = content;\n\n // Replace prompt injection patterns with filtered marker\n for (const pattern of PROMPT_INJECTION_PATTERNS) {\n sanitized = sanitized.replace(pattern, FILTERED_CONTENT_MARKER);\n }\n\n // Limit excessive consecutive newlines to prevent context stuffing\n const newlinePattern = new RegExp(\n `\\\\n{${MAX_CONSECUTIVE_NEWLINES + 1},}`,\n 'g',\n );\n sanitized = sanitized.replace(\n newlinePattern,\n '\\n'.repeat(MAX_CONSECUTIVE_NEWLINES),\n );\n\n // Normalize excessive whitespace\n sanitized = sanitized.replace(/[ \\t]{10,}/g, ' ');\n\n return sanitized.trim();\n};\n"],"names":["DEFAULT_MAX_FILE_SIZE_MB","SupportedFileType","InputError","parseFile"],"mappings":";;;;;;;;;AAiEO,MAAM,eAAA,GAAkB,CAAC,QAAA,KAA8B;AAC5D,EAAA,MAAM,OAAA,GAAUA,iCAAA;AAChB,EAAA,OAAO,QAAA,IAAY,OAAA;AACrB;AAKO,MAAM,eAAA,GAAkB,CAAC,QAAA,KAA8B;AAC5D,EAAA,MAAM,iBAAiB,QAAA,CAAS,WAAA,EAAY,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC/D,EAAA,OAAO,MAAA,CAAO,MAAA,CAAOC,0BAAiB,CAAA,CAAE,QAAA;AAAA,IACtC;AAAA,GACF;AACF;AASO,MAAM,gBAAA,GAAmB,OAC9B,MAAA,EACA,QAAA,EACA,IAAA,KACG;AACH,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,MAAM,IAAIC,kBAAW,kBAAkB,CAAA;AAAA,EACzC;AACA,EAAA,IAAI,CAAC,eAAA,CAAgB,IAAA,CAAK,IAAI,CAAA,EAAG;AAC/B,IAAA,MAAM,IAAIA,iBAAA;AAAA,MACR,CAAA,kBAAA,EAAqBF,iCAAA,GAA2B,IAAA,GAAO,IAAI,CAAA,QAAA;AAAA,KAC7D;AAAA,EACF;AACA,EAAA,MAAA,CAAO,KAAK,CAAA,aAAA,EAAgB,IAAA,CAAK,YAAY,CAAA,cAAA,EAAiB,QAAQ,CAAA,CAAE,CAAA;AACxE,EAAA,OAAO,MAAMG,oBAAA,CAAU,IAAA,CAAK,MAAA,EAAQ,IAAA,CAAK,cAAc,QAAQ,CAAA;AACjE;;;;;;"}
|
|
1
|
+
{"version":3,"file":"documentHelpers.cjs.js","sources":["../../../../src/service/notebooks/documents/documentHelpers.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { InputError } from '@backstage/errors';\n\nimport dns from 'dns/promises';\nimport { isIP } from 'net';\n\nimport {\n DEFAULT_MAX_FILE_SIZE_MB,\n FILTERED_CONTENT_MARKER,\n MAX_CONSECUTIVE_NEWLINES,\n PROMPT_INJECTION_PATTERNS,\n SSRF_BLOCKED_HOSTNAMES,\n SupportedFileType,\n} from '../../constant';\n\n// ==============================================================================\n// URL Support Functions (Reserved for Future URL File Type Support)\n// ==============================================================================\n// The following functions are currently unused but reserved for future\n// implementation of URL file type parsing. They provide:\n// - URL validation and format checking\n// - SSRF (Server-Side Request Forgery) protection\n// - HTML content parsing and sanitization\n// - Prompt injection attack prevention\n//\n// These functions were part of the URL parsing implementation that was removed\n// but are kept here to facilitate re-implementation if needed in the future.\n// ==============================================================================\n\n/**\n * Validate URL format\n * @reserved Reserved for future URL file type support\n */\nexport const isValidURL = (urlString: string): boolean => {\n try {\n const url = new URL(urlString);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n};\n\n/**\n * Validate file size (max 20MB by default)\n */\nexport const isValidFileSize = (fileSize: number): boolean => {\n const maxSize = DEFAULT_MAX_FILE_SIZE_MB;\n return fileSize <= maxSize;\n};\n\n/**\n * Validate file type\n */\nexport const isValidFileType = (fileType: string): boolean => {\n const normalizedType = fileType.toLowerCase().replace(/^\\./, '');\n return Object.values(SupportedFileType).includes(\n normalizedType as SupportedFileType,\n );\n};\n\n/**\n * Check if an IP address is private, internal, or a metadata endpoint\n * Blocks SSRF attacks to internal networks\n * @reserved Reserved for future URL file type support\n */\nconst isPrivateOrInternalIP = (ip: string): boolean => {\n // Check if it's a valid IP\n const ipVersion = isIP(ip);\n if (ipVersion === 0) {\n return false; // Not a valid IP\n }\n\n if (ipVersion === 4) {\n const parts = ip.split('.').map(Number);\n\n switch (parts[0]) {\n case 0: // 0.0.0.0/8 - Current network\n case 10: // 10.0.0.0/8 - Private\n case 127: // 127.0.0.0/8 - Loopback\n return true;\n case 100: // 100.64.0.0/10 - Carrier-grade NAT\n return parts[1] >= 64 && parts[1] <= 127;\n case 169: // 169.254.0.0/16 - Link-local (includes cloud metadata endpoint 169.254.169.254)\n return parts[1] === 254;\n case 172: // 172.16.0.0/12 - Private\n return parts[1] >= 16 && parts[1] <= 31;\n case 192: // 192.168.0.0/16 - Private\n return parts[1] === 168;\n default:\n // 224.0.0.0/4 - Multicast, 240.0.0.0/4 - Reserved\n return parts[0] >= 224;\n }\n } else if (ipVersion === 6) {\n const lower = ip.toLowerCase();\n\n // ::1 - Loopback\n if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') return true;\n\n // fc00::/7 - Unique local address\n if (lower.startsWith('fc') || lower.startsWith('fd')) return true;\n\n // fe80::/10 - Link-local\n if (\n lower.startsWith('fe8') ||\n lower.startsWith('fe9') ||\n lower.startsWith('fea') ||\n lower.startsWith('feb')\n )\n return true;\n\n // ::ffff:0:0/96 - IPv4-mapped IPv6\n if (lower.startsWith('::ffff:')) {\n const ipv4Part = ip.substring(7);\n // Check if it's in dotted-decimal format (e.g., ::ffff:127.0.0.1)\n if (ipv4Part.includes('.')) {\n return isPrivateOrInternalIP(ipv4Part);\n }\n // Handle hex format (e.g., ::ffff:7f00:1 which is 127.0.0.1)\n // Extract the hex parts and check against private ranges\n const parts = ipv4Part.split(':');\n if (parts.length >= 1) {\n const firstHex = parseInt(parts[0], 16);\n // Check for common private ranges in hex:\n // 127.x.x.x -> 0x7F00-0x7FFF (loopback)\n // 10.x.x.x -> 0x0A00-0x0AFF (private)\n // 192.168.x.x -> 0xC0A8 (private)\n // 172.16-31.x.x -> 0xAC10-0xAC1F (private)\n // 169.254.x.x -> 0xA9FE (link-local)\n if (\n (firstHex >= 0x7f00 && firstHex <= 0x7fff) || // 127.x.x.x\n (firstHex >= 0x0a00 && firstHex <= 0x0aff) || // 10.x.x.x\n firstHex === 0xc0a8 || // 192.168.x.x\n (firstHex >= 0xac10 && firstHex <= 0xac1f) || // 172.16-31.x.x\n firstHex === 0xa9fe // 169.254.x.x\n ) {\n return true;\n }\n }\n }\n }\n\n return false;\n};\n\n/**\n * Validate URL and check for SSRF (Server-Side Request Forgery) vulnerabilities\n *\n * This function protects against SSRF attacks by:\n * 1. Blocking access to private/internal IP ranges (RFC 1918, link-local, loopback)\n * 2. Blocking access to cloud metadata endpoints (AWS, GCP, Azure)\n * 3. Resolving hostnames to IPs to prevent DNS rebinding attacks\n *\n * @param urlString - URL to validate\n * @throws InputError if URL points to blocked hostname or private IP\n * @reserved Reserved for future URL file type support\n */\nexport const validateURLForSSRF = async (urlString: string): Promise<void> => {\n const url = new URL(urlString);\n\n // Strip brackets from IPv6 addresses (e.g., [::1] -> ::1)\n const hostname = url.hostname.replace(/^\\[|\\]$/g, '');\n\n // Check if hostname is already an IP address\n const ipVersion = isIP(hostname);\n if (ipVersion !== 0) {\n if (isPrivateOrInternalIP(hostname)) {\n throw new InputError(\n 'Access to private/internal IP addresses is not allowed',\n );\n }\n return;\n }\n\n // Block localhost and common internal hostnames used in SSRF attacks\n const lowerHostname = hostname.toLowerCase();\n if (SSRF_BLOCKED_HOSTNAMES.includes(lowerHostname as any)) {\n throw new InputError(`Access to ${lowerHostname} is not allowed`);\n }\n\n // Resolve hostname to IP addresses\n try {\n const addresses = await dns.resolve(hostname);\n\n // Check all resolved IPs\n for (const address of addresses) {\n if (isPrivateOrInternalIP(address)) {\n throw new InputError(\n `URL resolves to private/internal IP address (${address}), which is not allowed`,\n );\n }\n }\n } catch (error: any) {\n // Re-throw InputError from isPrivateOrInternalIP check\n if (error instanceof InputError) {\n throw error;\n }\n // DNS resolution failure is a user input issue (invalid hostname)\n throw new InputError(`Failed to resolve hostname: ${error.message}`);\n }\n};\n\n/**\n * Sanitize content to prevent prompt injection attacks\n * Detects and filters common prompt injection patterns\n * @param content - Raw content to sanitize\n * @returns Sanitized content with prompt injection patterns removed\n * @reserved Reserved for future URL file type support\n */\nexport const sanitizeContentForRAG = (content: string): string => {\n let sanitized = content;\n\n // Replace prompt injection patterns with filtered marker\n for (const pattern of PROMPT_INJECTION_PATTERNS) {\n sanitized = sanitized.replace(pattern, FILTERED_CONTENT_MARKER);\n }\n\n // Limit excessive consecutive newlines to prevent context stuffing\n const newlinePattern = new RegExp(\n `\\\\n{${MAX_CONSECUTIVE_NEWLINES + 1},}`,\n 'g',\n );\n sanitized = sanitized.replace(\n newlinePattern,\n '\\n'.repeat(MAX_CONSECUTIVE_NEWLINES),\n );\n\n // Normalize excessive whitespace\n sanitized = sanitized.replace(/[ \\t]{10,}/g, ' ');\n\n return sanitized.trim();\n};\n"],"names":["SupportedFileType"],"mappings":";;;;;;;AAmEO,MAAM,eAAA,GAAkB,CAAC,QAAA,KAA8B;AAC5D,EAAA,MAAM,iBAAiB,QAAA,CAAS,WAAA,EAAY,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC/D,EAAA,OAAO,MAAA,CAAO,MAAA,CAAOA,0BAAiB,CAAA,CAAE,QAAA;AAAA,IACtC;AAAA,GACF;AACF;;;;"}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
var errors = require('@backstage/errors');
|
|
4
4
|
var constant = require('../../constant.cjs.js');
|
|
5
|
-
var fileParser = require('./fileParser.cjs.js');
|
|
6
5
|
|
|
7
6
|
class DocumentService {
|
|
8
7
|
logger;
|
|
@@ -43,20 +42,21 @@ class DocumentService {
|
|
|
43
42
|
) || null;
|
|
44
43
|
}
|
|
45
44
|
/**
|
|
46
|
-
* Upload
|
|
47
|
-
* @param content -
|
|
45
|
+
* Upload markdown content to the Files API
|
|
46
|
+
* @param content - Markdown content string
|
|
48
47
|
* @param title - File title/name
|
|
49
48
|
* @returns File ID from the Files API
|
|
50
49
|
* @throws Error if upload fails
|
|
51
50
|
*/
|
|
52
51
|
async uploadFile(content, title) {
|
|
53
52
|
try {
|
|
54
|
-
const
|
|
55
|
-
const txtFilename = `${title.replace(/\.[^.]+$/, "")}.txt`;
|
|
53
|
+
const mdFilename = `${title.replace(/\.[^.]+$/, "")}.md`;
|
|
56
54
|
const file = await this.client.files.create({
|
|
57
|
-
file:
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
file: {
|
|
56
|
+
name: mdFilename,
|
|
57
|
+
buffer: Buffer.from(content, "utf-8"),
|
|
58
|
+
type: "text/markdown"
|
|
59
|
+
},
|
|
60
60
|
purpose: "assistants"
|
|
61
61
|
});
|
|
62
62
|
this.logger.info(
|
|
@@ -71,51 +71,93 @@ class DocumentService {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
* Upsert a document - create if it doesn't exist, update if it does
|
|
74
|
+
* Upsert a document - create if it doesn't exist, update if it does.
|
|
75
|
+
* Also used for rename-only operations when fileType/fileId are omitted.
|
|
75
76
|
* @param sessionId - Vector store ID
|
|
76
77
|
* @param title - Original document title
|
|
77
|
-
* @param
|
|
78
|
-
* @param
|
|
79
|
-
* @param
|
|
78
|
+
* @param opts - Optional parameters for upload or rename
|
|
79
|
+
* @param opts.fileType - Document source type (derived from existing file when omitted)
|
|
80
|
+
* @param opts.fileId - File ID from Files API (derived from existing file when omitted)
|
|
81
|
+
* @param opts.newTitle - New title for rename operation
|
|
80
82
|
* @returns Upsert result with document ID and status
|
|
83
|
+
* @throws NotFoundError if fileId is omitted and document does not exist
|
|
81
84
|
* @throws ConflictError if newTitle conflicts with existing document
|
|
82
85
|
*/
|
|
83
|
-
async upsertDocument(sessionId, title,
|
|
86
|
+
async upsertDocument(sessionId, title, opts) {
|
|
87
|
+
const { fileType, fileId, newTitle } = opts || {};
|
|
84
88
|
const existingFile = await this.findFileByTitle(sessionId, title);
|
|
89
|
+
let resolvedFileId;
|
|
90
|
+
let resolvedFileType;
|
|
91
|
+
if (fileId) {
|
|
92
|
+
resolvedFileId = fileId;
|
|
93
|
+
resolvedFileType = fileType || "unknown";
|
|
94
|
+
} else {
|
|
95
|
+
if (!existingFile) {
|
|
96
|
+
throw new errors.NotFoundError(`Document not found: ${title}`);
|
|
97
|
+
}
|
|
98
|
+
resolvedFileId = existingFile.id;
|
|
99
|
+
resolvedFileType = fileType || existingFile.attributes?.source_type || "unknown";
|
|
100
|
+
}
|
|
85
101
|
const createdAt = existingFile?.attributes?.created_at || (/* @__PURE__ */ new Date()).toISOString();
|
|
86
102
|
if (newTitle && title !== newTitle) {
|
|
87
103
|
const conflictingFile = await this.findFileByTitle(sessionId, newTitle);
|
|
88
104
|
if (conflictingFile) {
|
|
89
105
|
throw new errors.ConflictError(
|
|
90
|
-
`A
|
|
106
|
+
`A resource with the title "${newTitle || title}" already exists in this session`
|
|
91
107
|
);
|
|
92
108
|
}
|
|
93
109
|
}
|
|
94
110
|
if (existingFile) {
|
|
95
111
|
await this.deleteDocument(sessionId, title);
|
|
96
112
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
const baseAttrs = existingFile?.attributes || {};
|
|
114
|
+
const attributes = {
|
|
115
|
+
...baseAttrs,
|
|
116
|
+
title: newTitle || title,
|
|
117
|
+
source_type: resolvedFileType,
|
|
118
|
+
created_at: createdAt,
|
|
119
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
const vectorStoreFile = await this.client.vectorStores.files.create(
|
|
123
|
+
sessionId,
|
|
124
|
+
{
|
|
125
|
+
file_id: resolvedFileId,
|
|
126
|
+
chunking_strategy: this.chunkingStrategy,
|
|
127
|
+
attributes
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
this.logger.info(
|
|
131
|
+
`Document "${newTitle || title}" (ID: ${title}) upsert started with file ${resolvedFileId}`
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
document_id: newTitle || title,
|
|
135
|
+
file_id: resolvedFileId,
|
|
136
|
+
replaced: !!existingFile,
|
|
137
|
+
status: vectorStoreFile.status
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (existingFile) {
|
|
141
|
+
this.logger.error(
|
|
142
|
+
`Failed to re-create vector store entry after delete for "${title}". Attempting rollback.`
|
|
143
|
+
);
|
|
144
|
+
try {
|
|
145
|
+
await this.client.vectorStores.files.create(sessionId, {
|
|
146
|
+
file_id: existingFile.id,
|
|
147
|
+
chunking_strategy: this.chunkingStrategy,
|
|
148
|
+
attributes: existingFile.attributes || {}
|
|
149
|
+
});
|
|
150
|
+
this.logger.info(
|
|
151
|
+
`Rollback succeeded: restored "${title}" in session ${sessionId}`
|
|
152
|
+
);
|
|
153
|
+
} catch (rollbackError) {
|
|
154
|
+
this.logger.error(
|
|
155
|
+
`Rollback failed: document "${title}" (file ${existingFile.id}) is orphaned in session ${sessionId}`
|
|
156
|
+
);
|
|
107
157
|
}
|
|
108
158
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`Document "${newTitle || title}" (ID: ${title}) upload started with file ${fileId}`
|
|
112
|
-
);
|
|
113
|
-
return {
|
|
114
|
-
document_id: newTitle || title,
|
|
115
|
-
file_id: fileId,
|
|
116
|
-
replaced: false,
|
|
117
|
-
status: vectorStoreFile.status
|
|
118
|
-
};
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
119
161
|
}
|
|
120
162
|
/**
|
|
121
163
|
* Get file processing status
|
|
@@ -127,7 +169,7 @@ class DocumentService {
|
|
|
127
169
|
async getFileStatus(sessionId, documentTitle) {
|
|
128
170
|
const file = await this.findFileByTitle(sessionId, documentTitle);
|
|
129
171
|
if (!file) {
|
|
130
|
-
throw new errors.NotFoundError(`
|
|
172
|
+
throw new errors.NotFoundError(`Resource not found: ${documentTitle}`);
|
|
131
173
|
}
|
|
132
174
|
return {
|
|
133
175
|
status: file.status,
|
|
@@ -160,6 +202,10 @@ class DocumentService {
|
|
|
160
202
|
created_at: attrs.created_at,
|
|
161
203
|
updated_at: attrs.updated_at
|
|
162
204
|
};
|
|
205
|
+
}).sort((a, b) => {
|
|
206
|
+
const timeA = a.created_at ? new Date(a.created_at).getTime() : 0;
|
|
207
|
+
const timeB = b.created_at ? new Date(b.created_at).getTime() : 0;
|
|
208
|
+
return timeA - timeB;
|
|
163
209
|
});
|
|
164
210
|
this.logger.info(
|
|
165
211
|
`Found ${documents.length} documents in session ${sessionId}`
|
|
@@ -167,7 +213,9 @@ class DocumentService {
|
|
|
167
213
|
return documents;
|
|
168
214
|
}
|
|
169
215
|
/**
|
|
170
|
-
*
|
|
216
|
+
* Remove a document's vector store entry.
|
|
217
|
+
* Note: the underlying file in the Files API is intentionally preserved
|
|
218
|
+
* so it can be re-associated (e.g., during rename/upsert).
|
|
171
219
|
* @param sessionId - Vector store ID
|
|
172
220
|
* @param documentTitle - Document title to delete
|
|
173
221
|
* @throws NotFoundError if document not found
|
|
@@ -176,7 +224,7 @@ class DocumentService {
|
|
|
176
224
|
this.logger.info(`Deleting document ${documentTitle} from ${sessionId}`);
|
|
177
225
|
const file = await this.findFileByTitle(sessionId, documentTitle);
|
|
178
226
|
if (!file) {
|
|
179
|
-
throw new errors.NotFoundError(`
|
|
227
|
+
throw new errors.NotFoundError(`Resource not found: ${documentTitle}`);
|
|
180
228
|
}
|
|
181
229
|
await this.client.vectorStores.files.delete(sessionId, file.id);
|
|
182
230
|
this.logger.info(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documentService.cjs.js","sources":["../../../../src/service/notebooks/documents/documentService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { ConflictError, NotFoundError } from '@backstage/errors';\n\nimport {\n DEFAULT_CHUNK_OVERLAP_TOKENS,\n DEFAULT_CHUNKING_STRATEGY_TYPE,\n DEFAULT_MAX_CHUNK_SIZE_TOKENS,\n} from '../../constant';\nimport { SessionDocument, UpsertResult } from '../types/notebooksTypes';\nimport { VectorStoresOperator } from '../VectorStoresOperator';\nimport { toFile } from './fileParser';\n\n/**\n * Service for managing documents within notebook sessions using File-Based API\n * Each session has its own dedicated vector store\n * Uses VectorStoresOperator to proxy through lightspeed-core\n */\nexport class DocumentService {\n private logger: LoggerService;\n private client: VectorStoresOperator;\n private chunkingStrategy: {\n type: string;\n static?: { max_chunk_size_tokens: number; chunk_overlap_tokens: number };\n };\n\n constructor(\n client: VectorStoresOperator,\n logger: LoggerService,\n config?: Config,\n ) {\n this.client = client;\n this.logger = logger;\n\n // Chunking strategy configuration\n const chunkingType =\n config?.getOptionalString(\n 'intelligent-assistant.notebooks.chunkingStrategy.type',\n ) || DEFAULT_CHUNKING_STRATEGY_TYPE;\n\n if (chunkingType === 'static') {\n this.chunkingStrategy = {\n type: 'static',\n static: {\n max_chunk_size_tokens:\n config?.getOptionalNumber(\n 'intelligent-assistant.notebooks.chunkingStrategy.maxChunkSizeTokens',\n ) || DEFAULT_MAX_CHUNK_SIZE_TOKENS,\n chunk_overlap_tokens:\n config?.getOptionalNumber(\n 'intelligent-assistant.notebooks.chunkingStrategy.chunkOverlapTokens',\n ) || DEFAULT_CHUNK_OVERLAP_TOKENS,\n },\n };\n } else {\n this.chunkingStrategy = { type: 'auto' };\n }\n }\n\n /**\n * Find a file by title in vector store\n * @param sessionId - Vector store ID\n * @param documentTitle - Document title to search for\n * @returns File object if found, null otherwise\n */\n async findFileByTitle(\n sessionId: string,\n documentTitle: string,\n ): Promise<any | null> {\n const filesResponse = await this.client.vectorStores.files.list(sessionId);\n return (\n filesResponse.data.find(\n (f: any) => f.attributes?.title === documentTitle,\n ) || null\n );\n }\n\n /**\n * Upload a file to the Files API\n * @param content - File content as string\n * @param title - File title/name\n * @returns File ID from the Files API\n * @throws Error if upload fails\n */\n async uploadFile(content: string, title: string): Promise<string> {\n try {\n // Determine MIME type from file type or default to text/plain\n const mimeType = 'text/plain';\n const txtFilename = `${title.replace(/\\.[^.]+$/, '')}.txt`;\n const file = await this.client.files.create({\n file: await toFile(Buffer.from(content, 'utf-8'), txtFilename, {\n type: mimeType,\n }),\n purpose: 'assistants',\n });\n\n this.logger.info(\n `File created - id: ${file.id}, filename: ${file.filename}`,\n );\n return file.id;\n } catch (error) {\n // Preserve the original error type and message\n if (error instanceof Error) {\n throw error;\n }\n // For non-Error objects, wrap with context\n throw new Error(`Failed to upload file: ${String(error)}`);\n }\n }\n\n /**\n * Upsert a document - create if it doesn't exist, update if it does\n * @param sessionId - Vector store ID\n * @param title - Original document title\n * @param fileType - Document source type (text, pdf, url, etc.)\n * @param fileId - File ID from Files API\n * @param newTitle - New title for rename operation (optional)\n * @returns Upsert result with document ID and status\n * @throws ConflictError if newTitle conflicts with existing document\n */\n async upsertDocument(\n sessionId: string,\n title: string,\n fileType: string,\n fileId: string,\n newTitle?: string,\n ): Promise<UpsertResult> {\n // Find existing file by document_id\n const existingFile = await this.findFileByTitle(sessionId, title);\n const createdAt =\n (existingFile?.attributes?.created_at as string) ||\n new Date().toISOString();\n\n if (newTitle && title !== newTitle) {\n // Check for title conflicts when renaming\n const conflictingFile = await this.findFileByTitle(sessionId, newTitle);\n\n if (conflictingFile) {\n throw new ConflictError(\n `A document with the title \"${newTitle || title}\" already exists in this session`,\n );\n }\n }\n if (existingFile) {\n await this.deleteDocument(sessionId, title);\n }\n\n const vectorStoreFile = await this.client.vectorStores.files.create(\n sessionId,\n {\n file_id: fileId,\n chunking_strategy: this.chunkingStrategy,\n attributes: {\n title: newTitle || title,\n source_type: fileType,\n created_at: createdAt,\n updated_at: new Date().toISOString(),\n },\n },\n );\n\n this.logger.info(\n `Document \"${newTitle || title}\" (ID: ${title}) upload started with file ${fileId}`,\n );\n\n return {\n document_id: newTitle || title,\n file_id: fileId,\n replaced: false,\n status: vectorStoreFile.status,\n };\n }\n\n /**\n * Get file processing status\n * @param sessionId - Vector store ID\n * @param documentTitle - Document title\n * @returns File status including processing state, chunk count, and error if any\n * @throws NotFoundError if document not found\n */\n async getFileStatus(\n sessionId: string,\n documentTitle: string,\n ): Promise<{\n status: 'in_progress' | 'completed' | 'failed' | 'cancelled';\n chunks_count: number;\n error?: string;\n }> {\n const file = await this.findFileByTitle(sessionId, documentTitle);\n\n if (!file) {\n throw new NotFoundError(`Document not found: ${documentTitle}`);\n }\n return {\n status: file.status,\n chunks_count: file.chunks_count,\n error: file.last_error?.message,\n };\n }\n\n /**\n * List all documents in a session\n * @param sessionId - Vector store ID\n * @param fileTypeFilter - Optional filter by source type (text, pdf, url, etc.)\n * @returns Array of session documents\n */\n async listDocuments(\n sessionId: string,\n fileTypeFilter?: string,\n ): Promise<SessionDocument[]> {\n this.logger.info(`Listing documents for session ${sessionId}`);\n\n // List all files in vector store\n const filesResponse = await this.client.vectorStores.files.list(sessionId);\n\n if (!filesResponse.data || filesResponse.data.length === 0) {\n return [];\n }\n\n // Map files to SessionDocument format\n const documents = filesResponse.data\n .filter((file: any) => {\n // Apply file type filter if provided\n if (fileTypeFilter && file.attributes?.source_type !== fileTypeFilter) {\n return false;\n }\n return true;\n })\n .map((file: any) => {\n const attrs = file.attributes || {};\n return {\n document_id: attrs.title,\n source_type:\n (attrs.source_type as SessionDocument['source_type']) || 'text',\n created_at: attrs.created_at,\n updated_at: attrs.updated_at,\n };\n });\n\n this.logger.info(\n `Found ${documents.length} documents in session ${sessionId}`,\n );\n return documents;\n }\n\n /**\n * Delete a document from the vector store and Files API\n * @param sessionId - Vector store ID\n * @param documentTitle - Document title to delete\n * @throws NotFoundError if document not found\n */\n async deleteDocument(\n sessionId: string,\n documentTitle: string,\n ): Promise<void> {\n this.logger.info(`Deleting document ${documentTitle} from ${sessionId}`);\n\n const file = await this.findFileByTitle(sessionId, documentTitle);\n\n if (!file) {\n throw new NotFoundError(`Document not found: ${documentTitle}`);\n }\n\n // Delete from vector store first\n await this.client.vectorStores.files.delete(sessionId, file.id);\n\n this.logger.info(\n `Deleted document ${documentTitle} (file ${file.id}) from session ${sessionId}`,\n );\n }\n}\n"],"names":["DEFAULT_CHUNKING_STRATEGY_TYPE","DEFAULT_MAX_CHUNK_SIZE_TOKENS","DEFAULT_CHUNK_OVERLAP_TOKENS","toFile","ConflictError","NotFoundError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"documentService.cjs.js","sources":["../../../../src/service/notebooks/documents/documentService.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport { ConflictError, NotFoundError } from '@backstage/errors';\n\nimport {\n DEFAULT_CHUNK_OVERLAP_TOKENS,\n DEFAULT_CHUNKING_STRATEGY_TYPE,\n DEFAULT_MAX_CHUNK_SIZE_TOKENS,\n} from '../../constant';\nimport { SessionDocument, UpsertResult } from '../types/notebooksTypes';\nimport { VectorStoresOperator } from '../VectorStoresOperator';\n\n/**\n * Service for managing documents within notebook sessions using File-Based API\n * Each session has its own dedicated vector store\n * Uses VectorStoresOperator to proxy through lightspeed-core\n */\nexport class DocumentService {\n private logger: LoggerService;\n private client: VectorStoresOperator;\n private chunkingStrategy: {\n type: string;\n static?: { max_chunk_size_tokens: number; chunk_overlap_tokens: number };\n };\n\n constructor(\n client: VectorStoresOperator,\n logger: LoggerService,\n config?: Config,\n ) {\n this.client = client;\n this.logger = logger;\n\n // Chunking strategy configuration\n const chunkingType =\n config?.getOptionalString(\n 'intelligent-assistant.notebooks.chunkingStrategy.type',\n ) || DEFAULT_CHUNKING_STRATEGY_TYPE;\n\n if (chunkingType === 'static') {\n this.chunkingStrategy = {\n type: 'static',\n static: {\n max_chunk_size_tokens:\n config?.getOptionalNumber(\n 'intelligent-assistant.notebooks.chunkingStrategy.maxChunkSizeTokens',\n ) || DEFAULT_MAX_CHUNK_SIZE_TOKENS,\n chunk_overlap_tokens:\n config?.getOptionalNumber(\n 'intelligent-assistant.notebooks.chunkingStrategy.chunkOverlapTokens',\n ) || DEFAULT_CHUNK_OVERLAP_TOKENS,\n },\n };\n } else {\n this.chunkingStrategy = { type: 'auto' };\n }\n }\n\n /**\n * Find a file by title in vector store\n * @param sessionId - Vector store ID\n * @param documentTitle - Document title to search for\n * @returns File object if found, null otherwise\n */\n async findFileByTitle(\n sessionId: string,\n documentTitle: string,\n ): Promise<any | null> {\n const filesResponse = await this.client.vectorStores.files.list(sessionId);\n return (\n filesResponse.data.find(\n (f: any) => f.attributes?.title === documentTitle,\n ) || null\n );\n }\n\n /**\n * Upload markdown content to the Files API\n * @param content - Markdown content string\n * @param title - File title/name\n * @returns File ID from the Files API\n * @throws Error if upload fails\n */\n async uploadFile(content: string, title: string): Promise<string> {\n try {\n const mdFilename = `${title.replace(/\\.[^.]+$/, '')}.md`;\n\n const file = await this.client.files.create({\n file: {\n name: mdFilename,\n buffer: Buffer.from(content, 'utf-8'),\n type: 'text/markdown',\n },\n purpose: 'assistants',\n });\n\n this.logger.info(\n `File created - id: ${file.id}, filename: ${file.filename}`,\n );\n return file.id;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n }\n throw new Error(`Failed to upload file: ${String(error)}`);\n }\n }\n\n /**\n * Upsert a document - create if it doesn't exist, update if it does.\n * Also used for rename-only operations when fileType/fileId are omitted.\n * @param sessionId - Vector store ID\n * @param title - Original document title\n * @param opts - Optional parameters for upload or rename\n * @param opts.fileType - Document source type (derived from existing file when omitted)\n * @param opts.fileId - File ID from Files API (derived from existing file when omitted)\n * @param opts.newTitle - New title for rename operation\n * @returns Upsert result with document ID and status\n * @throws NotFoundError if fileId is omitted and document does not exist\n * @throws ConflictError if newTitle conflicts with existing document\n */\n async upsertDocument(\n sessionId: string,\n title: string,\n opts?: {\n fileType?: string;\n fileId?: string;\n newTitle?: string;\n },\n ): Promise<UpsertResult> {\n const { fileType, fileId, newTitle } = opts || {};\n const existingFile = await this.findFileByTitle(sessionId, title);\n\n // Rename-only mode: no fileId means we must have an existing file\n let resolvedFileId: string;\n let resolvedFileType: string;\n if (fileId) {\n resolvedFileId = fileId;\n resolvedFileType = fileType || 'unknown';\n } else {\n if (!existingFile) {\n throw new NotFoundError(`Document not found: ${title}`);\n }\n resolvedFileId = existingFile.id;\n resolvedFileType =\n fileType ||\n (existingFile.attributes?.source_type as string) ||\n 'unknown';\n }\n\n const createdAt =\n (existingFile?.attributes?.created_at as string) ||\n new Date().toISOString();\n\n if (newTitle && title !== newTitle) {\n const conflictingFile = await this.findFileByTitle(sessionId, newTitle);\n if (conflictingFile) {\n throw new ConflictError(\n `A resource with the title \"${newTitle || title}\" already exists in this session`,\n );\n }\n }\n\n if (existingFile) {\n await this.deleteDocument(sessionId, title);\n }\n\n // Preserve existing attributes, override with known fields\n const baseAttrs = existingFile?.attributes || {};\n const attributes = {\n ...baseAttrs,\n title: newTitle || title,\n source_type: resolvedFileType,\n created_at: createdAt,\n updated_at: new Date().toISOString(),\n };\n\n try {\n const vectorStoreFile = await this.client.vectorStores.files.create(\n sessionId,\n {\n file_id: resolvedFileId,\n chunking_strategy: this.chunkingStrategy,\n attributes,\n },\n );\n\n this.logger.info(\n `Document \"${newTitle || title}\" (ID: ${title}) upsert started with file ${resolvedFileId}`,\n );\n\n return {\n document_id: newTitle || title,\n file_id: resolvedFileId,\n replaced: !!existingFile,\n status: vectorStoreFile.status,\n };\n } catch (error) {\n // Rollback: restore original entry if delete succeeded but create failed\n if (existingFile) {\n this.logger.error(\n `Failed to re-create vector store entry after delete for \"${title}\". Attempting rollback.`,\n );\n try {\n await this.client.vectorStores.files.create(sessionId, {\n file_id: existingFile.id,\n chunking_strategy: this.chunkingStrategy,\n attributes: existingFile.attributes || {},\n });\n this.logger.info(\n `Rollback succeeded: restored \"${title}\" in session ${sessionId}`,\n );\n } catch (rollbackError) {\n this.logger.error(\n `Rollback failed: document \"${title}\" (file ${existingFile.id}) is orphaned in session ${sessionId}`,\n );\n }\n }\n throw error;\n }\n }\n\n /**\n * Get file processing status\n * @param sessionId - Vector store ID\n * @param documentTitle - Document title\n * @returns File status including processing state, chunk count, and error if any\n * @throws NotFoundError if document not found\n */\n async getFileStatus(\n sessionId: string,\n documentTitle: string,\n ): Promise<{\n status: 'in_progress' | 'completed' | 'failed' | 'cancelled';\n chunks_count: number;\n error?: string;\n }> {\n const file = await this.findFileByTitle(sessionId, documentTitle);\n\n if (!file) {\n throw new NotFoundError(`Resource not found: ${documentTitle}`);\n }\n return {\n status: file.status,\n chunks_count: file.chunks_count,\n error: file.last_error?.message,\n };\n }\n\n /**\n * List all documents in a session\n * @param sessionId - Vector store ID\n * @param fileTypeFilter - Optional filter by source type (text, pdf, url, etc.)\n * @returns Array of session documents\n */\n async listDocuments(\n sessionId: string,\n fileTypeFilter?: string,\n ): Promise<SessionDocument[]> {\n this.logger.info(`Listing documents for session ${sessionId}`);\n\n // List all files in vector store\n const filesResponse = await this.client.vectorStores.files.list(sessionId);\n\n if (!filesResponse.data || filesResponse.data.length === 0) {\n return [];\n }\n\n // Map files to SessionDocument format, sorted by original upload time\n const documents = filesResponse.data\n .filter((file: any) => {\n if (fileTypeFilter && file.attributes?.source_type !== fileTypeFilter) {\n return false;\n }\n return true;\n })\n .map((file: any) => {\n const attrs = file.attributes || {};\n return {\n document_id: attrs.title,\n source_type:\n (attrs.source_type as SessionDocument['source_type']) || 'text',\n created_at: attrs.created_at,\n updated_at: attrs.updated_at,\n };\n })\n .sort((a: SessionDocument, b: SessionDocument) => {\n const timeA = a.created_at ? new Date(a.created_at).getTime() : 0;\n const timeB = b.created_at ? new Date(b.created_at).getTime() : 0;\n return timeA - timeB;\n });\n\n this.logger.info(\n `Found ${documents.length} documents in session ${sessionId}`,\n );\n return documents;\n }\n\n /**\n * Remove a document's vector store entry.\n * Note: the underlying file in the Files API is intentionally preserved\n * so it can be re-associated (e.g., during rename/upsert).\n * @param sessionId - Vector store ID\n * @param documentTitle - Document title to delete\n * @throws NotFoundError if document not found\n */\n async deleteDocument(\n sessionId: string,\n documentTitle: string,\n ): Promise<void> {\n this.logger.info(`Deleting document ${documentTitle} from ${sessionId}`);\n\n const file = await this.findFileByTitle(sessionId, documentTitle);\n\n if (!file) {\n throw new NotFoundError(`Resource not found: ${documentTitle}`);\n }\n\n // Delete from vector store first\n await this.client.vectorStores.files.delete(sessionId, file.id);\n\n this.logger.info(\n `Deleted document ${documentTitle} (file ${file.id}) from session ${sessionId}`,\n );\n }\n}\n"],"names":["DEFAULT_CHUNKING_STRATEGY_TYPE","DEFAULT_MAX_CHUNK_SIZE_TOKENS","DEFAULT_CHUNK_OVERLAP_TOKENS","NotFoundError","ConflictError"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('@backstage/errors');
|
|
4
|
+
var markitdownTs = require('markitdown-ts');
|
|
5
|
+
|
|
6
|
+
const markitdown = new markitdownTs.MarkItDown();
|
|
7
|
+
const PLAINTEXT_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".yaml", ".yml", ".log"]);
|
|
8
|
+
async function convertToMarkdown(buffer, originalName, fileType) {
|
|
9
|
+
const normalizedFileType = fileType.toLowerCase();
|
|
10
|
+
const ext = `.${normalizedFileType}`;
|
|
11
|
+
const nameExt = originalName.includes(".") ? `.${originalName.split(".").pop().toLowerCase()}` : "";
|
|
12
|
+
const normalizeExt = (e) => e === ".yml" ? ".yaml" : e;
|
|
13
|
+
if (nameExt && normalizeExt(nameExt) !== normalizeExt(ext)) {
|
|
14
|
+
throw new errors.InputError(
|
|
15
|
+
`File extension "${nameExt}" does not match declared file type "${fileType}"`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (PLAINTEXT_EXTENSIONS.has(ext)) {
|
|
19
|
+
return buffer.toString("utf-8");
|
|
20
|
+
}
|
|
21
|
+
const result = await markitdown.convertBuffer(buffer, {
|
|
22
|
+
file_extension: ext
|
|
23
|
+
});
|
|
24
|
+
if (!result?.markdown) {
|
|
25
|
+
throw new errors.InputError(
|
|
26
|
+
`Markdown conversion produced no output for ${originalName}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return result.markdown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.convertToMarkdown = convertToMarkdown;
|
|
33
|
+
//# sourceMappingURL=markitdownClient.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markitdownClient.cjs.js","sources":["../../../../src/service/notebooks/documents/markitdownClient.ts"],"sourcesContent":["/*\n * Copyright Red Hat, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { InputError } from '@backstage/errors';\n\nimport { MarkItDown } from 'markitdown-ts';\n\nconst markitdown = new MarkItDown();\n\nconst PLAINTEXT_EXTENSIONS = new Set(['.json', '.yaml', '.yml', '.log']);\n\n/**\n * Convert a document buffer to markdown using markitdown-ts.\n * Plain-text formats (json, yaml, log) are passed through as-is.\n */\nexport async function convertToMarkdown(\n buffer: Buffer,\n originalName: string,\n fileType: string,\n): Promise<string> {\n const normalizedFileType = fileType.toLowerCase();\n const ext = `.${normalizedFileType}`;\n\n const nameExt = originalName.includes('.')\n ? `.${originalName.split('.').pop()!.toLowerCase()}`\n : '';\n // Treat .yml and .yaml as equivalent\n const normalizeExt = (e: string) => (e === '.yml' ? '.yaml' : e);\n if (nameExt && normalizeExt(nameExt) !== normalizeExt(ext)) {\n throw new InputError(\n `File extension \"${nameExt}\" does not match declared file type \"${fileType}\"`,\n );\n }\n\n if (PLAINTEXT_EXTENSIONS.has(ext)) {\n return buffer.toString('utf-8');\n }\n\n const result = await markitdown.convertBuffer(buffer, {\n file_extension: ext,\n });\n\n if (!result?.markdown) {\n throw new InputError(\n `Markdown conversion produced no output for ${originalName}`,\n );\n }\n\n return result.markdown;\n}\n"],"names":["MarkItDown","InputError"],"mappings":";;;;;AAoBA,MAAM,UAAA,GAAa,IAAIA,uBAAA,EAAW;AAElC,MAAM,oBAAA,uBAA2B,GAAA,CAAI,CAAC,SAAS,OAAA,EAAS,MAAA,EAAQ,MAAM,CAAC,CAAA;AAMvE,eAAsB,iBAAA,CACpB,MAAA,EACA,YAAA,EACA,QAAA,EACiB;AACjB,EAAA,MAAM,kBAAA,GAAqB,SAAS,WAAA,EAAY;AAChD,EAAA,MAAM,GAAA,GAAM,IAAI,kBAAkB,CAAA,CAAA;AAElC,EAAA,MAAM,OAAA,GAAU,YAAA,CAAa,QAAA,CAAS,GAAG,IACrC,CAAA,CAAA,EAAI,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,EAAI,CAAG,WAAA,EAAa,CAAA,CAAA,GAChD,EAAA;AAEJ,EAAA,MAAM,YAAA,GAAe,CAAC,CAAA,KAAe,CAAA,KAAM,SAAS,OAAA,GAAU,CAAA;AAC9D,EAAA,IAAI,WAAW,YAAA,CAAa,OAAO,CAAA,KAAM,YAAA,CAAa,GAAG,CAAA,EAAG;AAC1D,IAAA,MAAM,IAAIC,iBAAA;AAAA,MACR,CAAA,gBAAA,EAAmB,OAAO,CAAA,qCAAA,EAAwC,QAAQ,CAAA,CAAA;AAAA,KAC5E;AAAA,EACF;AAEA,EAAA,IAAI,oBAAA,CAAqB,GAAA,CAAI,GAAG,CAAA,EAAG;AACjC,IAAA,OAAO,MAAA,CAAO,SAAS,OAAO,CAAA;AAAA,EAChC;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,UAAA,CAAW,aAAA,CAAc,MAAA,EAAQ;AAAA,IACpD,cAAA,EAAgB;AAAA,GACjB,CAAA;AAED,EAAA,IAAI,CAAC,QAAQ,QAAA,EAAU;AACrB,IAAA,MAAM,IAAIA,iBAAA;AAAA,MACR,8CAA8C,YAAY,CAAA;AAAA,KAC5D;AAAA,EACF;AAEA,EAAA,OAAO,MAAA,CAAO,QAAA;AAChB;;;;"}
|