@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.
Files changed (38) hide show
  1. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  2. package/helper-apps/cortex-file-handler/package.json +1 -1
  3. package/helper-apps/cortex-file-handler/src/index.js +27 -4
  4. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +74 -10
  5. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +23 -2
  6. package/helper-apps/cortex-file-handler/src/start.js +2 -0
  7. package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +287 -0
  8. package/helper-apps/cortex-file-handler/tests/start.test.js +1 -1
  9. package/lib/entityConstants.js +1 -1
  10. package/lib/fileUtils.js +1481 -0
  11. package/lib/pathwayTools.js +7 -1
  12. package/lib/util.js +2 -313
  13. package/package.json +4 -3
  14. package/pathways/image_qwen.js +1 -1
  15. package/pathways/system/entity/memory/sys_read_memory.js +17 -3
  16. package/pathways/system/entity/memory/sys_save_memory.js +22 -6
  17. package/pathways/system/entity/sys_entity_agent.js +21 -4
  18. package/pathways/system/entity/tools/sys_tool_analyzefile.js +171 -0
  19. package/pathways/system/entity/tools/sys_tool_codingagent.js +38 -4
  20. package/pathways/system/entity/tools/sys_tool_editfile.js +403 -0
  21. package/pathways/system/entity/tools/sys_tool_file_collection.js +433 -0
  22. package/pathways/system/entity/tools/sys_tool_image.js +172 -10
  23. package/pathways/system/entity/tools/sys_tool_image_gemini.js +123 -10
  24. package/pathways/system/entity/tools/sys_tool_readfile.js +217 -124
  25. package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
  26. package/pathways/system/entity/tools/sys_tool_writefile.js +211 -0
  27. package/pathways/system/workspaces/run_workspace_prompt.js +4 -3
  28. package/pathways/transcribe_gemini.js +2 -1
  29. package/server/executeWorkspace.js +1 -1
  30. package/server/plugins/neuralSpacePlugin.js +2 -6
  31. package/server/plugins/openAiWhisperPlugin.js +2 -1
  32. package/server/plugins/replicateApiPlugin.js +4 -14
  33. package/server/typeDef.js +10 -1
  34. package/tests/integration/features/tools/fileCollection.test.js +858 -0
  35. package/tests/integration/features/tools/fileOperations.test.js +851 -0
  36. package/tests/integration/features/tools/writefile.test.js +350 -0
  37. package/tests/unit/core/fileCollection.test.js +259 -0
  38. 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
+ });