@aj-archipelago/cortex 1.4.7 → 1.4.8
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/lib/fileUtils.js +40 -30
- package/package.json +1 -1
- package/pathways/system/entity/tools/sys_tool_editfile.js +2 -4
- package/pathways/system/entity/tools/sys_tool_readfile.js +52 -3
- package/pathways/system/entity/tools/sys_tool_writefile.js +2 -4
- package/tests/integration/features/tools/fileCollection.test.js +2 -3
- package/tests/unit/core/util.test.js +2 -4
package/lib/fileUtils.js
CHANGED
|
@@ -983,10 +983,7 @@ async function generateFileMessageContent(fileParam, contextId, contextKey = nul
|
|
|
983
983
|
if (!contextId) {
|
|
984
984
|
// Without contextId, we can't look up in collection
|
|
985
985
|
// Return a basic content object from the URL
|
|
986
|
-
return
|
|
987
|
-
type: 'file',
|
|
988
|
-
url: fileParam
|
|
989
|
-
};
|
|
986
|
+
return null;
|
|
990
987
|
}
|
|
991
988
|
|
|
992
989
|
// Load file collection
|
|
@@ -1000,31 +997,14 @@ async function generateFileMessageContent(fileParam, contextId, contextKey = nul
|
|
|
1000
997
|
return null;
|
|
1001
998
|
}
|
|
1002
999
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return {
|
|
1012
|
-
type: 'image_url',
|
|
1013
|
-
image_url: { url: foundFile.url },
|
|
1014
|
-
url: foundFile.url,
|
|
1015
|
-
gcs: foundFile.gcs || null,
|
|
1016
|
-
originalFilename: foundFile.filename || null,
|
|
1017
|
-
hash: foundFile.hash || null
|
|
1018
|
-
};
|
|
1019
|
-
} else {
|
|
1020
|
-
return {
|
|
1021
|
-
type: 'file',
|
|
1022
|
-
url: foundFile.url,
|
|
1023
|
-
gcs: foundFile.gcs || null,
|
|
1024
|
-
originalFilename: foundFile.filename || null,
|
|
1025
|
-
hash: foundFile.hash || null
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1000
|
+
return {
|
|
1001
|
+
type: 'image_url',
|
|
1002
|
+
url: foundFile.url,
|
|
1003
|
+
gcs: foundFile.gcs || null,
|
|
1004
|
+
originalFilename: foundFile.filename || null,
|
|
1005
|
+
hash: foundFile.hash || null
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1028
1008
|
}
|
|
1029
1009
|
|
|
1030
1010
|
/**
|
|
@@ -1448,6 +1428,35 @@ function getMimeTypeFromExtension(extension, defaultMimeType = 'application/octe
|
|
|
1448
1428
|
return mimeType || defaultMimeType;
|
|
1449
1429
|
}
|
|
1450
1430
|
|
|
1431
|
+
/**
|
|
1432
|
+
* Check if a MIME type represents a text-based file that can be read as text
|
|
1433
|
+
* @param {string} mimeType - MIME type to check
|
|
1434
|
+
* @returns {boolean} - Returns true if it's a text-based MIME type
|
|
1435
|
+
*/
|
|
1436
|
+
function isTextMimeType(mimeType) {
|
|
1437
|
+
if (!mimeType || typeof mimeType !== 'string') {
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const mimeTypeLower = mimeType.toLowerCase();
|
|
1442
|
+
|
|
1443
|
+
// All text/* types
|
|
1444
|
+
if (mimeTypeLower.startsWith('text/')) {
|
|
1445
|
+
return true;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// Text-based application types (consistent with sys_tool_writefile and sys_tool_editfile)
|
|
1449
|
+
const textApplicationTypes = [
|
|
1450
|
+
'application/json',
|
|
1451
|
+
'application/javascript',
|
|
1452
|
+
'application/x-javascript',
|
|
1453
|
+
'application/typescript',
|
|
1454
|
+
'application/xml',
|
|
1455
|
+
];
|
|
1456
|
+
|
|
1457
|
+
return textApplicationTypes.includes(mimeTypeLower);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1451
1460
|
export {
|
|
1452
1461
|
computeFileHash,
|
|
1453
1462
|
computeBufferHash,
|
|
@@ -1476,6 +1485,7 @@ export {
|
|
|
1476
1485
|
uploadImageToCloud,
|
|
1477
1486
|
resolveFileHashesToContent,
|
|
1478
1487
|
getMimeTypeFromFilename,
|
|
1479
|
-
getMimeTypeFromExtension
|
|
1488
|
+
getMimeTypeFromExtension,
|
|
1489
|
+
isTextMimeType
|
|
1480
1490
|
};
|
|
1481
1491
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Entity tool that modifies existing files by replacing line ranges or exact string matches
|
|
3
3
|
import logger from '../../../../lib/logger.js';
|
|
4
4
|
import { axios } from '../../../../lib/requestExecutor.js';
|
|
5
|
-
import { uploadFileToCloud, findFileInCollection, loadFileCollection, saveFileCollection, getMimeTypeFromFilename, resolveFileParameter, deleteFileByHash, modifyFileCollectionWithLock } from '../../../../lib/fileUtils.js';
|
|
5
|
+
import { uploadFileToCloud, findFileInCollection, loadFileCollection, saveFileCollection, getMimeTypeFromFilename, resolveFileParameter, deleteFileByHash, modifyFileCollectionWithLock, isTextMimeType } from '../../../../lib/fileUtils.js';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
prompt: [],
|
|
@@ -297,9 +297,7 @@ export default {
|
|
|
297
297
|
let mimeType = getMimeTypeFromFilename(filename, 'text/plain');
|
|
298
298
|
|
|
299
299
|
// Add charset=utf-8 for text-based MIME types
|
|
300
|
-
if (mimeType
|
|
301
|
-
mimeType === 'application/javascript' || mimeType === 'application/typescript' ||
|
|
302
|
-
mimeType === 'application/xml') {
|
|
300
|
+
if (isTextMimeType(mimeType)) {
|
|
303
301
|
mimeType = `${mimeType}; charset=utf-8`;
|
|
304
302
|
}
|
|
305
303
|
|
|
@@ -2,7 +2,32 @@
|
|
|
2
2
|
// Tool pathway that reads text files with line number support
|
|
3
3
|
import logger from '../../../../lib/logger.js';
|
|
4
4
|
import { axios } from '../../../../lib/requestExecutor.js';
|
|
5
|
-
import { resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
5
|
+
import { resolveFileParameter, getMimeTypeFromFilename, isTextMimeType } from '../../../../lib/fileUtils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if a file is a text file type that can be read
|
|
9
|
+
* @param {string} url - File URL or path
|
|
10
|
+
* @returns {boolean} - Returns true if it's a text file, false otherwise
|
|
11
|
+
*/
|
|
12
|
+
function isTextFile(url) {
|
|
13
|
+
if (!url || typeof url !== 'string') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Extract filename from URL (remove query string and fragment)
|
|
18
|
+
const urlPath = url.split('?')[0].split('#')[0];
|
|
19
|
+
|
|
20
|
+
// Use existing MIME utility to get MIME type
|
|
21
|
+
const mimeType = getMimeTypeFromFilename(urlPath);
|
|
22
|
+
|
|
23
|
+
if (!mimeType || mimeType === 'application/octet-stream') {
|
|
24
|
+
// Unknown MIME type, reject to be safe
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Use shared utility function for consistency with other tools
|
|
29
|
+
return isTextMimeType(mimeType);
|
|
30
|
+
}
|
|
6
31
|
|
|
7
32
|
export default {
|
|
8
33
|
prompt: [],
|
|
@@ -11,8 +36,8 @@ export default {
|
|
|
11
36
|
type: "function",
|
|
12
37
|
icon: "📖",
|
|
13
38
|
function: {
|
|
14
|
-
name: "
|
|
15
|
-
description: "Read text content from a file. Can read the entire file or specific line ranges. Use this to access and analyze text files from your file collection. Supports text files, markdown files, html, csv, and other document formats that can be converted to text
|
|
39
|
+
name: "ReadTextFile",
|
|
40
|
+
description: "Read text content from a file. Can read the entire file or specific line ranges. Use this to access and analyze text files from your file collection. Supports text files, markdown files, html, csv, and other document formats that can be converted to text. DOES NOT support binary files, images, videos, or audio files or pdfs.",
|
|
16
41
|
parameters: {
|
|
17
42
|
type: "object",
|
|
18
43
|
properties: {
|
|
@@ -77,6 +102,19 @@ export default {
|
|
|
77
102
|
return JSON.stringify(errorResult);
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
// Check if file is a text type before attempting to read
|
|
106
|
+
if (!isTextFile(cloudUrl)) {
|
|
107
|
+
const mimeType = getMimeTypeFromFilename(cloudUrl.split('?')[0].split('#')[0]);
|
|
108
|
+
const detectedType = mimeType || 'unknown type';
|
|
109
|
+
|
|
110
|
+
const errorResult = {
|
|
111
|
+
success: false,
|
|
112
|
+
error: `This tool only supports text files. The file appears to be a non-text file (MIME type: ${detectedType}). For images, PDFs, videos, or other non-text files, please use the AnalyzeImage, AnalyzePDF, or AnalyzeVideo tools instead.`
|
|
113
|
+
};
|
|
114
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
115
|
+
return JSON.stringify(errorResult);
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
if (startLine !== undefined) {
|
|
81
119
|
if (typeof startLine !== 'number' || !Number.isInteger(startLine) || startLine < 1) {
|
|
82
120
|
const errorResult = {
|
|
@@ -130,6 +168,17 @@ export default {
|
|
|
130
168
|
throw new Error(`Failed to download file content: ${response.status}`);
|
|
131
169
|
}
|
|
132
170
|
|
|
171
|
+
// Secondary check: verify content-type header if available
|
|
172
|
+
const contentType = response.headers['content-type'] || response.headers['Content-Type'];
|
|
173
|
+
if (contentType && !isTextMimeType(contentType)) {
|
|
174
|
+
const errorResult = {
|
|
175
|
+
success: false,
|
|
176
|
+
error: `This tool only supports text files. The file appears to be a non-text file (Content-Type: ${contentType}). For images, PDFs, videos, or other non-text files, please use the AnalyzeImage, AnalyzePDF, or AnalyzeVideo tools instead.`
|
|
177
|
+
};
|
|
178
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
179
|
+
return JSON.stringify(errorResult);
|
|
180
|
+
}
|
|
181
|
+
|
|
133
182
|
// Explicitly decode as UTF-8 to prevent mojibake (encoding corruption)
|
|
134
183
|
const textContent = Buffer.from(response.data).toString('utf8');
|
|
135
184
|
const allLines = textContent.split(/\r?\n/);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sys_tool_writefile.js
|
|
2
2
|
// Entity tool that writes content to a file and uploads it to cloud storage
|
|
3
3
|
import logger from '../../../../lib/logger.js';
|
|
4
|
-
import { uploadFileToCloud, addFileToCollection, getMimeTypeFromFilename } from '../../../../lib/fileUtils.js';
|
|
4
|
+
import { uploadFileToCloud, addFileToCollection, getMimeTypeFromFilename, isTextMimeType } from '../../../../lib/fileUtils.js';
|
|
5
5
|
|
|
6
6
|
// Helper function to format file size
|
|
7
7
|
function formatFileSize(bytes) {
|
|
@@ -129,9 +129,7 @@ export default {
|
|
|
129
129
|
let mimeType = getMimeTypeFromFilename(filename, 'text/plain');
|
|
130
130
|
|
|
131
131
|
// Add charset=utf-8 for text-based MIME types to ensure proper encoding
|
|
132
|
-
if (mimeType
|
|
133
|
-
mimeType === 'application/javascript' || mimeType === 'application/typescript' ||
|
|
134
|
-
mimeType === 'application/xml') {
|
|
132
|
+
if (isTextMimeType(mimeType)) {
|
|
135
133
|
mimeType = `${mimeType}; charset=utf-8`;
|
|
136
134
|
}
|
|
137
135
|
|
|
@@ -518,7 +518,7 @@ test('generateFileMessageContent should find file by ID', async t => {
|
|
|
518
518
|
const result = await generateFileMessageContent(fileId, contextId);
|
|
519
519
|
|
|
520
520
|
t.truthy(result);
|
|
521
|
-
t.is(result.type, '
|
|
521
|
+
t.is(result.type, 'image_url');
|
|
522
522
|
t.is(result.url, 'https://example.com/test.pdf');
|
|
523
523
|
t.is(result.gcs, 'gs://bucket/test.pdf');
|
|
524
524
|
t.is(result.originalFilename, 'test.pdf');
|
|
@@ -607,8 +607,7 @@ test('generateFileMessageContent should detect image type', async t => {
|
|
|
607
607
|
|
|
608
608
|
t.truthy(result);
|
|
609
609
|
t.is(result.type, 'image_url');
|
|
610
|
-
t.
|
|
611
|
-
t.is(result.image_url.url, 'https://example.com/image.jpg');
|
|
610
|
+
t.is(result.url, 'https://example.com/image.jpg');
|
|
612
611
|
} finally {
|
|
613
612
|
await cleanup(contextId);
|
|
614
613
|
}
|
|
@@ -239,12 +239,10 @@ test('generateFileMessageContent should return null for invalid input', async t
|
|
|
239
239
|
t.is(await generateFileMessageContent(123, 'context-1'), null);
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
test('generateFileMessageContent should return
|
|
242
|
+
test('generateFileMessageContent should return null when no contextId', async t => {
|
|
243
243
|
const result = await generateFileMessageContent('https://example.com/file.pdf', null);
|
|
244
244
|
|
|
245
|
-
t.
|
|
246
|
-
t.is(result.type, 'file');
|
|
247
|
-
t.is(result.url, 'https://example.com/file.pdf');
|
|
245
|
+
t.is(result, null);
|
|
248
246
|
});
|
|
249
247
|
|
|
250
248
|
test('generateFileMessageContent should return null for file not in collection', async t => {
|