@red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend 3.1.0 → 4.0.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 +89 -0
- package/README.md +29 -21
- package/dist/service/attachment-validation.cjs.js +30 -0
- package/dist/service/attachment-validation.cjs.js.map +1 -0
- package/dist/service/constant.cjs.js +7 -1
- package/dist/service/constant.cjs.js.map +1 -1
- package/dist/service/notebooks/documents/documentService.cjs.js +77 -29
- package/dist/service/notebooks/documents/documentService.cjs.js.map +1 -1
- package/dist/service/notebooks/notebooksRouters.cjs.js +48 -18
- package/dist/service/notebooks/notebooksRouters.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +150 -15
- package/dist/service/router.cjs.js.map +1 -1
- package/dist/service/validation.cjs.js +59 -6
- package/dist/service/validation.cjs.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# @red-hat-developer-hub/backstage-plugin-intelligent-assistant-backend
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- adb65c9: Breaking changes to MCP permissions using behavior-linked vocabulary rather than CRUD-linked vocabulary:
|
|
8
|
+
|
|
9
|
+
| Before (Lightspeed) | Before (Intelligent Assistant) | After |
|
|
10
|
+
| ----------------------- | ---------------------------------- | ------------------ |
|
|
11
|
+
| `lightspeed.mcp.read` | `intelligent-assistant.mcp.read` | `mcp.tools.use` |
|
|
12
|
+
| `lightspeed.mcp.manage` | `intelligent-assistant.mcp.manage` | `mcp.tools.manage` |
|
|
13
|
+
|
|
14
|
+
Removed permission CRUD action attributes; RBAC entries for these permission sets now use the generic `use` action.
|
|
15
|
+
|
|
16
|
+
Permission variable renamed from `iaMcpReadPermission` to `iaMcpUsePermission`; `iaMcpManagePermission` keeps its name.
|
|
17
|
+
|
|
18
|
+
Plugin documentation and example RBAC policy CSV updated to reflect the new MCP permission names.
|
|
19
|
+
|
|
20
|
+
- 0ed3adc: Breaking changes to the notebooks permissions model that uses behavior-linked vocabulary:
|
|
21
|
+
|
|
22
|
+
| Before (Lightspeed) | Before (Intelligent Assistant) | After |
|
|
23
|
+
| -------------------------- | ------------------------------------- | ---------------------------------------- |
|
|
24
|
+
| `lightspeed.notebooks.use` | `intelligent-assistant.notebooks.use` | `intelligent-assistant.notebooks.use` |
|
|
25
|
+
| | | `intelligent-assistant.notebooks.manage` |
|
|
26
|
+
- `notebooks.use` covers list/read/create session, upload document, and query endpoints
|
|
27
|
+
- `notebooks.manage` covers update/delete session and document endpoints
|
|
28
|
+
|
|
29
|
+
Removed permission CRUD action attributes; RBAC entries for notebooks permission sets now use the generic `use` action.
|
|
30
|
+
|
|
31
|
+
Hard-coded permission names were replaced by constants from the permission entities.
|
|
32
|
+
|
|
33
|
+
Plugin documentation and example RBAC policy CSV updated to reflect the notebooks permission model.
|
|
34
|
+
|
|
35
|
+
- f1dba9b: Breaking changes to the permissions model that uses behavior-linked vocabulary rather than CRUD-linked vocabulary:
|
|
36
|
+
|
|
37
|
+
| Before (Lightspeed) | Before (Intelligent Assistant) | After |
|
|
38
|
+
| ------------------------ | ----------------------------------- | ----------------------------------- |
|
|
39
|
+
| `lightspeed.chat.read` | `intelligent-assistant.chat.read` | `intelligent-assistant.chat.access` |
|
|
40
|
+
| `lightspeed.chat.create` | `intelligent-assistant.chat.create` | `intelligent-assistant.chat.use` |
|
|
41
|
+
| `lightspeed.chat.delete` | `intelligent-assistant.chat.delete` | `intelligent-assistant.chat.manage` |
|
|
42
|
+
| `lightspeed.chat.update` | `intelligent-assistant.chat.update` | `intelligent-assistant.chat.manage` |
|
|
43
|
+
|
|
44
|
+
Changes are applied to `lightspeed` (Frontend), `lightspeed-backend`, and `lightspeed-common` plugins.
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
Any hard-coded permission names have been replaced by constants, source references permission names from the permission entities.
|
|
49
|
+
|
|
50
|
+
`Trans.test.tsx` component test unit has permission names to reflect Intelligent Assistant for RHDH.
|
|
51
|
+
|
|
52
|
+
Additionally, hard-coded permission names have been replaced by local constants with the new names set.
|
|
53
|
+
|
|
54
|
+
Plugin documentation changes to revise information to permissions model changes to Intelligent Assistant for RHDH.
|
|
55
|
+
|
|
56
|
+
Changed permission variable 'lightspeed' prefix to 'ia' to use Intelligent Assistant rebranding.
|
|
57
|
+
|
|
58
|
+
Changes to example RBAC policy CSV file to reflect Intelligent Assistant for RHDH.
|
|
59
|
+
|
|
60
|
+
### Minor Changes
|
|
61
|
+
|
|
62
|
+
- 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
|
|
63
|
+
- 0f32982: Deep context inteeligent-assistant model vision verification for 500 is not cached anymore
|
|
64
|
+
- 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
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- 82742f0: Unify notebook terminology from "document" to "resource" across all UI strings, translations, and backend messages.
|
|
69
|
+
- Updated dependencies [3d1d7d7]
|
|
70
|
+
- Updated dependencies [adb65c9]
|
|
71
|
+
- Updated dependencies [0ed3adc]
|
|
72
|
+
- Updated dependencies [f1dba9b]
|
|
73
|
+
- @red-hat-developer-hub/backstage-plugin-intelligent-assistant-common@4.0.0
|
|
74
|
+
|
|
75
|
+
## 3.2.0
|
|
76
|
+
|
|
77
|
+
### Minor Changes
|
|
78
|
+
|
|
79
|
+
- 81b7ed2: add saved prompts endpoint
|
|
80
|
+
- 9d2012f: add model-validation route to verify whether a model is multimodal. Can now attach images to models that support it.
|
|
81
|
+
|
|
82
|
+
### Patch Changes
|
|
83
|
+
|
|
84
|
+
- 21e3f43: Require `intelligent-assistant.chat.update` on `PUT /v2/conversations/:conversation_id` instead of the `intelligent-assistant.chat.create` permission.
|
|
85
|
+
- e0d0986: Updated dependency `prettier` to `3.9.6`.
|
|
86
|
+
- 8966faf: Updated dependency `prettier` to `3.9.5`.
|
|
87
|
+
- Updated dependencies [81b7ed2]
|
|
88
|
+
- Updated dependencies [e0d0986]
|
|
89
|
+
- Updated dependencies [8966faf]
|
|
90
|
+
- @red-hat-developer-hub/backstage-plugin-intelligent-assistant-common@3.2.0
|
|
91
|
+
|
|
3
92
|
## 3.1.0
|
|
4
93
|
|
|
5
94
|
### Patch 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
|
|
|
@@ -117,11 +118,11 @@ dynamicPlugins:
|
|
|
117
118
|
red-hat-developer-hub.backstage-plugin-intelligent-assistant:
|
|
118
119
|
dynamicRoutes:
|
|
119
120
|
- path: /intelligent-assistant
|
|
120
|
-
importName: LightspeedPage
|
|
121
|
+
importName: LightspeedPage # unchanged
|
|
121
122
|
module: Legacy
|
|
122
123
|
mountPoints:
|
|
123
124
|
- mountPoint: application/internal/drawer-content
|
|
124
|
-
importName: LightspeedChatContainer
|
|
125
|
+
importName: LightspeedChatContainer # unchanged
|
|
125
126
|
module: Legacy
|
|
126
127
|
config:
|
|
127
128
|
id: intelligent-assistant
|
|
@@ -332,17 +333,20 @@ 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,
|
|
345
|
+
p, role:default/team_a, mcp.tools.use, use, allow
|
|
346
|
+
p, role:default/team_a, mcp.tools.manage, use, allow
|
|
347
|
+
|
|
348
|
+
# Required for saved prompts
|
|
349
|
+
p, role:default/team_a, intelligent-assistant.saved-prompts.manage, update, allow
|
|
346
350
|
|
|
347
351
|
g, user:default/<your-user-name>, role:default/team_a
|
|
348
352
|
|
|
@@ -446,6 +450,7 @@ When enabled, Notebooks exposes the following REST API endpoints:
|
|
|
446
450
|
- `PUT /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents` - Upload or update a document (multipart/form-data)
|
|
447
451
|
- `GET /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents` - List all documents in a session
|
|
448
452
|
- `GET /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents/:documentId/status` - Get document processing status
|
|
453
|
+
- `PATCH /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents/:documentId` - Rename a document (JSON body: `{ "title": "new name" }`)
|
|
449
454
|
- `DELETE /intelligent-assistant/notebooks/v1/sessions/:sessionId/documents/:documentId` - Delete a document
|
|
450
455
|
|
|
451
456
|
- **Queries**:
|
|
@@ -454,16 +459,19 @@ When enabled, Notebooks exposes the following REST API endpoints:
|
|
|
454
459
|
**Notes**:
|
|
455
460
|
|
|
456
461
|
- All endpoints require authentication (user context is automatically provided by Backstage)
|
|
457
|
-
- All `/v1/*` endpoints require
|
|
462
|
+
- All `/v1/*` endpoints require notebooks permissions:
|
|
463
|
+
- `intelligent-assistant.notebooks.use` for list/read/create session, upload document, and query endpoints
|
|
464
|
+
- `intelligent-assistant.notebooks.manage` for update/delete session and document endpoints
|
|
458
465
|
- Document endpoints verify session ownership before allowing operations
|
|
459
466
|
- `documentId` in paths is the document title (URL-encoded for special characters)
|
|
460
467
|
|
|
461
468
|
#### Permission Framework Support for Notebooks
|
|
462
469
|
|
|
463
|
-
When RBAC is enabled, users need the following
|
|
470
|
+
When RBAC is enabled, users need the following permissions to use Notebooks:
|
|
464
471
|
|
|
465
472
|
```CSV
|
|
466
|
-
p, role:default/team_a, intelligent-assistant.notebooks.use,
|
|
473
|
+
p, role:default/team_a, intelligent-assistant.notebooks.use, use, allow
|
|
474
|
+
p, role:default/team_a, intelligent-assistant.notebooks.manage, use, allow
|
|
467
475
|
|
|
468
476
|
g, user:default/<your-user-name>, role:default/team_a
|
|
469
477
|
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
4
|
+
const ModelCapabilitiesCache = {
|
|
5
|
+
cache: {},
|
|
6
|
+
get(model) {
|
|
7
|
+
const entry = this.cache[model];
|
|
8
|
+
if (!entry) return void 0;
|
|
9
|
+
if (Date.now() > entry.expiry) {
|
|
10
|
+
delete this.cache[model];
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
return entry.value;
|
|
14
|
+
},
|
|
15
|
+
set(model, supportsVision) {
|
|
16
|
+
this.cache[model] = {
|
|
17
|
+
value: supportsVision,
|
|
18
|
+
expiry: Date.now() + CACHE_TTL_MS
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
has(model) {
|
|
22
|
+
return this.get(model) !== void 0;
|
|
23
|
+
},
|
|
24
|
+
clear() {
|
|
25
|
+
this.cache = {};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.ModelCapabilitiesCache = ModelCapabilitiesCache;
|
|
30
|
+
//# sourceMappingURL=attachment-validation.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-validation.cjs.js","sources":["../../src/service/attachment-validation.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\nconst CACHE_TTL_MS = 24 * 60 * 60 * 1000;\n\nexport const ModelCapabilitiesCache = {\n cache: {} as Record<string, { value: boolean; expiry: number }>,\n\n get(model: string): boolean | undefined {\n const entry = this.cache[model];\n if (!entry) return undefined;\n if (Date.now() > entry.expiry) {\n delete this.cache[model];\n return undefined;\n }\n return entry.value;\n },\n\n set(model: string, supportsVision: boolean): void {\n this.cache[model] = {\n value: supportsVision,\n expiry: Date.now() + CACHE_TTL_MS,\n };\n },\n\n has(model: string): boolean {\n return this.get(model) !== undefined;\n },\n\n clear(): void {\n this.cache = {};\n },\n};\n"],"names":[],"mappings":";;AAgBA,MAAM,YAAA,GAAe,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,GAAA;AAE7B,MAAM,sBAAA,GAAyB;AAAA,EACpC,OAAO,EAAC;AAAA,EAER,IAAI,KAAA,EAAoC;AACtC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAC9B,IAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,IAAA,IAAI,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,MAAA,EAAQ;AAC7B,MAAA,OAAO,IAAA,CAAK,MAAM,KAAK,CAAA;AACvB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,OAAO,KAAA,CAAM,KAAA;AAAA,EACf,CAAA;AAAA,EAEA,GAAA,CAAI,OAAe,cAAA,EAA+B;AAChD,IAAA,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA,GAAI;AAAA,MAClB,KAAA,EAAO,cAAA;AAAA,MACP,MAAA,EAAQ,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,KACvB;AAAA,EACF,CAAA;AAAA,EAEA,IAAI,KAAA,EAAwB;AAC1B,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA,KAAM,MAAA;AAAA,EAC7B,CAAA;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,QAAQ,EAAC;AAAA,EAChB;AACF;;;;"}
|
|
@@ -80,8 +80,13 @@ var SupportedFileType = /* @__PURE__ */ ((SupportedFileType2) => {
|
|
|
80
80
|
SupportedFileType2["LOG"] = "log";
|
|
81
81
|
return SupportedFileType2;
|
|
82
82
|
})(SupportedFileType || {});
|
|
83
|
-
const SKIP_USER_ID_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
83
|
+
const SKIP_USER_ID_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
84
|
+
"/v1/models",
|
|
85
|
+
"/v1/shields",
|
|
86
|
+
"/v1/saved-prompts/config"
|
|
87
|
+
]);
|
|
84
88
|
const DEFAULT_HISTORY_LENGTH = 10;
|
|
89
|
+
const TEST_VISION_JPEG = "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCwAA//2Q==";
|
|
85
90
|
|
|
86
91
|
exports.DEFAULT_CHUNKING_STRATEGY_TYPE = DEFAULT_CHUNKING_STRATEGY_TYPE;
|
|
87
92
|
exports.DEFAULT_CHUNK_OVERLAP_TOKENS = DEFAULT_CHUNK_OVERLAP_TOKENS;
|
|
@@ -103,5 +108,6 @@ exports.NOTEBOOKS_SYSTEM_PROMPT = NOTEBOOKS_SYSTEM_PROMPT;
|
|
|
103
108
|
exports.RATE_LIMIT_WINDOW_MS = RATE_LIMIT_WINDOW_MS;
|
|
104
109
|
exports.SKIP_USER_ID_ENDPOINTS = SKIP_USER_ID_ENDPOINTS;
|
|
105
110
|
exports.SupportedFileType = SupportedFileType;
|
|
111
|
+
exports.TEST_VISION_JPEG = TEST_VISION_JPEG;
|
|
106
112
|
exports.upload = upload;
|
|
107
113
|
//# sourceMappingURL=constant.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constant.cjs.js","sources":["../../src/service/constant.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 multer from 'multer';\n\n/**\n * Default values for AI Notebooks\n */\nexport const DEFAULT_CHUNKING_STRATEGY_TYPE = 'auto'; // auto chunking\nexport const DEFAULT_MAX_CHUNK_SIZE_TOKENS = 512; // 512 tokens\nexport const DEFAULT_CHUNK_OVERLAP_TOKENS = 50; // 50 tokens\nexport const DEFAULT_LIGHTSPEED_SERVICE_HOST = '127.0.0.1'; // Lightspeed core service host\nexport const DEFAULT_LIGHTSPEED_SERVICE_PORT = 8080; // Lightspeed service port\nexport const DEFAULT_MAX_FILE_SIZE_MB = 20 * 1024 * 1024; // 20MB\n\n/**\n * Rate limiting defaults (window is fixed at 1 minute)\n */\nexport const RATE_LIMIT_WINDOW_MS = 60000;\nexport const DEFAULT_EXPENSIVE_RATE_LIMIT_MAX = 25;\nexport const DEFAULT_GENERAL_RATE_LIMIT_MAX = 200;\n\n/**\n * Input validation limits for query endpoints\n */\nexport const MAX_QUERY_LENGTH = 32000; // 32K characters (reasonable for LLM context)\nexport const MAX_ATTACHMENT_SIZE_BYTES = 20 * 1024 * 1024; // 20MB (matches notebooks limit)\nexport const MAX_TOTAL_ATTACHMENTS_SIZE_BYTES = 50 * 1024 * 1024; // 50MB total\nexport const EXPRESS_JSON_BODY_LIMIT = '60mb';\n\nexport const NOTEBOOKS_SYSTEM_PROMPT = `\nYou are a helpful, analytical Research Analyst assistant. Your primary objective is to synthesize cross-document information to answer user queries with 100% fidelity to the provided documents.\n\n### QUERY TYPES - IMPORTANT\n* **Meta Queries ONLY:** ONLY when the user asks specifically about YOU as an assistant (e.g., \"who are you\", \"what can you do\", \"hello\"), respond naturally without requiring documents.\n* **ALL OTHER QUERIES:** For ANY other question, you MUST use the strict document-grounding rules below. This includes general knowledge questions, trivia, explanations, etc. If it's not about you as an assistant, it requires document evidence.\n\n### STRICT OPERATIONAL CONSTRAINTS\n* **Zero Outside Knowledge:** Do NOT use prior training data, general knowledge, or unsupported logical leaps to answer queries.\n* **Absolute Grounding:** If the provided documents do not contain explicit, direct evidence to answer the query, you MUST output exactly: \"I cannot answer this based on the provided documents.\"\n* **Precision Citations:** Every single factual claim, metric, or conclusion must have an inline citation [Document Title].\n* **Contradictions:** Do not resolve discrepancies. If sources conflict, explicitly document the friction (e.g., \"Source A states X, whereas Source B states Y\").\n\n### ANALYTICAL GUIDELINES\n* **Comprehensive Responses:** Provide thorough, detailed analysis. Don't be overly brief - expand on the evidence with full context and explanation.\n* **Quantitative Focus:** Prioritize extracting specific metrics, dates, and figures.\n* **Objective Tone:** Use neutral, professional language. Do not use subjective adjectives (e.g., \"impressive,\" \"concerning\") unless quoting the text directly.\n\n### REQUIRED ANALYSIS PROCESS\nBefore generating your response, you must internally perform evidence extraction (DO NOT show this in your output):\n1. Identify the core entities and requirements of the user's query.\n2. Extract exact, verbatim quotes from the provided documents that directly address the query.\n3. If no explicit quotes exist to answer the prompt, output ONLY: \"I cannot answer this based on the provided documents.\"\n\n### CRITICAL: NEVER output <evidence_extraction> tags or any internal reasoning in your visible response. These are for your internal analysis only.\n\n### REQUIRED OUTPUT STRUCTURE\nWhen you have evidence from documents, structure your response as:\n\n**Executive Summary:**\n[A comprehensive 2-4 sentence synthesis of the primary findings based strictly on the extracted evidence. Provide full context and detail.]\n\n**Detailed Analysis:**\n* **[Key Entity/Theme]:** [Thorough explanation of the fact or data point derived from text, with full context and supporting details] [Document Title].\n* **[Key Entity/Theme]:** [Thorough explanation of the fact or data point derived from text, with full context and supporting details] [Document Title].\n\n**Referenced Documents:**\n* [Document Title 1]\n* [Document Title 2]\n\nWhen you lack evidence, output ONLY: \"I cannot answer this based on the provided documents.\"\n`.trim();\n\n/**\n * HTTP and networking constants\n * @reserved Reserved for future URL file type support\n */\nexport const URL_FETCH_TIMEOUT_MS = 30000; // 30 second timeout for URL fetching\nexport const USER_AGENT = 'RHDH-Notebooks-Bot/1.0'; // User agent for HTTP requests\nexport const MAX_URL_CONTENT_SIZE = 10 * 1024 * 1024; // 10MB max for URL fetched content\n\n/**\n * HTTP status codes\n */\nexport const HTTP_STATUS_ACCEPTED = 202; // Async operation accepted\nexport const HTTP_STATUS_BAD_REQUEST = 400; // Bad request\nexport const HTTP_STATUS_FORBIDDEN = 403; // Forbidden\nexport const HTTP_STATUS_NOT_FOUND = 404; // Not found\nexport const HTTP_STATUS_CONFLICT = 409; // Conflict\nexport const HTTP_STATUS_INTERNAL_ERROR = 500; // Internal server error\n\n/**\n * SSRF Protection - Blocked hostnames for security\n * These hostnames are commonly used for Server-Side Request Forgery attacks\n * @reserved Reserved for future URL file type support\n */\nexport const SSRF_BLOCKED_HOSTNAMES = [\n 'localhost',\n 'metadata.google.internal', // GCP metadata endpoint\n 'kubernetes.default.svc', // Kubernetes internal DNS\n 'host.docker.internal', // Docker host access\n '169.254.169.254', // NOSONAR local netwok, used by AWS/Azure/GCP metadata IP\n '127.0.0.1', // IPv4 loopback\n '0.0.0.0', // IPv4 any address\n '::1', // IPv6 loopback\n '::', // IPv6 any address\n] as const;\n\n/**\n * Prompt Injection Protection - Patterns to detect and sanitize\n * These patterns are commonly used in prompt injection attacks\n * @reserved Reserved for future URL file type support\n */\nexport const PROMPT_INJECTION_PATTERNS = [\n /ignore\\s+(all\\s+)?previous\\s+(instructions?|prompts?)/gi,\n /disregard\\s+(all\\s+)?previous\\s+(instructions?|prompts?)/gi,\n /forget\\s+(all\\s+)?previous\\s+(instructions?|prompts?)/gi,\n /you\\s+are\\s+now\\s+(a\\s+)?different/gi,\n /new\\s+(instructions?|prompts?)\\s*:/gi,\n /system\\s*:\\s*/gi,\n /assistant\\s*:\\s*/gi,\n /\\[INST\\]/gi,\n /\\[\\/INST\\]/gi,\n /<\\|im_start\\|>/gi,\n /<\\|im_end\\|>/gi,\n /<\\|endoftext\\|>/gi,\n /\\[SYSTEM\\]/gi,\n /\\[\\/SYSTEM\\]/gi,\n /\\[ASSISTANT\\]/gi,\n /\\[\\/ASSISTANT\\]/gi,\n] as const;\n\n/**\n * Content sanitization constants\n * @reserved Reserved for future URL file type support\n */\nexport const MAX_CONSECUTIVE_NEWLINES = 4; // Max consecutive newlines allowed in content\nexport const FILTERED_CONTENT_MARKER = '[CONTENT_FILTERED]'; // Marker for filtered content\n\n/**\n * File type to MIME type mapping\n */\nexport const FILE_TYPE_TO_MIME: Record<string, string> = {\n txt: 'text/plain',\n md: 'text/markdown',\n log: 'text/plain',\n json: 'application/json',\n yaml: 'application/x-yaml',\n yml: 'application/x-yaml',\n pdf: 'application/pdf',\n};\n\nexport const MAX_QUERY_RETRIES = 1; // Max number of retries for query\n\nexport const upload = multer({\n storage: multer.memoryStorage(),\n limits: {\n fileSize: DEFAULT_MAX_FILE_SIZE_MB,\n },\n});\n\n/**\n * Supported file types for document upload AI Notebooks\n */\nexport enum SupportedFileType {\n MARKDOWN = 'md',\n TEXT = 'txt',\n PDF = 'pdf',\n JSON = 'json',\n YAML = 'yaml',\n YML = 'yml',\n LOG = 'log',\n}\n\n/**\n * HTML parsing constants for stripping tags and extracting text\n * @reserved Reserved for future URL file type support\n */\nexport const HTML_BLOCK_TAGS = new Set([\n 'div',\n 'p',\n 'br',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'li',\n 'tr',\n 'section',\n 'article',\n 'header',\n 'footer',\n]);\n\nexport const HTML_IGNORED_TAGS = new Set(['script', 'style']);\n\nexport const POLL_INTERVAL_MS = 1000; // 1 second\n\nexport const SKIP_USER_ID_ENDPOINTS = new Set(['/v1/models', '/v1/shields']);\n\n// default number of message history being loaded\nexport const DEFAULT_HISTORY_LENGTH = 10;\n"],"names":["multer","SupportedFileType"],"mappings":";;;;;;;;AAoBO,MAAM,8BAAA,GAAiC;AACvC,MAAM,6BAAA,GAAgC;AACtC,MAAM,4BAAA,GAA+B;AACrC,MAAM,+BAAA,GAAkC;AACxC,MAAM,+BAAA,GAAkC;AACxC,MAAM,wBAAA,GAA2B,KAAK,IAAA,GAAO;AAK7C,MAAM,oBAAA,GAAuB;AAC7B,MAAM,gCAAA,GAAmC;AACzC,MAAM,8BAAA,GAAiC;AAKvC,MAAM,gBAAA,GAAmB;AACzB,MAAM,yBAAA,GAA4B,KAAK,IAAA,GAAO;AAC9C,MAAM,gCAAA,GAAmC,KAAK,IAAA,GAAO;AACrD,MAAM,uBAAA,GAA0B;AAEhC,MAAM,uBAAA,GAA0B;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA,CAAA,CAyCrC,IAAA;AAaK,MAAM,oBAAA,GAAuB;AAK7B,MAAM,0BAAA,GAA6B;AA+DnC,MAAM,iBAAA,GAAoB;AAE1B,MAAM,SAASA,uBAAA,CAAO;AAAA,EAC3B,OAAA,EAASA,wBAAO,aAAA,EAAc;AAAA,EAC9B,MAAA,EAAQ;AAAA,IACN,QAAA,EAAU;AAAA;AAEd,CAAC;AAKM,IAAK,iBAAA,qBAAAC,kBAAAA,KAAL;AACL,EAAAA,mBAAA,UAAA,CAAA,GAAW,IAAA;AACX,EAAAA,mBAAA,MAAA,CAAA,GAAO,KAAA;AACP,EAAAA,mBAAA,KAAA,CAAA,GAAM,KAAA;AACN,EAAAA,mBAAA,MAAA,CAAA,GAAO,MAAA;AACP,EAAAA,mBAAA,MAAA,CAAA,GAAO,MAAA;AACP,EAAAA,mBAAA,KAAA,CAAA,GAAM,KAAA;AACN,EAAAA,mBAAA,KAAA,CAAA,GAAM,KAAA;AAPI,EAAA,OAAAA,kBAAAA;AAAA,CAAA,EAAA,iBAAA,IAAA,EAAA;AAoCL,MAAM,yCAAyB,IAAI,GAAA,CAAI,CAAC,YAAA,EAAc,aAAa,CAAC;AAGpE,MAAM,sBAAA,GAAyB;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"constant.cjs.js","sources":["../../src/service/constant.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 multer from 'multer';\n\n/**\n * Default values for AI Notebooks\n */\nexport const DEFAULT_CHUNKING_STRATEGY_TYPE = 'auto'; // auto chunking\nexport const DEFAULT_MAX_CHUNK_SIZE_TOKENS = 512; // 512 tokens\nexport const DEFAULT_CHUNK_OVERLAP_TOKENS = 50; // 50 tokens\nexport const DEFAULT_LIGHTSPEED_SERVICE_HOST = '127.0.0.1'; // Lightspeed core service host\nexport const DEFAULT_LIGHTSPEED_SERVICE_PORT = 8080; // Lightspeed service port\nexport const DEFAULT_MAX_FILE_SIZE_MB = 20 * 1024 * 1024; // 20MB\n\n/**\n * Rate limiting defaults (window is fixed at 1 minute)\n */\nexport const RATE_LIMIT_WINDOW_MS = 60000;\nexport const DEFAULT_EXPENSIVE_RATE_LIMIT_MAX = 25;\nexport const DEFAULT_GENERAL_RATE_LIMIT_MAX = 200;\n\n/**\n * Input validation limits for query endpoints\n */\nexport const MAX_QUERY_LENGTH = 32000; // 32K characters (reasonable for LLM context)\nexport const MAX_ATTACHMENT_SIZE_BYTES = 20 * 1024 * 1024; // 20MB (matches notebooks limit)\nexport const MAX_TOTAL_ATTACHMENTS_SIZE_BYTES = 50 * 1024 * 1024; // 50MB total\nexport const EXPRESS_JSON_BODY_LIMIT = '60mb';\n\nexport const NOTEBOOKS_SYSTEM_PROMPT = `\nYou are a helpful, analytical Research Analyst assistant. Your primary objective is to synthesize cross-document information to answer user queries with 100% fidelity to the provided documents.\n\n### QUERY TYPES - IMPORTANT\n* **Meta Queries ONLY:** ONLY when the user asks specifically about YOU as an assistant (e.g., \"who are you\", \"what can you do\", \"hello\"), respond naturally without requiring documents.\n* **ALL OTHER QUERIES:** For ANY other question, you MUST use the strict document-grounding rules below. This includes general knowledge questions, trivia, explanations, etc. If it's not about you as an assistant, it requires document evidence.\n\n### STRICT OPERATIONAL CONSTRAINTS\n* **Zero Outside Knowledge:** Do NOT use prior training data, general knowledge, or unsupported logical leaps to answer queries.\n* **Absolute Grounding:** If the provided documents do not contain explicit, direct evidence to answer the query, you MUST output exactly: \"I cannot answer this based on the provided documents.\"\n* **Precision Citations:** Every single factual claim, metric, or conclusion must have an inline citation [Document Title].\n* **Contradictions:** Do not resolve discrepancies. If sources conflict, explicitly document the friction (e.g., \"Source A states X, whereas Source B states Y\").\n\n### ANALYTICAL GUIDELINES\n* **Comprehensive Responses:** Provide thorough, detailed analysis. Don't be overly brief - expand on the evidence with full context and explanation.\n* **Quantitative Focus:** Prioritize extracting specific metrics, dates, and figures.\n* **Objective Tone:** Use neutral, professional language. Do not use subjective adjectives (e.g., \"impressive,\" \"concerning\") unless quoting the text directly.\n\n### REQUIRED ANALYSIS PROCESS\nBefore generating your response, you must internally perform evidence extraction (DO NOT show this in your output):\n1. Identify the core entities and requirements of the user's query.\n2. Extract exact, verbatim quotes from the provided documents that directly address the query.\n3. If no explicit quotes exist to answer the prompt, output ONLY: \"I cannot answer this based on the provided documents.\"\n\n### CRITICAL: NEVER output <evidence_extraction> tags or any internal reasoning in your visible response. These are for your internal analysis only.\n\n### REQUIRED OUTPUT STRUCTURE\nWhen you have evidence from documents, structure your response as:\n\n**Executive Summary:**\n[A comprehensive 2-4 sentence synthesis of the primary findings based strictly on the extracted evidence. Provide full context and detail.]\n\n**Detailed Analysis:**\n* **[Key Entity/Theme]:** [Thorough explanation of the fact or data point derived from text, with full context and supporting details] [Document Title].\n* **[Key Entity/Theme]:** [Thorough explanation of the fact or data point derived from text, with full context and supporting details] [Document Title].\n\n**Referenced Documents:**\n* [Document Title 1]\n* [Document Title 2]\n\nWhen you lack evidence, output ONLY: \"I cannot answer this based on the provided documents.\"\n`.trim();\n\n/**\n * HTTP and networking constants\n * @reserved Reserved for future URL file type support\n */\nexport const URL_FETCH_TIMEOUT_MS = 30000; // 30 second timeout for URL fetching\nexport const USER_AGENT = 'RHDH-Notebooks-Bot/1.0'; // User agent for HTTP requests\nexport const MAX_URL_CONTENT_SIZE = 10 * 1024 * 1024; // 10MB max for URL fetched content\n\n/**\n * HTTP status codes\n */\nexport const HTTP_STATUS_ACCEPTED = 202; // Async operation accepted\nexport const HTTP_STATUS_BAD_REQUEST = 400; // Bad request\nexport const HTTP_STATUS_FORBIDDEN = 403; // Forbidden\nexport const HTTP_STATUS_NOT_FOUND = 404; // Not found\nexport const HTTP_STATUS_CONFLICT = 409; // Conflict\nexport const HTTP_STATUS_INTERNAL_ERROR = 500; // Internal server error\n\n/**\n * SSRF Protection - Blocked hostnames for security\n * These hostnames are commonly used for Server-Side Request Forgery attacks\n * @reserved Reserved for future URL file type support\n */\nexport const SSRF_BLOCKED_HOSTNAMES = [\n 'localhost',\n 'metadata.google.internal', // GCP metadata endpoint\n 'kubernetes.default.svc', // Kubernetes internal DNS\n 'host.docker.internal', // Docker host access\n '169.254.169.254', // NOSONAR local netwok, used by AWS/Azure/GCP metadata IP\n '127.0.0.1', // IPv4 loopback\n '0.0.0.0', // IPv4 any address\n '::1', // IPv6 loopback\n '::', // IPv6 any address\n] as const;\n\n/**\n * Prompt Injection Protection - Patterns to detect and sanitize\n * These patterns are commonly used in prompt injection attacks\n * @reserved Reserved for future URL file type support\n */\nexport const PROMPT_INJECTION_PATTERNS = [\n /ignore\\s+(all\\s+)?previous\\s+(instructions?|prompts?)/gi,\n /disregard\\s+(all\\s+)?previous\\s+(instructions?|prompts?)/gi,\n /forget\\s+(all\\s+)?previous\\s+(instructions?|prompts?)/gi,\n /you\\s+are\\s+now\\s+(a\\s+)?different/gi,\n /new\\s+(instructions?|prompts?)\\s*:/gi,\n /system\\s*:\\s*/gi,\n /assistant\\s*:\\s*/gi,\n /\\[INST\\]/gi,\n /\\[\\/INST\\]/gi,\n /<\\|im_start\\|>/gi,\n /<\\|im_end\\|>/gi,\n /<\\|endoftext\\|>/gi,\n /\\[SYSTEM\\]/gi,\n /\\[\\/SYSTEM\\]/gi,\n /\\[ASSISTANT\\]/gi,\n /\\[\\/ASSISTANT\\]/gi,\n] as const;\n\n/**\n * Content sanitization constants\n * @reserved Reserved for future URL file type support\n */\nexport const MAX_CONSECUTIVE_NEWLINES = 4; // Max consecutive newlines allowed in content\nexport const FILTERED_CONTENT_MARKER = '[CONTENT_FILTERED]'; // Marker for filtered content\n\n/**\n * File type to MIME type mapping\n */\nexport const FILE_TYPE_TO_MIME: Record<string, string> = {\n txt: 'text/plain',\n md: 'text/markdown',\n log: 'text/plain',\n json: 'application/json',\n yaml: 'application/x-yaml',\n yml: 'application/x-yaml',\n pdf: 'application/pdf',\n};\n\nexport const MAX_QUERY_RETRIES = 1; // Max number of retries for query\n\nexport const upload = multer({\n storage: multer.memoryStorage(),\n limits: {\n fileSize: DEFAULT_MAX_FILE_SIZE_MB,\n },\n});\n\n/**\n * Supported file types for document upload AI Notebooks\n */\nexport enum SupportedFileType {\n MARKDOWN = 'md',\n TEXT = 'txt',\n PDF = 'pdf',\n JSON = 'json',\n YAML = 'yaml',\n YML = 'yml',\n LOG = 'log',\n}\n\n/**\n * HTML parsing constants for stripping tags and extracting text\n * @reserved Reserved for future URL file type support\n */\nexport const HTML_BLOCK_TAGS = new Set([\n 'div',\n 'p',\n 'br',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'li',\n 'tr',\n 'section',\n 'article',\n 'header',\n 'footer',\n]);\n\nexport const HTML_IGNORED_TAGS = new Set(['script', 'style']);\n\nexport const POLL_INTERVAL_MS = 1000; // 1 second\n\nexport const SKIP_USER_ID_ENDPOINTS = new Set([\n '/v1/models',\n '/v1/shields',\n '/v1/saved-prompts/config',\n]);\n\n// default number of message history being loaded\nexport const DEFAULT_HISTORY_LENGTH = 10;\n\n/**\n * Minimal 1x1 pixel JPEG image for testing model vision capabilities.\n * Base64-encoded JPEG with minimal headers.\n */\nexport const TEST_VISION_JPEG =\n '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a' +\n 'HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy' +\n 'MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIA' +\n 'AhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEB' +\n 'AQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCwAA//2Q==';\n"],"names":["multer","SupportedFileType"],"mappings":";;;;;;;;AAoBO,MAAM,8BAAA,GAAiC;AACvC,MAAM,6BAAA,GAAgC;AACtC,MAAM,4BAAA,GAA+B;AACrC,MAAM,+BAAA,GAAkC;AACxC,MAAM,+BAAA,GAAkC;AACxC,MAAM,wBAAA,GAA2B,KAAK,IAAA,GAAO;AAK7C,MAAM,oBAAA,GAAuB;AAC7B,MAAM,gCAAA,GAAmC;AACzC,MAAM,8BAAA,GAAiC;AAKvC,MAAM,gBAAA,GAAmB;AACzB,MAAM,yBAAA,GAA4B,KAAK,IAAA,GAAO;AAC9C,MAAM,gCAAA,GAAmC,KAAK,IAAA,GAAO;AACrD,MAAM,uBAAA,GAA0B;AAEhC,MAAM,uBAAA,GAA0B;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA,CAAA,CAyCrC,IAAA;AAaK,MAAM,oBAAA,GAAuB;AAK7B,MAAM,0BAAA,GAA6B;AA+DnC,MAAM,iBAAA,GAAoB;AAE1B,MAAM,SAASA,uBAAA,CAAO;AAAA,EAC3B,OAAA,EAASA,wBAAO,aAAA,EAAc;AAAA,EAC9B,MAAA,EAAQ;AAAA,IACN,QAAA,EAAU;AAAA;AAEd,CAAC;AAKM,IAAK,iBAAA,qBAAAC,kBAAAA,KAAL;AACL,EAAAA,mBAAA,UAAA,CAAA,GAAW,IAAA;AACX,EAAAA,mBAAA,MAAA,CAAA,GAAO,KAAA;AACP,EAAAA,mBAAA,KAAA,CAAA,GAAM,KAAA;AACN,EAAAA,mBAAA,MAAA,CAAA,GAAO,MAAA;AACP,EAAAA,mBAAA,MAAA,CAAA,GAAO,MAAA;AACP,EAAAA,mBAAA,KAAA,CAAA,GAAM,KAAA;AACN,EAAAA,mBAAA,KAAA,CAAA,GAAM,KAAA;AAPI,EAAA,OAAAA,kBAAAA;AAAA,CAAA,EAAA,iBAAA,IAAA,EAAA;AAoCL,MAAM,sBAAA,uBAA6B,GAAA,CAAI;AAAA,EAC5C,YAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAC;AAGM,MAAM,sBAAA,GAAyB;AAM/B,MAAM,gBAAA,GACX;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -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';\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 * 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","toFile","NotFoundError","ConflictError"],"mappings":"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"}
|