@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
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
// fileCollection.test.js
|
|
2
|
+
// Integration tests for file collection tool
|
|
3
|
+
|
|
4
|
+
import test from 'ava';
|
|
5
|
+
import serverFactory from '../../../../index.js';
|
|
6
|
+
import { callPathway } from '../../../../lib/pathwayTools.js';
|
|
7
|
+
import { generateFileMessageContent, resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
8
|
+
|
|
9
|
+
let testServer;
|
|
10
|
+
|
|
11
|
+
test.before(async () => {
|
|
12
|
+
const { server, startServer } = await serverFactory();
|
|
13
|
+
if (startServer) {
|
|
14
|
+
await startServer();
|
|
15
|
+
}
|
|
16
|
+
testServer = server;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test.after.always('cleanup', async () => {
|
|
20
|
+
if (testServer) {
|
|
21
|
+
await testServer.stop();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Helper to create a test context
|
|
26
|
+
const createTestContext = () => {
|
|
27
|
+
const contextId = `test-file-collection-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
28
|
+
return contextId;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Helper to extract files array from stored format (handles both old array format and new {version, files} format)
|
|
32
|
+
const extractFilesFromStored = (stored) => {
|
|
33
|
+
if (!stored) return [];
|
|
34
|
+
const parsed = typeof stored === 'string' ? JSON.parse(stored) : stored;
|
|
35
|
+
// Handle new format: { version, files }
|
|
36
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.files) {
|
|
37
|
+
return Array.isArray(parsed.files) ? parsed.files : [];
|
|
38
|
+
}
|
|
39
|
+
// Handle old format: just an array
|
|
40
|
+
if (Array.isArray(parsed)) {
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
return [];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Helper to clean up test data
|
|
47
|
+
const cleanup = async (contextId, contextKey = null) => {
|
|
48
|
+
try {
|
|
49
|
+
const { keyValueStorageClient } = await import('../../../../lib/keyValueStorageClient.js');
|
|
50
|
+
// Delete the key entirely instead of setting to empty array
|
|
51
|
+
await keyValueStorageClient.delete(`${contextId}-memoryFiles`);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// Ignore cleanup errors
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
test('File collection: Add file to collection', async t => {
|
|
58
|
+
const contextId = createTestContext();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
62
|
+
contextId,
|
|
63
|
+
url: 'https://example.com/test.jpg',
|
|
64
|
+
gcs: 'gs://bucket/test.jpg',
|
|
65
|
+
filename: 'test.jpg',
|
|
66
|
+
tags: ['photo', 'test'],
|
|
67
|
+
notes: 'Test file',
|
|
68
|
+
userMessage: 'Adding test file'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const parsed = JSON.parse(result);
|
|
72
|
+
t.is(parsed.success, true);
|
|
73
|
+
t.truthy(parsed.fileId);
|
|
74
|
+
t.true(parsed.message.includes('test.jpg'));
|
|
75
|
+
|
|
76
|
+
// Verify it was saved
|
|
77
|
+
const saved = await callPathway('sys_read_memory', {
|
|
78
|
+
contextId,
|
|
79
|
+
section: 'memoryFiles'
|
|
80
|
+
});
|
|
81
|
+
const collection = extractFilesFromStored(saved);
|
|
82
|
+
t.is(collection.length, 1);
|
|
83
|
+
t.is(collection[0].filename, 'test.jpg');
|
|
84
|
+
t.is(collection[0].url, 'https://example.com/test.jpg');
|
|
85
|
+
t.is(collection[0].gcs, 'gs://bucket/test.jpg');
|
|
86
|
+
t.deepEqual(collection[0].tags, ['photo', 'test']);
|
|
87
|
+
t.is(collection[0].notes, 'Test file');
|
|
88
|
+
} finally {
|
|
89
|
+
await cleanup(contextId);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('File collection: List files', async t => {
|
|
94
|
+
const contextId = createTestContext();
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Add a few files first
|
|
98
|
+
await callPathway('sys_tool_file_collection', {
|
|
99
|
+
contextId,
|
|
100
|
+
url: 'https://example.com/file1.jpg',
|
|
101
|
+
filename: 'file1.jpg',
|
|
102
|
+
userMessage: 'Add file 1'
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await callPathway('sys_tool_file_collection', {
|
|
106
|
+
contextId,
|
|
107
|
+
url: 'https://example.com/file2.pdf',
|
|
108
|
+
filename: 'file2.pdf',
|
|
109
|
+
tags: ['document'],
|
|
110
|
+
userMessage: 'Add file 2'
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// List files
|
|
114
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
115
|
+
contextId,
|
|
116
|
+
userMessage: 'List files'
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const parsed = JSON.parse(result);
|
|
120
|
+
t.is(parsed.success, true);
|
|
121
|
+
t.is(parsed.count, 2);
|
|
122
|
+
t.is(parsed.totalFiles, 2);
|
|
123
|
+
t.is(parsed.files.length, 2);
|
|
124
|
+
t.true(parsed.files.some(f => f.filename === 'file1.jpg'));
|
|
125
|
+
t.true(parsed.files.some(f => f.filename === 'file2.pdf'));
|
|
126
|
+
} finally {
|
|
127
|
+
await cleanup(contextId);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('File collection: Search files', async t => {
|
|
132
|
+
const contextId = createTestContext();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Add files with different metadata
|
|
136
|
+
await callPathway('sys_tool_file_collection', {
|
|
137
|
+
contextId,
|
|
138
|
+
url: 'https://example.com/report.pdf',
|
|
139
|
+
filename: 'report.pdf',
|
|
140
|
+
tags: ['document', 'report'],
|
|
141
|
+
notes: 'Monthly report',
|
|
142
|
+
userMessage: 'Add report'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await callPathway('sys_tool_file_collection', {
|
|
146
|
+
contextId,
|
|
147
|
+
url: 'https://example.com/image.jpg',
|
|
148
|
+
filename: 'image.jpg',
|
|
149
|
+
tags: ['photo'],
|
|
150
|
+
notes: 'Photo of office',
|
|
151
|
+
userMessage: 'Add image'
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Search by filename
|
|
155
|
+
const result1 = await callPathway('sys_tool_file_collection', {
|
|
156
|
+
contextId,
|
|
157
|
+
query: 'report',
|
|
158
|
+
userMessage: 'Search for report'
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const parsed1 = JSON.parse(result1);
|
|
162
|
+
t.is(parsed1.success, true);
|
|
163
|
+
t.is(parsed1.count, 1);
|
|
164
|
+
t.is(parsed1.files[0].filename, 'report.pdf');
|
|
165
|
+
|
|
166
|
+
// Search by tag
|
|
167
|
+
const result2 = await callPathway('sys_tool_file_collection', {
|
|
168
|
+
contextId,
|
|
169
|
+
query: 'photo',
|
|
170
|
+
userMessage: 'Search for photo'
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const parsed2 = JSON.parse(result2);
|
|
174
|
+
t.is(parsed2.success, true);
|
|
175
|
+
t.is(parsed2.count, 1);
|
|
176
|
+
t.is(parsed2.files[0].filename, 'image.jpg');
|
|
177
|
+
|
|
178
|
+
// Search by notes
|
|
179
|
+
const result3 = await callPathway('sys_tool_file_collection', {
|
|
180
|
+
contextId,
|
|
181
|
+
query: 'office',
|
|
182
|
+
userMessage: 'Search for office'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const parsed3 = JSON.parse(result3);
|
|
186
|
+
t.is(parsed3.success, true);
|
|
187
|
+
t.is(parsed3.count, 1);
|
|
188
|
+
t.is(parsed3.files[0].filename, 'image.jpg');
|
|
189
|
+
} finally {
|
|
190
|
+
await cleanup(contextId);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('File collection: Remove single file', async t => {
|
|
195
|
+
const contextId = createTestContext();
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// Add files
|
|
199
|
+
const addResult1 = await callPathway('sys_tool_file_collection', {
|
|
200
|
+
contextId,
|
|
201
|
+
url: 'https://example.com/file1.jpg',
|
|
202
|
+
filename: 'file1.jpg',
|
|
203
|
+
userMessage: 'Add file 1'
|
|
204
|
+
});
|
|
205
|
+
const file1Id = JSON.parse(addResult1).fileId;
|
|
206
|
+
|
|
207
|
+
await callPathway('sys_tool_file_collection', {
|
|
208
|
+
contextId,
|
|
209
|
+
url: 'https://example.com/file2.pdf',
|
|
210
|
+
filename: 'file2.pdf',
|
|
211
|
+
userMessage: 'Add file 2'
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Remove file1
|
|
215
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
216
|
+
contextId,
|
|
217
|
+
fileId: file1Id,
|
|
218
|
+
userMessage: 'Remove file 1'
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const parsed = JSON.parse(result);
|
|
222
|
+
t.is(parsed.success, true);
|
|
223
|
+
t.is(parsed.removedCount, 1);
|
|
224
|
+
t.is(parsed.remainingFiles, 1);
|
|
225
|
+
t.is(parsed.removedFiles.length, 1);
|
|
226
|
+
t.is(parsed.removedFiles[0].filename, 'file1.jpg');
|
|
227
|
+
// Note: deletedFromCloud may be 0 if file has no hash or deletion fails (which is OK)
|
|
228
|
+
t.true(typeof parsed.deletedFromCloud === 'number');
|
|
229
|
+
|
|
230
|
+
// Verify it was removed
|
|
231
|
+
const listResult = await callPathway('sys_tool_file_collection', {
|
|
232
|
+
contextId,
|
|
233
|
+
userMessage: 'List files'
|
|
234
|
+
});
|
|
235
|
+
const listParsed = JSON.parse(listResult);
|
|
236
|
+
t.is(listParsed.totalFiles, 1);
|
|
237
|
+
t.false(listParsed.files.some(f => f.filename === 'file1.jpg'));
|
|
238
|
+
} finally {
|
|
239
|
+
await cleanup(contextId);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('File collection: Remove all files', async t => {
|
|
244
|
+
const contextId = createTestContext();
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
// Add files
|
|
248
|
+
await callPathway('sys_tool_file_collection', {
|
|
249
|
+
contextId,
|
|
250
|
+
url: 'https://example.com/file1.jpg',
|
|
251
|
+
filename: 'file1.jpg',
|
|
252
|
+
userMessage: 'Add file 1'
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await callPathway('sys_tool_file_collection', {
|
|
256
|
+
contextId,
|
|
257
|
+
url: 'https://example.com/file2.pdf',
|
|
258
|
+
filename: 'file2.pdf',
|
|
259
|
+
userMessage: 'Add file 2'
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Remove all
|
|
263
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
264
|
+
contextId,
|
|
265
|
+
fileId: '*',
|
|
266
|
+
userMessage: 'Remove all files'
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const parsed = JSON.parse(result);
|
|
270
|
+
t.is(parsed.success, true);
|
|
271
|
+
t.is(parsed.removedCount, 2);
|
|
272
|
+
t.is(parsed.remainingFiles, 0);
|
|
273
|
+
t.is(parsed.removedFiles.length, 2);
|
|
274
|
+
t.true(parsed.message.includes('All 2 file(s)'));
|
|
275
|
+
// Note: deletedFromCloud may be 0 if files have no hash or deletion fails (which is OK)
|
|
276
|
+
t.true(typeof parsed.deletedFromCloud === 'number');
|
|
277
|
+
|
|
278
|
+
// Verify collection is empty
|
|
279
|
+
const listResult = await callPathway('sys_tool_file_collection', {
|
|
280
|
+
contextId,
|
|
281
|
+
userMessage: 'List files'
|
|
282
|
+
});
|
|
283
|
+
const listParsed = JSON.parse(listResult);
|
|
284
|
+
t.is(listParsed.totalFiles, 0);
|
|
285
|
+
} finally {
|
|
286
|
+
await cleanup(contextId);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('File collection: Error handling - missing contextId', async t => {
|
|
291
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
292
|
+
url: 'https://example.com/test.jpg',
|
|
293
|
+
filename: 'test.jpg',
|
|
294
|
+
userMessage: 'Test'
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const parsed = JSON.parse(result);
|
|
298
|
+
t.is(parsed.success, false);
|
|
299
|
+
t.true(parsed.error.includes('contextId is required'));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('File collection: Error handling - remove non-existent file', async t => {
|
|
303
|
+
const contextId = createTestContext();
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
307
|
+
contextId,
|
|
308
|
+
fileId: 'non-existent-id',
|
|
309
|
+
userMessage: 'Remove file'
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const parsed = JSON.parse(result);
|
|
313
|
+
t.is(parsed.success, false);
|
|
314
|
+
t.true(parsed.error.includes('not found in collection'));
|
|
315
|
+
} finally {
|
|
316
|
+
await cleanup(contextId);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('File collection: List with filters and sorting', async t => {
|
|
321
|
+
const contextId = createTestContext();
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
// Add files with different tags and dates
|
|
325
|
+
await callPathway('sys_tool_file_collection', {
|
|
326
|
+
contextId,
|
|
327
|
+
url: 'https://example.com/file1.jpg',
|
|
328
|
+
filename: 'a_file.jpg',
|
|
329
|
+
tags: ['photo'],
|
|
330
|
+
userMessage: 'Add file 1'
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Wait a bit to ensure different timestamps
|
|
334
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
335
|
+
|
|
336
|
+
await callPathway('sys_tool_file_collection', {
|
|
337
|
+
contextId,
|
|
338
|
+
url: 'https://example.com/file2.pdf',
|
|
339
|
+
filename: 'z_file.pdf',
|
|
340
|
+
tags: ['document'],
|
|
341
|
+
userMessage: 'Add file 2'
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// List sorted by filename
|
|
345
|
+
const result1 = await callPathway('sys_tool_file_collection', {
|
|
346
|
+
contextId,
|
|
347
|
+
sortBy: 'filename',
|
|
348
|
+
userMessage: 'List sorted by filename'
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const parsed1 = JSON.parse(result1);
|
|
352
|
+
t.is(parsed1.files[0].filename, 'a_file.jpg');
|
|
353
|
+
t.is(parsed1.files[1].filename, 'z_file.pdf');
|
|
354
|
+
|
|
355
|
+
// List filtered by tag
|
|
356
|
+
const result2 = await callPathway('sys_tool_file_collection', {
|
|
357
|
+
contextId,
|
|
358
|
+
tags: ['photo'],
|
|
359
|
+
userMessage: 'List photos'
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const parsed2 = JSON.parse(result2);
|
|
363
|
+
t.is(parsed2.count, 1);
|
|
364
|
+
t.is(parsed2.files[0].filename, 'a_file.jpg');
|
|
365
|
+
} finally {
|
|
366
|
+
await cleanup(contextId);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('Memory system: memoryFiles excluded from memoryAll', async t => {
|
|
371
|
+
const contextId = createTestContext();
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// Save a file collection
|
|
375
|
+
await callPathway('sys_save_memory', {
|
|
376
|
+
contextId,
|
|
377
|
+
section: 'memoryFiles',
|
|
378
|
+
aiMemory: JSON.stringify([{
|
|
379
|
+
id: 'test-1',
|
|
380
|
+
url: 'https://example.com/test.jpg',
|
|
381
|
+
filename: 'test.jpg'
|
|
382
|
+
}])
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Save other memory
|
|
386
|
+
await callPathway('sys_save_memory', {
|
|
387
|
+
contextId,
|
|
388
|
+
section: 'memorySelf',
|
|
389
|
+
aiMemory: 'Test memory content'
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Read all memory - should not include memoryFiles
|
|
393
|
+
const allMemory = await callPathway('sys_read_memory', {
|
|
394
|
+
contextId,
|
|
395
|
+
section: 'memoryAll'
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const parsed = JSON.parse(allMemory);
|
|
399
|
+
t.truthy(parsed.memorySelf);
|
|
400
|
+
t.falsy(parsed.memoryFiles);
|
|
401
|
+
|
|
402
|
+
// But should be accessible explicitly
|
|
403
|
+
const files = await callPathway('sys_read_memory', {
|
|
404
|
+
contextId,
|
|
405
|
+
section: 'memoryFiles'
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const filesParsed = JSON.parse(files);
|
|
409
|
+
t.is(filesParsed.length, 1);
|
|
410
|
+
t.is(filesParsed[0].filename, 'test.jpg');
|
|
411
|
+
} finally {
|
|
412
|
+
await cleanup(contextId);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('Memory system: memoryFiles not cleared by memoryAll clear', async t => {
|
|
417
|
+
const contextId = createTestContext();
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
// Save file collection
|
|
421
|
+
await callPathway('sys_save_memory', {
|
|
422
|
+
contextId,
|
|
423
|
+
section: 'memoryFiles',
|
|
424
|
+
aiMemory: JSON.stringify([{
|
|
425
|
+
id: 'test-1',
|
|
426
|
+
url: 'https://example.com/test.jpg',
|
|
427
|
+
filename: 'test.jpg'
|
|
428
|
+
}])
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Clear all memory
|
|
432
|
+
await callPathway('sys_save_memory', {
|
|
433
|
+
contextId,
|
|
434
|
+
section: 'memoryAll',
|
|
435
|
+
aiMemory: ''
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Verify files are still there
|
|
439
|
+
const files = await callPathway('sys_read_memory', {
|
|
440
|
+
contextId,
|
|
441
|
+
section: 'memoryFiles'
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const filesParsed = JSON.parse(files);
|
|
445
|
+
t.is(filesParsed.length, 1);
|
|
446
|
+
t.is(filesParsed[0].filename, 'test.jpg');
|
|
447
|
+
} finally {
|
|
448
|
+
await cleanup(contextId);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('Memory system: memoryFiles ignored in memoryAll save', async t => {
|
|
453
|
+
const contextId = createTestContext();
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
// Save file collection first
|
|
457
|
+
await callPathway('sys_save_memory', {
|
|
458
|
+
contextId,
|
|
459
|
+
section: 'memoryFiles',
|
|
460
|
+
aiMemory: JSON.stringify([{
|
|
461
|
+
id: 'original',
|
|
462
|
+
cloudUrl: 'https://example.com/original.jpg',
|
|
463
|
+
filename: 'original.jpg'
|
|
464
|
+
}])
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Try to save all memory with memoryFiles included
|
|
468
|
+
await callPathway('sys_save_memory', {
|
|
469
|
+
contextId,
|
|
470
|
+
section: 'memoryAll',
|
|
471
|
+
aiMemory: JSON.stringify({
|
|
472
|
+
memorySelf: 'Test content',
|
|
473
|
+
memoryFiles: JSON.stringify([{
|
|
474
|
+
id: 'new',
|
|
475
|
+
url: 'https://example.com/new.jpg',
|
|
476
|
+
filename: 'new.jpg'
|
|
477
|
+
}])
|
|
478
|
+
})
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Verify original files are still there (not overwritten)
|
|
482
|
+
const files = await callPathway('sys_read_memory', {
|
|
483
|
+
contextId,
|
|
484
|
+
section: 'memoryFiles'
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const filesParsed = JSON.parse(files);
|
|
488
|
+
t.is(filesParsed.length, 1);
|
|
489
|
+
t.is(filesParsed[0].filename, 'original.jpg');
|
|
490
|
+
} finally {
|
|
491
|
+
await cleanup(contextId);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Test generateFileMessageContent function (integration tests)
|
|
496
|
+
test('generateFileMessageContent should find file by ID', async t => {
|
|
497
|
+
const contextId = createTestContext();
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
// Add a file to collection
|
|
501
|
+
await callPathway('sys_tool_file_collection', {
|
|
502
|
+
contextId,
|
|
503
|
+
url: 'https://example.com/test.pdf',
|
|
504
|
+
gcs: 'gs://bucket/test.pdf',
|
|
505
|
+
filename: 'test.pdf',
|
|
506
|
+
userMessage: 'Add test file'
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Get the file ID from the collection
|
|
510
|
+
const saved = await callPathway('sys_read_memory', {
|
|
511
|
+
contextId,
|
|
512
|
+
section: 'memoryFiles'
|
|
513
|
+
});
|
|
514
|
+
const collection = extractFilesFromStored(saved);
|
|
515
|
+
const fileId = collection[0].id;
|
|
516
|
+
|
|
517
|
+
// Normalize by ID
|
|
518
|
+
const result = await generateFileMessageContent(fileId, contextId);
|
|
519
|
+
|
|
520
|
+
t.truthy(result);
|
|
521
|
+
t.is(result.type, 'file');
|
|
522
|
+
t.is(result.url, 'https://example.com/test.pdf');
|
|
523
|
+
t.is(result.gcs, 'gs://bucket/test.pdf');
|
|
524
|
+
t.is(result.originalFilename, 'test.pdf');
|
|
525
|
+
} finally {
|
|
526
|
+
await cleanup(contextId);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test('generateFileMessageContent should find file by URL', async t => {
|
|
531
|
+
const contextId = createTestContext();
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
// Add a file to collection
|
|
535
|
+
await callPathway('sys_tool_file_collection', {
|
|
536
|
+
contextId,
|
|
537
|
+
url: 'https://example.com/test.pdf',
|
|
538
|
+
gcs: 'gs://bucket/test.pdf',
|
|
539
|
+
filename: 'test.pdf',
|
|
540
|
+
userMessage: 'Add test file'
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Normalize by URL
|
|
544
|
+
const result = await generateFileMessageContent('https://example.com/test.pdf', contextId);
|
|
545
|
+
|
|
546
|
+
t.truthy(result);
|
|
547
|
+
t.is(result.url, 'https://example.com/test.pdf');
|
|
548
|
+
t.is(result.gcs, 'gs://bucket/test.pdf');
|
|
549
|
+
} finally {
|
|
550
|
+
await cleanup(contextId);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('generateFileMessageContent should find file by fuzzy filename match', async t => {
|
|
555
|
+
const contextId = createTestContext();
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
// Add files to collection
|
|
559
|
+
await callPathway('sys_tool_file_collection', {
|
|
560
|
+
contextId,
|
|
561
|
+
url: 'https://example.com/document.pdf',
|
|
562
|
+
filename: 'document.pdf',
|
|
563
|
+
userMessage: 'Add document'
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
await callPathway('sys_tool_file_collection', {
|
|
567
|
+
contextId,
|
|
568
|
+
url: 'https://example.com/image.jpg',
|
|
569
|
+
filename: 'image.jpg',
|
|
570
|
+
userMessage: 'Add image'
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Normalize by partial filename
|
|
574
|
+
const result1 = await generateFileMessageContent('document', contextId);
|
|
575
|
+
t.truthy(result1);
|
|
576
|
+
t.is(result1.originalFilename, 'document.pdf');
|
|
577
|
+
|
|
578
|
+
// Normalize by full filename
|
|
579
|
+
const result2 = await generateFileMessageContent('image.jpg', contextId);
|
|
580
|
+
t.truthy(result2);
|
|
581
|
+
t.is(result2.originalFilename, 'image.jpg');
|
|
582
|
+
} finally {
|
|
583
|
+
await cleanup(contextId);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('generateFileMessageContent should detect image type', async t => {
|
|
588
|
+
const contextId = createTestContext();
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
// Add an image file
|
|
592
|
+
await callPathway('sys_tool_file_collection', {
|
|
593
|
+
contextId,
|
|
594
|
+
url: 'https://example.com/image.jpg',
|
|
595
|
+
filename: 'image.jpg',
|
|
596
|
+
userMessage: 'Add image'
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const saved = await callPathway('sys_read_memory', {
|
|
600
|
+
contextId,
|
|
601
|
+
section: 'memoryFiles'
|
|
602
|
+
});
|
|
603
|
+
const collection = extractFilesFromStored(saved);
|
|
604
|
+
const fileId = collection[0].id;
|
|
605
|
+
|
|
606
|
+
const result = await generateFileMessageContent(fileId, contextId);
|
|
607
|
+
|
|
608
|
+
t.truthy(result);
|
|
609
|
+
t.is(result.type, 'image_url');
|
|
610
|
+
t.truthy(result.image_url);
|
|
611
|
+
t.is(result.image_url.url, 'https://example.com/image.jpg');
|
|
612
|
+
} finally {
|
|
613
|
+
await cleanup(contextId);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Tests for resolveFileParameter
|
|
618
|
+
test('resolveFileParameter: Resolve by file ID', async t => {
|
|
619
|
+
const contextId = createTestContext();
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
// Add a file to collection
|
|
623
|
+
const addResult = await callPathway('sys_tool_file_collection', {
|
|
624
|
+
contextId,
|
|
625
|
+
url: 'https://example.com/test-doc.pdf',
|
|
626
|
+
gcs: 'gs://bucket/test-doc.pdf',
|
|
627
|
+
filename: 'test-doc.pdf',
|
|
628
|
+
userMessage: 'Adding test file'
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const addParsed = JSON.parse(addResult);
|
|
632
|
+
const fileId = addParsed.fileId;
|
|
633
|
+
|
|
634
|
+
// Resolve by file ID
|
|
635
|
+
const resolved = await resolveFileParameter(fileId, contextId);
|
|
636
|
+
t.is(resolved, 'https://example.com/test-doc.pdf');
|
|
637
|
+
} finally {
|
|
638
|
+
await cleanup(contextId);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test('resolveFileParameter: Resolve by filename', async t => {
|
|
643
|
+
const contextId = createTestContext();
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
// Add a file to collection
|
|
647
|
+
await callPathway('sys_tool_file_collection', {
|
|
648
|
+
contextId,
|
|
649
|
+
url: 'https://example.com/my-file.txt',
|
|
650
|
+
gcs: 'gs://bucket/my-file.txt',
|
|
651
|
+
filename: 'my-file.txt',
|
|
652
|
+
userMessage: 'Adding test file'
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Resolve by filename
|
|
656
|
+
const resolved = await resolveFileParameter('my-file.txt', contextId);
|
|
657
|
+
t.is(resolved, 'https://example.com/my-file.txt');
|
|
658
|
+
} finally {
|
|
659
|
+
await cleanup(contextId);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test('resolveFileParameter: Resolve by hash', async t => {
|
|
664
|
+
const contextId = createTestContext();
|
|
665
|
+
const testHash = 'abc123def456';
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
// Add a file to collection with hash
|
|
669
|
+
await callPathway('sys_tool_file_collection', {
|
|
670
|
+
contextId,
|
|
671
|
+
url: 'https://example.com/hashed-file.jpg',
|
|
672
|
+
gcs: 'gs://bucket/hashed-file.jpg',
|
|
673
|
+
filename: 'hashed-file.jpg',
|
|
674
|
+
hash: testHash,
|
|
675
|
+
userMessage: 'Adding test file'
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Resolve by hash
|
|
679
|
+
const resolved = await resolveFileParameter(testHash, contextId);
|
|
680
|
+
t.is(resolved, 'https://example.com/hashed-file.jpg');
|
|
681
|
+
} finally {
|
|
682
|
+
await cleanup(contextId);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test('resolveFileParameter: Resolve by Azure URL', async t => {
|
|
687
|
+
const contextId = createTestContext();
|
|
688
|
+
const testUrl = 'https://example.com/existing-file.pdf';
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
// Add a file to collection
|
|
692
|
+
await callPathway('sys_tool_file_collection', {
|
|
693
|
+
contextId,
|
|
694
|
+
url: testUrl,
|
|
695
|
+
gcs: 'gs://bucket/existing-file.pdf',
|
|
696
|
+
filename: 'existing-file.pdf',
|
|
697
|
+
userMessage: 'Adding test file'
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// Resolve by Azure URL
|
|
701
|
+
const resolved = await resolveFileParameter(testUrl, contextId);
|
|
702
|
+
t.is(resolved, testUrl);
|
|
703
|
+
} finally {
|
|
704
|
+
await cleanup(contextId);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test('resolveFileParameter: Resolve by GCS URL', async t => {
|
|
709
|
+
const contextId = createTestContext();
|
|
710
|
+
const testGcsUrl = 'gs://bucket/gcs-file.pdf';
|
|
711
|
+
|
|
712
|
+
try {
|
|
713
|
+
// Add a file to collection
|
|
714
|
+
await callPathway('sys_tool_file_collection', {
|
|
715
|
+
contextId,
|
|
716
|
+
url: 'https://example.com/gcs-file.pdf',
|
|
717
|
+
gcs: testGcsUrl,
|
|
718
|
+
filename: 'gcs-file.pdf',
|
|
719
|
+
userMessage: 'Adding test file'
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Resolve by GCS URL
|
|
723
|
+
const resolved = await resolveFileParameter(testGcsUrl, contextId);
|
|
724
|
+
t.is(resolved, 'https://example.com/gcs-file.pdf');
|
|
725
|
+
} finally {
|
|
726
|
+
await cleanup(contextId);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test('resolveFileParameter: Prefer GCS URL when preferGcs is true', async t => {
|
|
731
|
+
const contextId = createTestContext();
|
|
732
|
+
const testGcsUrl = 'gs://bucket/prefer-gcs-file.pdf';
|
|
733
|
+
const testAzureUrl = 'https://example.com/prefer-gcs-file.pdf';
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
// Add a file to collection with both URLs
|
|
737
|
+
await callPathway('sys_tool_file_collection', {
|
|
738
|
+
contextId,
|
|
739
|
+
url: testAzureUrl,
|
|
740
|
+
gcs: testGcsUrl,
|
|
741
|
+
filename: 'prefer-gcs-file.pdf',
|
|
742
|
+
userMessage: 'Adding test file'
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Resolve by filename without preferGcs (should return Azure URL)
|
|
746
|
+
const resolvedDefault = await resolveFileParameter('prefer-gcs-file.pdf', contextId);
|
|
747
|
+
t.is(resolvedDefault, testAzureUrl);
|
|
748
|
+
|
|
749
|
+
// Resolve by filename with preferGcs (should return GCS URL)
|
|
750
|
+
const resolvedGcs = await resolveFileParameter('prefer-gcs-file.pdf', contextId, null, { preferGcs: true });
|
|
751
|
+
t.is(resolvedGcs, testGcsUrl);
|
|
752
|
+
} finally {
|
|
753
|
+
await cleanup(contextId);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test('resolveFileParameter: Return null when file not found', async t => {
|
|
758
|
+
const contextId = createTestContext();
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
// Try to resolve a non-existent file
|
|
762
|
+
const resolved = await resolveFileParameter('non-existent-file.txt', contextId);
|
|
763
|
+
t.is(resolved, null);
|
|
764
|
+
} finally {
|
|
765
|
+
await cleanup(contextId);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test('resolveFileParameter: Return null when contextId is missing', async t => {
|
|
770
|
+
// Try to resolve without contextId
|
|
771
|
+
const resolved = await resolveFileParameter('some-file.txt', null);
|
|
772
|
+
t.is(resolved, null);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test('resolveFileParameter: Return null when fileParam is empty', async t => {
|
|
776
|
+
const contextId = createTestContext();
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
// Try with empty string
|
|
780
|
+
const resolved1 = await resolveFileParameter('', contextId);
|
|
781
|
+
t.is(resolved1, null);
|
|
782
|
+
|
|
783
|
+
// Try with null
|
|
784
|
+
const resolved2 = await resolveFileParameter(null, contextId);
|
|
785
|
+
t.is(resolved2, null);
|
|
786
|
+
|
|
787
|
+
// Try with undefined
|
|
788
|
+
const resolved3 = await resolveFileParameter(undefined, contextId);
|
|
789
|
+
t.is(resolved3, null);
|
|
790
|
+
} finally {
|
|
791
|
+
await cleanup(contextId);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test('resolveFileParameter: Fuzzy filename matching', async t => {
|
|
796
|
+
const contextId = createTestContext();
|
|
797
|
+
|
|
798
|
+
try {
|
|
799
|
+
// Add a file with a specific filename
|
|
800
|
+
await callPathway('sys_tool_file_collection', {
|
|
801
|
+
contextId,
|
|
802
|
+
url: 'https://example.com/my-document.pdf',
|
|
803
|
+
gcs: 'gs://bucket/my-document.pdf',
|
|
804
|
+
filename: 'my-document.pdf',
|
|
805
|
+
userMessage: 'Adding test file'
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// Resolve by partial filename (fuzzy match)
|
|
809
|
+
const resolved = await resolveFileParameter('document.pdf', contextId);
|
|
810
|
+
t.is(resolved, 'https://example.com/my-document.pdf');
|
|
811
|
+
} finally {
|
|
812
|
+
await cleanup(contextId);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test('resolveFileParameter: Fallback to Azure URL when GCS not available and preferGcs is true', async t => {
|
|
817
|
+
const contextId = createTestContext();
|
|
818
|
+
const testAzureUrl = 'https://example.com/no-gcs-file.pdf';
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
// Add a file without GCS URL
|
|
822
|
+
await callPathway('sys_tool_file_collection', {
|
|
823
|
+
contextId,
|
|
824
|
+
url: testAzureUrl,
|
|
825
|
+
filename: 'no-gcs-file.pdf',
|
|
826
|
+
userMessage: 'Adding test file'
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Resolve with preferGcs=true, but no GCS available (should fallback to Azure URL)
|
|
830
|
+
const resolved = await resolveFileParameter('no-gcs-file.pdf', contextId, null, { preferGcs: true });
|
|
831
|
+
t.is(resolved, testAzureUrl);
|
|
832
|
+
} finally {
|
|
833
|
+
await cleanup(contextId);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test('resolveFileParameter: Handle contextKey for encrypted collections', async t => {
|
|
838
|
+
const contextId = createTestContext();
|
|
839
|
+
const contextKey = 'test-encryption-key';
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
// Add a file to collection with contextKey
|
|
843
|
+
await callPathway('sys_tool_file_collection', {
|
|
844
|
+
contextId,
|
|
845
|
+
contextKey,
|
|
846
|
+
url: 'https://example.com/encrypted-file.pdf',
|
|
847
|
+
gcs: 'gs://bucket/encrypted-file.pdf',
|
|
848
|
+
filename: 'encrypted-file.pdf',
|
|
849
|
+
userMessage: 'Adding test file'
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// Resolve with contextKey
|
|
853
|
+
const resolved = await resolveFileParameter('encrypted-file.pdf', contextId, contextKey);
|
|
854
|
+
t.is(resolved, 'https://example.com/encrypted-file.pdf');
|
|
855
|
+
} finally {
|
|
856
|
+
await cleanup(contextId);
|
|
857
|
+
}
|
|
858
|
+
});
|