@aj-archipelago/cortex 1.4.6 → 1.4.7
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/helper-apps/cortex-file-handler/package-lock.json +2 -2
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/src/index.js +27 -4
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +74 -10
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +23 -2
- package/helper-apps/cortex-file-handler/src/start.js +2 -0
- package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +287 -0
- package/helper-apps/cortex-file-handler/tests/start.test.js +1 -1
- package/lib/entityConstants.js +1 -1
- package/lib/fileUtils.js +1481 -0
- package/lib/pathwayTools.js +7 -1
- package/lib/util.js +2 -313
- package/package.json +4 -3
- package/pathways/image_qwen.js +1 -1
- package/pathways/system/entity/memory/sys_read_memory.js +17 -3
- package/pathways/system/entity/memory/sys_save_memory.js +22 -6
- package/pathways/system/entity/sys_entity_agent.js +21 -4
- package/pathways/system/entity/tools/sys_tool_analyzefile.js +171 -0
- package/pathways/system/entity/tools/sys_tool_codingagent.js +38 -4
- package/pathways/system/entity/tools/sys_tool_editfile.js +403 -0
- package/pathways/system/entity/tools/sys_tool_file_collection.js +433 -0
- package/pathways/system/entity/tools/sys_tool_image.js +172 -10
- package/pathways/system/entity/tools/sys_tool_image_gemini.js +123 -10
- package/pathways/system/entity/tools/sys_tool_readfile.js +217 -124
- package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
- package/pathways/system/entity/tools/sys_tool_writefile.js +211 -0
- package/pathways/system/workspaces/run_workspace_prompt.js +4 -3
- package/pathways/transcribe_gemini.js +2 -1
- package/server/executeWorkspace.js +1 -1
- package/server/plugins/neuralSpacePlugin.js +2 -6
- package/server/plugins/openAiWhisperPlugin.js +2 -1
- package/server/plugins/replicateApiPlugin.js +4 -14
- package/server/typeDef.js +10 -1
- package/tests/integration/features/tools/fileCollection.test.js +858 -0
- package/tests/integration/features/tools/fileOperations.test.js +851 -0
- package/tests/integration/features/tools/writefile.test.js +350 -0
- package/tests/unit/core/fileCollection.test.js +259 -0
- package/tests/unit/core/util.test.js +320 -1
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// Tests for utility functions in cortex/lib/util.js
|
|
3
3
|
|
|
4
4
|
import test from 'ava';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
5
8
|
import { removeOldImageAndFileContent } from '../../../lib/util.js';
|
|
9
|
+
import { computeFileHash, computeBufferHash, generateFileMessageContent, injectFileIntoChatHistory } from '../../../lib/fileUtils.js';
|
|
6
10
|
|
|
7
11
|
// Test removeOldImageAndFileContent function
|
|
8
12
|
|
|
@@ -143,4 +147,319 @@ test('removeOldImageAndFileContent should handle mixed content types', t => {
|
|
|
143
147
|
|
|
144
148
|
const result = removeOldImageAndFileContent(chatHistory);
|
|
145
149
|
t.deepEqual(result, expected);
|
|
146
|
-
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Test computeFileHash function
|
|
153
|
+
test('computeFileHash should compute hash for a file', async t => {
|
|
154
|
+
// Create a temporary file
|
|
155
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-test-'));
|
|
156
|
+
const testFile = path.join(tempDir, 'test.txt');
|
|
157
|
+
const testContent = 'Hello, World! This is a test file.';
|
|
158
|
+
fs.writeFileSync(testFile, testContent);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const hash = await computeFileHash(testFile);
|
|
162
|
+
t.truthy(hash);
|
|
163
|
+
t.is(typeof hash, 'string');
|
|
164
|
+
t.is(hash.length, 16); // xxhash64 produces 16 hex characters
|
|
165
|
+
|
|
166
|
+
// Same content should produce same hash
|
|
167
|
+
const hash2 = await computeFileHash(testFile);
|
|
168
|
+
t.is(hash, hash2);
|
|
169
|
+
} finally {
|
|
170
|
+
// Cleanup
|
|
171
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('computeFileHash should handle different file contents', async t => {
|
|
176
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cortex-test-'));
|
|
177
|
+
const file1 = path.join(tempDir, 'file1.txt');
|
|
178
|
+
const file2 = path.join(tempDir, 'file2.txt');
|
|
179
|
+
|
|
180
|
+
fs.writeFileSync(file1, 'Content 1');
|
|
181
|
+
fs.writeFileSync(file2, 'Content 2');
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const hash1 = await computeFileHash(file1);
|
|
185
|
+
const hash2 = await computeFileHash(file2);
|
|
186
|
+
|
|
187
|
+
t.not(hash1, hash2);
|
|
188
|
+
} finally {
|
|
189
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('computeFileHash should reject on non-existent file', async t => {
|
|
194
|
+
const nonExistentFile = path.join(os.tmpdir(), 'non-existent-file-' + Date.now());
|
|
195
|
+
|
|
196
|
+
await t.throwsAsync(async () => {
|
|
197
|
+
await computeFileHash(nonExistentFile);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Test computeBufferHash function
|
|
202
|
+
test('computeBufferHash should compute hash for a buffer', async t => {
|
|
203
|
+
const buffer = Buffer.from('Hello, World! This is a test.');
|
|
204
|
+
const hash = await computeBufferHash(buffer);
|
|
205
|
+
|
|
206
|
+
t.truthy(hash);
|
|
207
|
+
t.is(typeof hash, 'string');
|
|
208
|
+
t.is(hash.length, 16); // xxhash64 produces 16 hex characters
|
|
209
|
+
|
|
210
|
+
// Same buffer should produce same hash
|
|
211
|
+
const hash2 = await computeBufferHash(buffer);
|
|
212
|
+
t.is(hash, hash2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('computeBufferHash should handle different buffer contents', async t => {
|
|
216
|
+
const buffer1 = Buffer.from('Content 1');
|
|
217
|
+
const buffer2 = Buffer.from('Content 2');
|
|
218
|
+
|
|
219
|
+
const hash1 = await computeBufferHash(buffer1);
|
|
220
|
+
const hash2 = await computeBufferHash(buffer2);
|
|
221
|
+
|
|
222
|
+
t.not(hash1, hash2);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('computeBufferHash should handle empty buffer', async t => {
|
|
226
|
+
const buffer = Buffer.from('');
|
|
227
|
+
const hash = await computeBufferHash(buffer);
|
|
228
|
+
|
|
229
|
+
t.truthy(hash);
|
|
230
|
+
t.is(typeof hash, 'string');
|
|
231
|
+
t.is(hash.length, 16);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Test generateFileMessageContent function
|
|
235
|
+
test('generateFileMessageContent should return null for invalid input', async t => {
|
|
236
|
+
t.is(await generateFileMessageContent(null, 'context-1'), null);
|
|
237
|
+
t.is(await generateFileMessageContent(undefined, 'context-1'), null);
|
|
238
|
+
t.is(await generateFileMessageContent('', 'context-1'), null);
|
|
239
|
+
t.is(await generateFileMessageContent(123, 'context-1'), null);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('generateFileMessageContent should return basic object when no contextId', async t => {
|
|
243
|
+
const result = await generateFileMessageContent('https://example.com/file.pdf', null);
|
|
244
|
+
|
|
245
|
+
t.truthy(result);
|
|
246
|
+
t.is(result.type, 'file');
|
|
247
|
+
t.is(result.url, 'https://example.com/file.pdf');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('generateFileMessageContent should return null for file not in collection', async t => {
|
|
251
|
+
const contextId = `test-normalize-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
252
|
+
|
|
253
|
+
const result = await generateFileMessageContent('nonexistent.pdf', contextId);
|
|
254
|
+
t.is(result, null);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Test injectFileIntoChatHistory function
|
|
258
|
+
test('injectFileIntoChatHistory should inject file into empty chat history', t => {
|
|
259
|
+
const chatHistory = [];
|
|
260
|
+
const fileContent = {
|
|
261
|
+
type: 'file',
|
|
262
|
+
file: 'https://example.com/test.pdf',
|
|
263
|
+
url: 'https://example.com/test.pdf',
|
|
264
|
+
gcs: 'gs://bucket/test.pdf',
|
|
265
|
+
originalFilename: 'test.pdf'
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const result = injectFileIntoChatHistory(chatHistory, fileContent);
|
|
269
|
+
|
|
270
|
+
t.is(result.length, 1);
|
|
271
|
+
t.is(result[0].role, 'user');
|
|
272
|
+
t.true(Array.isArray(result[0].content));
|
|
273
|
+
t.is(result[0].content.length, 1);
|
|
274
|
+
|
|
275
|
+
// Content should be an object (OpenAI-compatible format), not a JSON string
|
|
276
|
+
const injected = result[0].content[0];
|
|
277
|
+
t.is(typeof injected, 'object');
|
|
278
|
+
t.is(injected.type, 'file');
|
|
279
|
+
t.is(injected.file, 'https://example.com/test.pdf');
|
|
280
|
+
t.is(injected.url, 'https://example.com/test.pdf');
|
|
281
|
+
t.is(injected.gcs, 'gs://bucket/test.pdf');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('injectFileIntoChatHistory should inject file into existing chat history', t => {
|
|
285
|
+
const chatHistory = [
|
|
286
|
+
{ role: 'user', content: 'Hello' },
|
|
287
|
+
{ role: 'assistant', content: 'Hi there!' }
|
|
288
|
+
];
|
|
289
|
+
const fileContent = {
|
|
290
|
+
type: 'file',
|
|
291
|
+
url: 'https://example.com/test.pdf',
|
|
292
|
+
originalFilename: 'test.pdf'
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const result = injectFileIntoChatHistory(chatHistory, fileContent);
|
|
296
|
+
|
|
297
|
+
t.is(result.length, 3);
|
|
298
|
+
t.is(result[0].role, 'user');
|
|
299
|
+
t.is(result[0].content, 'Hello');
|
|
300
|
+
t.is(result[1].role, 'assistant');
|
|
301
|
+
t.is(result[1].content, 'Hi there!');
|
|
302
|
+
t.is(result[2].role, 'user');
|
|
303
|
+
t.true(Array.isArray(result[2].content));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('injectFileIntoChatHistory should not inject duplicate file by URL', t => {
|
|
307
|
+
const chatHistory = [
|
|
308
|
+
{
|
|
309
|
+
role: 'user',
|
|
310
|
+
content: [{
|
|
311
|
+
type: 'file',
|
|
312
|
+
file: 'https://example.com/test.pdf',
|
|
313
|
+
url: 'https://example.com/test.pdf',
|
|
314
|
+
gcs: 'gs://bucket/test.pdf',
|
|
315
|
+
originalFilename: 'test.pdf'
|
|
316
|
+
}]
|
|
317
|
+
}
|
|
318
|
+
];
|
|
319
|
+
const fileContent = {
|
|
320
|
+
type: 'file',
|
|
321
|
+
file: 'https://example.com/test.pdf',
|
|
322
|
+
url: 'https://example.com/test.pdf',
|
|
323
|
+
gcs: 'gs://bucket/test.pdf',
|
|
324
|
+
originalFilename: 'test.pdf'
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const result = injectFileIntoChatHistory(chatHistory, fileContent);
|
|
328
|
+
|
|
329
|
+
// Should be unchanged (no duplicate added)
|
|
330
|
+
t.is(result.length, 1);
|
|
331
|
+
t.is(result[0].content.length, 1);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('injectFileIntoChatHistory should not inject duplicate file by GCS URL', t => {
|
|
335
|
+
const chatHistory = [
|
|
336
|
+
{
|
|
337
|
+
role: 'user',
|
|
338
|
+
content: [{
|
|
339
|
+
type: 'file',
|
|
340
|
+
file: 'https://example.com/test.pdf',
|
|
341
|
+
url: 'https://example.com/test.pdf',
|
|
342
|
+
gcs: 'gs://bucket/test.pdf',
|
|
343
|
+
originalFilename: 'test.pdf'
|
|
344
|
+
}]
|
|
345
|
+
}
|
|
346
|
+
];
|
|
347
|
+
const fileContent = {
|
|
348
|
+
type: 'file',
|
|
349
|
+
file: 'https://example.com/other.pdf',
|
|
350
|
+
url: 'https://example.com/other.pdf',
|
|
351
|
+
gcs: 'gs://bucket/test.pdf', // Same GCS URL
|
|
352
|
+
originalFilename: 'other.pdf'
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const result = injectFileIntoChatHistory(chatHistory, fileContent);
|
|
356
|
+
|
|
357
|
+
// Should be unchanged (no duplicate added)
|
|
358
|
+
t.is(result.length, 1);
|
|
359
|
+
t.is(result[0].content.length, 1);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('injectFileIntoChatHistory should not inject duplicate file by hash', t => {
|
|
363
|
+
const chatHistory = [
|
|
364
|
+
{
|
|
365
|
+
role: 'user',
|
|
366
|
+
content: [{
|
|
367
|
+
type: 'file',
|
|
368
|
+
file: 'https://example.com/test.pdf',
|
|
369
|
+
url: 'https://example.com/test.pdf',
|
|
370
|
+
hash: 'abc123def456',
|
|
371
|
+
originalFilename: 'test.pdf'
|
|
372
|
+
}]
|
|
373
|
+
}
|
|
374
|
+
];
|
|
375
|
+
const fileContent = {
|
|
376
|
+
type: 'file',
|
|
377
|
+
file: 'https://example.com/other.pdf',
|
|
378
|
+
url: 'https://example.com/other.pdf',
|
|
379
|
+
hash: 'abc123def456', // Same hash
|
|
380
|
+
originalFilename: 'other.pdf'
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const result = injectFileIntoChatHistory(chatHistory, fileContent);
|
|
384
|
+
|
|
385
|
+
// Should be unchanged (no duplicate added)
|
|
386
|
+
t.is(result.length, 1);
|
|
387
|
+
t.is(result[0].content.length, 1);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('injectFileIntoChatHistory should inject different file', t => {
|
|
391
|
+
const chatHistory = [
|
|
392
|
+
{
|
|
393
|
+
role: 'user',
|
|
394
|
+
content: [{
|
|
395
|
+
type: 'file',
|
|
396
|
+
file: 'https://example.com/file1.pdf',
|
|
397
|
+
url: 'https://example.com/file1.pdf',
|
|
398
|
+
originalFilename: 'file1.pdf'
|
|
399
|
+
}]
|
|
400
|
+
}
|
|
401
|
+
];
|
|
402
|
+
const fileContent = {
|
|
403
|
+
type: 'file',
|
|
404
|
+
file: 'https://example.com/file2.pdf',
|
|
405
|
+
url: 'https://example.com/file2.pdf',
|
|
406
|
+
originalFilename: 'file2.pdf'
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const result = injectFileIntoChatHistory(chatHistory, fileContent);
|
|
410
|
+
|
|
411
|
+
// Should have both files
|
|
412
|
+
t.is(result.length, 2);
|
|
413
|
+
t.is(result[1].role, 'user');
|
|
414
|
+
t.true(Array.isArray(result[1].content));
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('injectFileIntoChatHistory should handle null/undefined chat history', t => {
|
|
418
|
+
const fileContent = {
|
|
419
|
+
type: 'file',
|
|
420
|
+
url: 'https://example.com/test.pdf'
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const result1 = injectFileIntoChatHistory(null, fileContent);
|
|
424
|
+
t.is(result1.length, 1);
|
|
425
|
+
t.is(result1[0].role, 'user');
|
|
426
|
+
|
|
427
|
+
const result2 = injectFileIntoChatHistory(undefined, fileContent);
|
|
428
|
+
t.is(result2.length, 1);
|
|
429
|
+
t.is(result2[0].role, 'user');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('injectFileIntoChatHistory should handle null/undefined file content', t => {
|
|
433
|
+
const chatHistory = [
|
|
434
|
+
{ role: 'user', content: 'Hello' }
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
const result1 = injectFileIntoChatHistory(chatHistory, null);
|
|
438
|
+
t.deepEqual(result1, chatHistory);
|
|
439
|
+
|
|
440
|
+
const result2 = injectFileIntoChatHistory(chatHistory, undefined);
|
|
441
|
+
t.deepEqual(result2, chatHistory);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('injectFileIntoChatHistory should handle image_url type', t => {
|
|
445
|
+
const chatHistory = [];
|
|
446
|
+
const fileContent = {
|
|
447
|
+
type: 'image_url',
|
|
448
|
+
image_url: { url: 'https://example.com/image.jpg' },
|
|
449
|
+
url: 'https://example.com/image.jpg',
|
|
450
|
+
gcs: 'gs://bucket/image.jpg',
|
|
451
|
+
originalFilename: 'image.jpg'
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const result = injectFileIntoChatHistory(chatHistory, fileContent);
|
|
455
|
+
|
|
456
|
+
t.is(result.length, 1);
|
|
457
|
+
// Content should be an object (OpenAI-compatible format), not a JSON string
|
|
458
|
+
const injected = result[0].content[0];
|
|
459
|
+
t.is(typeof injected, 'object');
|
|
460
|
+
t.is(injected.type, 'image_url');
|
|
461
|
+
t.truthy(injected.image_url);
|
|
462
|
+
t.is(injected.image_url.url, 'https://example.com/image.jpg');
|
|
463
|
+
t.is(injected.url, 'https://example.com/image.jpg');
|
|
464
|
+
t.is(injected.gcs, 'gs://bucket/image.jpg');
|
|
465
|
+
});
|