@aj-archipelago/cortex 1.4.28 → 1.4.30
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 +97 -13
- package/package.json +1 -1
- package/pathways/system/entity/tools/sys_tool_codingagent.js +0 -2
- package/pathways/system/entity/tools/sys_tool_editfile.js +4 -6
- package/pathways/system/entity/tools/sys_tool_file_collection.js +103 -31
- package/pathways/system/entity/tools/sys_tool_image.js +5 -28
- package/pathways/system/entity/tools/sys_tool_image_gemini.js +5 -29
- package/pathways/system/entity/tools/sys_tool_slides_gemini.js +8 -31
- package/pathways/system/entity/tools/sys_tool_store_memory.js +0 -2
- package/pathways/system/entity/tools/sys_tool_video_veo.js +5 -29
- package/pathways/system/entity/tools/sys_tool_view_image.js +2 -3
- package/pathways/system/entity/tools/sys_tool_writefile.js +17 -3
- package/tests/integration/features/tools/fileCollection.test.js +321 -0
- package/tests/integration/features/tools/fileOperations.test.js +1 -1
- package/tests/integration/features/tools/writefile.test.js +1 -1
package/lib/fileUtils.js
CHANGED
|
@@ -266,7 +266,7 @@ async function getMediaChunks(file, requestId, contextId = null) {
|
|
|
266
266
|
requestId,
|
|
267
267
|
...(contextId ? { contextId } : {})
|
|
268
268
|
});
|
|
269
|
-
const res = await axios.get(url, { timeout:
|
|
269
|
+
const res = await axios.get(url, { timeout: 600000 });
|
|
270
270
|
return res.data;
|
|
271
271
|
} else {
|
|
272
272
|
logger.info(`No API_URL set, returning file as chunk`);
|
|
@@ -1444,33 +1444,36 @@ function getDefaultContext(agentContext) {
|
|
|
1444
1444
|
* Load merged file collection from agentContext array
|
|
1445
1445
|
* Merges all contexts in the array for read operations
|
|
1446
1446
|
* @param {Array} agentContext - Array of context objects { contextId, contextKey, default }
|
|
1447
|
+
* @param {string|null} chatId - Optional chat ID to filter files by (if provided, only includes files with '*' or this chatId in inCollection)
|
|
1447
1448
|
* @returns {Promise<Array>} Merged file collection
|
|
1448
1449
|
*/
|
|
1449
|
-
async function loadMergedFileCollection(agentContext) {
|
|
1450
|
+
async function loadMergedFileCollection(agentContext, chatId = null) {
|
|
1450
1451
|
if (!agentContext || !Array.isArray(agentContext) || agentContext.length === 0) {
|
|
1451
1452
|
return [];
|
|
1452
1453
|
}
|
|
1453
1454
|
|
|
1454
|
-
// Load first context as primary
|
|
1455
|
+
// Load first context as primary
|
|
1456
|
+
// If chatId is provided, use loadFileCollection to filter by chatId
|
|
1457
|
+
// Otherwise, use loadFileCollectionAll to get all files (we'll filter by inCollection below)
|
|
1455
1458
|
const primaryCtx = agentContext[0];
|
|
1456
|
-
const primaryCollection =
|
|
1459
|
+
const primaryCollection = chatId
|
|
1460
|
+
? await loadFileCollection(primaryCtx.contextId, primaryCtx.contextKey || null, true, chatId)
|
|
1461
|
+
: await loadFileCollectionAll(primaryCtx.contextId, primaryCtx.contextKey || null);
|
|
1457
1462
|
|
|
1458
1463
|
// Tag primary files with their source context
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
// If only one context, return early
|
|
1462
|
-
if (agentContext.length === 1) {
|
|
1463
|
-
return collection;
|
|
1464
|
-
}
|
|
1464
|
+
let collection = primaryCollection.map(f => ({ ...f, _contextId: primaryCtx.contextId }));
|
|
1465
1465
|
|
|
1466
1466
|
// Load and merge additional contexts
|
|
1467
1467
|
for (let i = 1; i < agentContext.length; i++) {
|
|
1468
1468
|
const ctx = agentContext[i];
|
|
1469
1469
|
if (!ctx.contextId) continue;
|
|
1470
1470
|
|
|
1471
|
-
// Load alternate collection
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1471
|
+
// Load alternate collection
|
|
1472
|
+
// If chatId is provided, use loadFileCollection to filter by chatId
|
|
1473
|
+
// Otherwise, use loadFileCollectionAll to get all files
|
|
1474
|
+
const altCollection = chatId
|
|
1475
|
+
? await loadFileCollection(ctx.contextId, ctx.contextKey || null, true, chatId)
|
|
1476
|
+
: await loadFileCollectionAll(ctx.contextId, ctx.contextKey || null);
|
|
1474
1477
|
|
|
1475
1478
|
// Build set of existing identifiers from current collection
|
|
1476
1479
|
const existingHashes = new Set(collection.map(f => f.hash).filter(Boolean));
|
|
@@ -1488,6 +1491,27 @@ async function loadMergedFileCollection(agentContext) {
|
|
|
1488
1491
|
}
|
|
1489
1492
|
}
|
|
1490
1493
|
|
|
1494
|
+
// When chatId is null (includeAllChats=true), filter to only include files with inCollection set
|
|
1495
|
+
// Agent tools should only see files that are actually in the collection (have inCollection set)
|
|
1496
|
+
if (chatId === null) {
|
|
1497
|
+
collection = collection.filter(file => {
|
|
1498
|
+
const inCollection = file.inCollection;
|
|
1499
|
+
|
|
1500
|
+
// Exclude files without inCollection set or with empty inCollection array/string
|
|
1501
|
+
if (inCollection === undefined || inCollection === null || inCollection === false || inCollection === '') {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Exclude empty arrays (file not in any collection)
|
|
1506
|
+
if (Array.isArray(inCollection) && inCollection.length === 0) {
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Include files with inCollection set (truthy and non-empty)
|
|
1511
|
+
return true;
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1491
1515
|
return collection;
|
|
1492
1516
|
}
|
|
1493
1517
|
|
|
@@ -2491,6 +2515,65 @@ function isTextMimeType(mimeType) {
|
|
|
2491
2515
|
return false;
|
|
2492
2516
|
}
|
|
2493
2517
|
|
|
2518
|
+
/**
|
|
2519
|
+
* Build a standardized JSON response for file creation tools (image, video, slides).
|
|
2520
|
+
* Provides consistent format with structured file objects and instructional message.
|
|
2521
|
+
*
|
|
2522
|
+
* @param {Array} successfulFiles - Array of successful file objects, each with optional fileEntry and url/hash
|
|
2523
|
+
* @param {Object} options - Configuration options
|
|
2524
|
+
* @param {string} options.mediaType - Type of media: 'image' or 'video' (default: 'image')
|
|
2525
|
+
* @param {string} options.action - Action description for message: 'Image generation', 'Video generation', etc.
|
|
2526
|
+
* @param {Array} options.legacyUrls - Optional array of URLs for backward compatibility (imageUrls field)
|
|
2527
|
+
* @returns {string} JSON string with success, count, message, files, and optional imageUrls
|
|
2528
|
+
*/
|
|
2529
|
+
function buildFileCreationResponse(successfulFiles, options = {}) {
|
|
2530
|
+
const {
|
|
2531
|
+
mediaType = 'image',
|
|
2532
|
+
action = 'Generation',
|
|
2533
|
+
legacyUrls = []
|
|
2534
|
+
} = options;
|
|
2535
|
+
|
|
2536
|
+
const files = successfulFiles.map((item) => {
|
|
2537
|
+
if (item.fileEntry) {
|
|
2538
|
+
const fe = item.fileEntry;
|
|
2539
|
+
return {
|
|
2540
|
+
hash: fe.hash || null,
|
|
2541
|
+
displayFilename: fe.displayFilename || null,
|
|
2542
|
+
url: fe.url || item.url,
|
|
2543
|
+
addedDate: fe.addedDate || null,
|
|
2544
|
+
tags: Array.isArray(fe.tags) ? fe.tags : []
|
|
2545
|
+
};
|
|
2546
|
+
} else {
|
|
2547
|
+
return {
|
|
2548
|
+
hash: item.hash || null,
|
|
2549
|
+
displayFilename: null,
|
|
2550
|
+
url: item.url,
|
|
2551
|
+
addedDate: null,
|
|
2552
|
+
tags: []
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
const count = files.length;
|
|
2558
|
+
const displayInstruction = mediaType === 'video'
|
|
2559
|
+
? 'Display videos using markdown link: [video description](url).'
|
|
2560
|
+
: 'Display images using markdown: .';
|
|
2561
|
+
|
|
2562
|
+
const response = {
|
|
2563
|
+
success: true,
|
|
2564
|
+
count: count,
|
|
2565
|
+
message: `${action} complete. ${count} ${mediaType}(s) uploaded and added to file collection. ${displayInstruction} Reference files by hash or displayFilename.`,
|
|
2566
|
+
files: files
|
|
2567
|
+
};
|
|
2568
|
+
|
|
2569
|
+
// Add legacyUrls as imageUrls for backward compatibility if provided
|
|
2570
|
+
if (legacyUrls && legacyUrls.length > 0) {
|
|
2571
|
+
response.imageUrls = legacyUrls;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
return JSON.stringify(response);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2494
2577
|
export {
|
|
2495
2578
|
computeFileHash,
|
|
2496
2579
|
computeBufferHash,
|
|
@@ -2522,6 +2605,7 @@ export {
|
|
|
2522
2605
|
getRedisClient,
|
|
2523
2606
|
checkHashExists,
|
|
2524
2607
|
ensureShortLivedUrl,
|
|
2608
|
+
buildFileCreationResponse,
|
|
2525
2609
|
uploadFileToCloud,
|
|
2526
2610
|
uploadImageToCloud,
|
|
2527
2611
|
resolveFileHashesToContent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.30",
|
|
4
4
|
"description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"repository": {
|
|
@@ -83,8 +83,6 @@ export default {
|
|
|
83
83
|
try {
|
|
84
84
|
const { codingTask, userMessage, inputFiles, codingTaskKeywords, contextId } = args;
|
|
85
85
|
|
|
86
|
-
// pathwayResolver.executePathway should have already extracted contextId from agentContext,
|
|
87
|
-
// but validate it's present as a safety check
|
|
88
86
|
if (!contextId) {
|
|
89
87
|
throw new Error("contextId is required. It should be provided via agentContext or contextId parameter.");
|
|
90
88
|
}
|
|
@@ -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, loadMergedFileCollection,
|
|
5
|
+
import { uploadFileToCloud, findFileInCollection, loadMergedFileCollection, getMimeTypeFromFilename, deleteFileByHash, isTextMimeType, updateFileMetadata, writeFileDataToRedis, invalidateFileCollectionCache, getActualContentMimeType } from '../../../../lib/fileUtils.js';
|
|
6
6
|
|
|
7
7
|
// Maximum file size for editing (50MB) - prevents memory blowup on huge files
|
|
8
8
|
const MAX_EDITABLE_FILE_SIZE = 50 * 1024 * 1024;
|
|
@@ -147,17 +147,15 @@ export default {
|
|
|
147
147
|
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
148
148
|
const { file, startLine, endLine, content, oldString, newString, replaceAll = false, agentContext, chatId } = args;
|
|
149
149
|
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
150
|
+
const { contextId, contextKey } = args;
|
|
151
|
+
if (!contextId) {
|
|
152
152
|
const errorResult = {
|
|
153
153
|
success: false,
|
|
154
|
-
error: "
|
|
154
|
+
error: "contextId is required. It should be provided via agentContext or contextId parameter."
|
|
155
155
|
};
|
|
156
156
|
resolver.tool = JSON.stringify({ toolUsed: "EditFile" });
|
|
157
157
|
return JSON.stringify(errorResult);
|
|
158
158
|
}
|
|
159
|
-
const contextId = defaultCtx.contextId;
|
|
160
|
-
const contextKey = defaultCtx.contextKey || null;
|
|
161
159
|
|
|
162
160
|
// Determine which tool was called based on parameters
|
|
163
161
|
const isSearchReplace = oldString !== undefined && newString !== undefined;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Uses Redis hash maps (FileStoreMap:ctx:<contextId>) for storage
|
|
4
4
|
// Supports atomic rename/tag/notes updates via UpdateFileMetadata
|
|
5
5
|
import logger from '../../../../lib/logger.js';
|
|
6
|
-
import { addFileToCollection, loadFileCollection, loadMergedFileCollection, findFileInCollection, deleteFileByHash, updateFileMetadata, invalidateFileCollectionCache
|
|
6
|
+
import { addFileToCollection, loadFileCollection, loadFileCollectionAll, loadMergedFileCollection, findFileInCollection, deleteFileByHash, updateFileMetadata, invalidateFileCollectionCache } from '../../../../lib/fileUtils.js';
|
|
7
7
|
|
|
8
8
|
export default {
|
|
9
9
|
prompt: [],
|
|
@@ -82,6 +82,10 @@ export default {
|
|
|
82
82
|
type: "number",
|
|
83
83
|
description: "Optional: Maximum number of results to return (default: 20)"
|
|
84
84
|
},
|
|
85
|
+
includeAllChats: {
|
|
86
|
+
type: "boolean",
|
|
87
|
+
description: "Optional: Only use true if you need to search across all chats. Default (false) searches only the current chat, which is usually what you want."
|
|
88
|
+
},
|
|
85
89
|
userMessage: {
|
|
86
90
|
type: "string",
|
|
87
91
|
description: "A user-friendly message that describes what you're doing with this tool"
|
|
@@ -114,6 +118,10 @@ export default {
|
|
|
114
118
|
type: "number",
|
|
115
119
|
description: "Optional: Maximum number of results to return (default: 50)"
|
|
116
120
|
},
|
|
121
|
+
includeAllChats: {
|
|
122
|
+
type: "boolean",
|
|
123
|
+
description: "Optional: Only use true if you need to list files from all chats. Default (false) lists only the current chat, which is usually what you want."
|
|
124
|
+
},
|
|
117
125
|
userMessage: {
|
|
118
126
|
type: "string",
|
|
119
127
|
description: "A user-friendly message that describes what you're doing with this tool"
|
|
@@ -198,12 +206,10 @@ export default {
|
|
|
198
206
|
],
|
|
199
207
|
|
|
200
208
|
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
throw new Error("
|
|
209
|
+
const { contextId, contextKey } = args;
|
|
210
|
+
if (!contextId) {
|
|
211
|
+
throw new Error("contextId is required. It should be provided via agentContext or contextId parameter.");
|
|
204
212
|
}
|
|
205
|
-
const contextId = defaultCtx.contextId;
|
|
206
|
-
const contextKey = defaultCtx.contextKey || null;
|
|
207
213
|
const chatId = args.chatId || null;
|
|
208
214
|
|
|
209
215
|
// Determine which function was called based on which parameters are present
|
|
@@ -234,7 +240,9 @@ export default {
|
|
|
234
240
|
}
|
|
235
241
|
|
|
236
242
|
// Load collection and find the file
|
|
237
|
-
|
|
243
|
+
// Use loadFileCollectionAll to find files regardless of inCollection status
|
|
244
|
+
// This ensures files can be updated even if they're chat-specific
|
|
245
|
+
const collection = await loadFileCollectionAll(contextId, contextKey);
|
|
238
246
|
const foundFile = findFileInCollection(file, collection);
|
|
239
247
|
|
|
240
248
|
if (!foundFile) {
|
|
@@ -356,7 +364,7 @@ export default {
|
|
|
356
364
|
|
|
357
365
|
} else if (isSearch) {
|
|
358
366
|
// Search collection
|
|
359
|
-
const { query, tags: filterTags = [], limit = 20 } = args;
|
|
367
|
+
const { query, tags: filterTags = [], limit = 20, includeAllChats = false } = args;
|
|
360
368
|
|
|
361
369
|
if (!query || typeof query !== 'string') {
|
|
362
370
|
throw new Error("query is required and must be a string");
|
|
@@ -366,8 +374,15 @@ export default {
|
|
|
366
374
|
const safeFilterTags = Array.isArray(filterTags) ? filterTags : [];
|
|
367
375
|
const queryLower = query.toLowerCase();
|
|
368
376
|
|
|
377
|
+
// Normalize query for flexible matching: treat spaces, dashes, underscores as equivalent
|
|
378
|
+
const normalizeForSearch = (str) => str.toLowerCase().replace(/[-_\s]+/g, ' ').trim();
|
|
379
|
+
const queryNormalized = normalizeForSearch(query);
|
|
380
|
+
|
|
381
|
+
// Determine which chatId to use for filtering (null if includeAllChats is true)
|
|
382
|
+
const filterChatId = includeAllChats ? null : chatId;
|
|
383
|
+
|
|
369
384
|
// Load primary collection for lastAccessed updates (only update files in primary context)
|
|
370
|
-
const primaryFiles = await loadFileCollection(contextId, contextKey, false);
|
|
385
|
+
const primaryFiles = await loadFileCollection(contextId, contextKey, false, filterChatId);
|
|
371
386
|
const now = new Date().toISOString();
|
|
372
387
|
|
|
373
388
|
// Find matching files in primary collection and update lastAccessed directly
|
|
@@ -376,9 +391,9 @@ export default {
|
|
|
376
391
|
|
|
377
392
|
// Fallback to filename if displayFilename is not set (for files uploaded before displayFilename was added)
|
|
378
393
|
const displayFilename = file.displayFilename || file.filename || '';
|
|
379
|
-
const filenameMatch = displayFilename
|
|
380
|
-
const notesMatch = file.notes && file.notes
|
|
381
|
-
const tagMatch = Array.isArray(file.tags) && file.tags.some(tag => tag
|
|
394
|
+
const filenameMatch = normalizeForSearch(displayFilename).includes(queryNormalized);
|
|
395
|
+
const notesMatch = file.notes && normalizeForSearch(file.notes).includes(queryNormalized);
|
|
396
|
+
const tagMatch = Array.isArray(file.tags) && file.tags.some(tag => normalizeForSearch(tag).includes(queryNormalized));
|
|
382
397
|
const matchesQuery = filenameMatch || notesMatch || tagMatch;
|
|
383
398
|
|
|
384
399
|
const matchesTags = safeFilterTags.length === 0 ||
|
|
@@ -396,20 +411,26 @@ export default {
|
|
|
396
411
|
}
|
|
397
412
|
|
|
398
413
|
// Load merged collection for search results (includes all agentContext files)
|
|
399
|
-
|
|
414
|
+
// Filter by chatId if includeAllChats is false and chatId is available
|
|
415
|
+
// loadMergedFileCollection now handles inCollection filtering centrally
|
|
416
|
+
const updatedFiles = await loadMergedFileCollection(args.agentContext, filterChatId);
|
|
400
417
|
|
|
401
418
|
// Filter and sort results (for display only, not modifying)
|
|
402
419
|
let results = updatedFiles.filter(file => {
|
|
420
|
+
// Filter by query and tags
|
|
403
421
|
// Fallback to filename if displayFilename is not set
|
|
404
422
|
const displayFilename = file.displayFilename || file.filename || '';
|
|
405
423
|
const filename = file.filename || '';
|
|
406
424
|
|
|
407
425
|
// Check both displayFilename and filename for matches
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
const
|
|
426
|
+
// Use normalized matching (treating spaces, dashes, underscores as equivalent)
|
|
427
|
+
// so "News Corp" matches "News-Corp" and "News_Corp"
|
|
428
|
+
const displayFilenameNorm = normalizeForSearch(displayFilename);
|
|
429
|
+
const filenameNorm = normalizeForSearch(filename);
|
|
430
|
+
const filenameMatch = displayFilenameNorm.includes(queryNormalized) ||
|
|
431
|
+
(filename && filename !== displayFilename && filenameNorm.includes(queryNormalized));
|
|
432
|
+
const notesMatch = file.notes && normalizeForSearch(file.notes).includes(queryNormalized);
|
|
433
|
+
const tagMatch = Array.isArray(file.tags) && file.tags.some(tag => normalizeForSearch(tag).includes(queryNormalized));
|
|
413
434
|
|
|
414
435
|
const matchesQuery = filenameMatch || notesMatch || tagMatch;
|
|
415
436
|
|
|
@@ -426,8 +447,8 @@ export default {
|
|
|
426
447
|
// Fallback to filename if displayFilename is not set
|
|
427
448
|
const aDisplayFilename = a.displayFilename || a.filename || '';
|
|
428
449
|
const bDisplayFilename = b.displayFilename || b.filename || '';
|
|
429
|
-
const aFilenameMatch = aDisplayFilename
|
|
430
|
-
const bFilenameMatch = bDisplayFilename
|
|
450
|
+
const aFilenameMatch = normalizeForSearch(aDisplayFilename).includes(queryNormalized);
|
|
451
|
+
const bFilenameMatch = normalizeForSearch(bDisplayFilename).includes(queryNormalized);
|
|
431
452
|
if (aFilenameMatch && !bFilenameMatch) return -1;
|
|
432
453
|
if (!aFilenameMatch && bFilenameMatch) return 1;
|
|
433
454
|
return new Date(b.addedDate) - new Date(a.addedDate);
|
|
@@ -437,11 +458,28 @@ export default {
|
|
|
437
458
|
results = results.slice(0, limit);
|
|
438
459
|
|
|
439
460
|
resolver.tool = JSON.stringify({ toolUsed: "SearchFileCollection" });
|
|
461
|
+
|
|
462
|
+
// Build helpful message when no results found
|
|
463
|
+
let message;
|
|
464
|
+
if (results.length === 0) {
|
|
465
|
+
const suggestions = [];
|
|
466
|
+
if (chatId && !includeAllChats) {
|
|
467
|
+
suggestions.push('try includeAllChats=true to search across all chats');
|
|
468
|
+
}
|
|
469
|
+
suggestions.push('use ListFileCollection to see all available files');
|
|
470
|
+
|
|
471
|
+
message = `No files found matching "${query}". Count: 0. Suggestions: ${suggestions.join('; ')}.`;
|
|
472
|
+
} else {
|
|
473
|
+
message = `Found ${results.length} file(s) matching "${query}". Use the hash or displayFilename to reference files in other tools.`;
|
|
474
|
+
}
|
|
475
|
+
|
|
440
476
|
return JSON.stringify({
|
|
441
477
|
success: true,
|
|
442
478
|
count: results.length,
|
|
479
|
+
message,
|
|
443
480
|
files: results.map(f => ({
|
|
444
481
|
id: f.id,
|
|
482
|
+
hash: f.hash || null,
|
|
445
483
|
displayFilename: f.displayFilename || f.filename || null,
|
|
446
484
|
url: f.url,
|
|
447
485
|
gcs: f.gcs || null,
|
|
@@ -472,8 +510,9 @@ export default {
|
|
|
472
510
|
let filesToProcess = [];
|
|
473
511
|
|
|
474
512
|
// Load collection ONCE to find all files and their data
|
|
475
|
-
//
|
|
476
|
-
|
|
513
|
+
// Do NOT filter by chatId - remove should be able to delete files from any chat
|
|
514
|
+
// Use merged collection to include files from all agentContext contexts
|
|
515
|
+
const collection = await loadMergedFileCollection(args.agentContext, null);
|
|
477
516
|
|
|
478
517
|
// Resolve all files and collect their info in a single pass
|
|
479
518
|
for (const target of targetFiles) {
|
|
@@ -498,7 +537,7 @@ export default {
|
|
|
498
537
|
}
|
|
499
538
|
|
|
500
539
|
if (filesToProcess.length === 0 && notFoundFiles.length > 0) {
|
|
501
|
-
throw new Error(`No files found matching: ${notFoundFiles.join(', ')}
|
|
540
|
+
throw new Error(`No files found matching: ${notFoundFiles.join(', ')}. Try using the file hash, URL, or filename instead of ID. If the file was found in a search, use the hash or filename from the search results.`);
|
|
502
541
|
}
|
|
503
542
|
|
|
504
543
|
// Import helpers for reference counting
|
|
@@ -523,15 +562,23 @@ export default {
|
|
|
523
562
|
// No chatId context - fully remove
|
|
524
563
|
filesToFullyDelete.push(fileInfo);
|
|
525
564
|
} else {
|
|
526
|
-
//
|
|
527
|
-
const
|
|
565
|
+
// Check if current chatId is in the file's inCollection
|
|
566
|
+
const currentChatInCollection = Array.isArray(fileInfo.inCollection) && fileInfo.inCollection.includes(chatId);
|
|
528
567
|
|
|
529
|
-
if (
|
|
530
|
-
//
|
|
568
|
+
if (!currentChatInCollection) {
|
|
569
|
+
// File doesn't belong to current chat - fully remove it (cross-chat removal)
|
|
531
570
|
filesToFullyDelete.push(fileInfo);
|
|
532
571
|
} else {
|
|
533
|
-
//
|
|
534
|
-
|
|
572
|
+
// Remove this chatId from inCollection
|
|
573
|
+
const updatedInCollection = removeChatIdFromInCollection(fileInfo.inCollection, chatId);
|
|
574
|
+
|
|
575
|
+
if (updatedInCollection.length === 0) {
|
|
576
|
+
// No more references - fully delete
|
|
577
|
+
filesToFullyDelete.push(fileInfo);
|
|
578
|
+
} else {
|
|
579
|
+
// Still has references from other chats - just update inCollection
|
|
580
|
+
filesToUpdate.push({ ...fileInfo, updatedInCollection });
|
|
581
|
+
}
|
|
535
582
|
}
|
|
536
583
|
}
|
|
537
584
|
}
|
|
@@ -615,10 +662,15 @@ export default {
|
|
|
615
662
|
|
|
616
663
|
} else {
|
|
617
664
|
// List collection (read-only, no locking needed)
|
|
618
|
-
const { tags: filterTags = [], sortBy = 'date', limit = 50 } = args;
|
|
665
|
+
const { tags: filterTags = [], sortBy = 'date', limit = 50, includeAllChats = false } = args;
|
|
666
|
+
|
|
667
|
+
// Determine which chatId to use for filtering (null if includeAllChats is true)
|
|
668
|
+
const filterChatId = includeAllChats ? null : chatId;
|
|
619
669
|
|
|
620
670
|
// Use merged collection to include files from all agentContext contexts
|
|
621
|
-
|
|
671
|
+
// Filter by chatId if includeAllChats is false and chatId is available
|
|
672
|
+
// loadMergedFileCollection now handles inCollection filtering centrally
|
|
673
|
+
const collection = await loadMergedFileCollection(args.agentContext, filterChatId);
|
|
622
674
|
let results = collection;
|
|
623
675
|
|
|
624
676
|
// Filter by tags if provided
|
|
@@ -646,12 +698,32 @@ export default {
|
|
|
646
698
|
results = results.slice(0, limit);
|
|
647
699
|
|
|
648
700
|
resolver.tool = JSON.stringify({ toolUsed: "ListFileCollection" });
|
|
701
|
+
|
|
702
|
+
// Build helpful message
|
|
703
|
+
let message;
|
|
704
|
+
if (results.length === 0) {
|
|
705
|
+
const suggestions = [];
|
|
706
|
+
if (chatId && !includeAllChats) {
|
|
707
|
+
suggestions.push('try includeAllChats=true to see files from all chats');
|
|
708
|
+
}
|
|
709
|
+
if (filterTags.length > 0) {
|
|
710
|
+
suggestions.push('remove tag filters to see more files');
|
|
711
|
+
}
|
|
712
|
+
message = suggestions.length > 0
|
|
713
|
+
? `No files in collection. Suggestions: ${suggestions.join('; ')}.`
|
|
714
|
+
: 'No files in collection.';
|
|
715
|
+
} else {
|
|
716
|
+
message = (results.length === collection.length) ? `Showing all ${results.length} file(s). These are ALL of the files that you can access. Use the hash or displayFilename to reference files in other tools.` : `Showing ${results.length} of ${collection.length} file(s). Use the hash or displayFilename to reference files in other tools.`;
|
|
717
|
+
}
|
|
718
|
+
|
|
649
719
|
return JSON.stringify({
|
|
650
720
|
success: true,
|
|
651
721
|
count: results.length,
|
|
652
722
|
totalFiles: collection.length,
|
|
723
|
+
message,
|
|
653
724
|
files: results.map(f => ({
|
|
654
725
|
id: f.id,
|
|
726
|
+
hash: f.hash || null,
|
|
655
727
|
displayFilename: f.displayFilename || f.filename || null,
|
|
656
728
|
url: f.url,
|
|
657
729
|
gcs: f.gcs || null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sys_tool_image.js
|
|
2
2
|
// Entity tool that creates and modifies images for the entity to show to the user
|
|
3
3
|
import { callPathway } from '../../../../lib/pathwayTools.js';
|
|
4
|
-
import { uploadFileToCloud, addFileToCollection, resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
4
|
+
import { uploadFileToCloud, addFileToCollection, resolveFileParameter, buildFileCreationResponse } from '../../../../lib/fileUtils.js';
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
prompt: [],
|
|
@@ -269,36 +269,13 @@ export default {
|
|
|
269
269
|
};
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
-
// Return image info in the same format as availableFiles for the text message
|
|
273
|
-
// Format: hash | filename | url | date | tags
|
|
274
|
-
const imageList = successfulImages.map((img) => {
|
|
275
|
-
if (img.fileEntry) {
|
|
276
|
-
// Use the file entry data from addFileToCollection
|
|
277
|
-
const fe = img.fileEntry;
|
|
278
|
-
const dateStr = fe.addedDate
|
|
279
|
-
? new Date(fe.addedDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
280
|
-
: '';
|
|
281
|
-
const tagsStr = Array.isArray(fe.tags) ? fe.tags.join(',') : '';
|
|
282
|
-
return `${fe.hash || ''} | ${fe.displayFilename || ''} | ${fe.url || img.url} | ${dateStr} | ${tagsStr}`;
|
|
283
|
-
} else {
|
|
284
|
-
// Fallback if file collection wasn't available
|
|
285
|
-
return `${img.hash || 'unknown'} | | ${img.url} | |`;
|
|
286
|
-
}
|
|
287
|
-
}).join('\n');
|
|
288
|
-
|
|
289
|
-
const count = successfulImages.length;
|
|
290
272
|
const isModification = args.inputImages && Array.isArray(args.inputImages) && args.inputImages.length > 0;
|
|
291
|
-
|
|
292
|
-
// Make the success message very explicit so the agent knows files were created and added to collection
|
|
293
|
-
// This format matches availableFiles so the agent can reference them by hash/filename
|
|
294
273
|
const action = isModification ? 'Image modification' : 'Image generation';
|
|
295
|
-
const message = `${action} completed successfully. ${count} image${count > 1 ? 's have' : ' has'} been generated, uploaded to cloud storage, and added to your file collection. The image${count > 1 ? 's are' : ' is'} now available in your file collection:\n\n${imageList}\n\nYou can reference these images by their hash, filename, or URL in future tool calls.`;
|
|
296
274
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
imageUrls: imageUrls
|
|
275
|
+
result = buildFileCreationResponse(successfulImages, {
|
|
276
|
+
mediaType: 'image',
|
|
277
|
+
action: action,
|
|
278
|
+
legacyUrls: imageUrls
|
|
302
279
|
});
|
|
303
280
|
}
|
|
304
281
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sys_tool_image_gemini.js
|
|
2
2
|
// Entity tool that creates and modifies images for the entity to show to the user
|
|
3
3
|
import { callPathway } from '../../../../lib/pathwayTools.js';
|
|
4
|
-
import { uploadImageToCloud, addFileToCollection, resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
4
|
+
import { uploadImageToCloud, addFileToCollection, resolveFileParameter, buildFileCreationResponse } from '../../../../lib/fileUtils.js';
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
prompt: [],
|
|
@@ -247,34 +247,10 @@ export default {
|
|
|
247
247
|
};
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// Use the file entry data from addFileToCollection
|
|
255
|
-
const fe = img.fileEntry;
|
|
256
|
-
const dateStr = fe.addedDate
|
|
257
|
-
? new Date(fe.addedDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
258
|
-
: '';
|
|
259
|
-
const tagsStr = Array.isArray(fe.tags) ? fe.tags.join(',') : '';
|
|
260
|
-
return `${fe.hash || ''} | ${fe.displayFilename || ''} | ${fe.url || img.url} | ${dateStr} | ${tagsStr}`;
|
|
261
|
-
} else {
|
|
262
|
-
// Fallback if file collection wasn't available
|
|
263
|
-
return `${img.hash || 'unknown'} | | ${img.url} | |`;
|
|
264
|
-
}
|
|
265
|
-
}).join('\n');
|
|
266
|
-
|
|
267
|
-
const count = successfulImages.length;
|
|
268
|
-
|
|
269
|
-
// Make the success message very explicit so the agent knows files were created and added to collection
|
|
270
|
-
// This format matches availableFiles so the agent can reference them by hash/filename
|
|
271
|
-
const message = `Image generation completed successfully. ${count} image${count > 1 ? 's have' : ' has'} been generated, uploaded to cloud storage, and added to your file collection. The image${count > 1 ? 's are' : ' is'} now available in your file collection:\n\n${imageList}\n\nYou can reference these images by their hash, filename, or URL in future tool calls.`;
|
|
272
|
-
|
|
273
|
-
// Return JSON object with imageUrls (kept for backward compatibility, but explicit message should prevent looping)
|
|
274
|
-
return JSON.stringify({
|
|
275
|
-
success: true,
|
|
276
|
-
message: message,
|
|
277
|
-
imageUrls: imageUrls
|
|
250
|
+
return buildFileCreationResponse(successfulImages, {
|
|
251
|
+
mediaType: 'image',
|
|
252
|
+
action: 'Image generation',
|
|
253
|
+
legacyUrls: imageUrls
|
|
278
254
|
});
|
|
279
255
|
} else {
|
|
280
256
|
throw new Error('Image generation failed: Images were generated but could not be uploaded to storage');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sys_tool_slides_gemini.js
|
|
2
2
|
// Entity tool that creates slides, infographics, and presentations using Gemini 3 Pro image generation
|
|
3
3
|
import { callPathway } from '../../../../lib/pathwayTools.js';
|
|
4
|
-
import { uploadImageToCloud, addFileToCollection, resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
4
|
+
import { uploadImageToCloud, addFileToCollection, resolveFileParameter, buildFileCreationResponse } from '../../../../lib/fileUtils.js';
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
prompt: [],
|
|
@@ -61,6 +61,7 @@ export default {
|
|
|
61
61
|
}],
|
|
62
62
|
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
63
63
|
const pathwayResolver = resolver;
|
|
64
|
+
const chatId = args.chatId || null;
|
|
64
65
|
|
|
65
66
|
try {
|
|
66
67
|
let model = "gemini-pro-3-image";
|
|
@@ -184,7 +185,8 @@ export default {
|
|
|
184
185
|
imageHash,
|
|
185
186
|
null,
|
|
186
187
|
pathwayResolver,
|
|
187
|
-
true // permanent => retention=permanent
|
|
188
|
+
true, // permanent => retention=permanent
|
|
189
|
+
chatId
|
|
188
190
|
);
|
|
189
191
|
|
|
190
192
|
// Use the file entry data for the return message
|
|
@@ -226,35 +228,10 @@ export default {
|
|
|
226
228
|
};
|
|
227
229
|
});
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// Use the file entry data from addFileToCollection
|
|
234
|
-
const fe = img.fileEntry;
|
|
235
|
-
const dateStr = fe.addedDate
|
|
236
|
-
? new Date(fe.addedDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
237
|
-
: '';
|
|
238
|
-
const tagsStr = Array.isArray(fe.tags) ? fe.tags.join(',') : '';
|
|
239
|
-
return `${fe.hash || ''} | ${fe.displayFilename || ''} | ${fe.url || img.url} | ${dateStr} | ${tagsStr}`;
|
|
240
|
-
} else {
|
|
241
|
-
// Fallback if file collection wasn't available
|
|
242
|
-
return `${img.hash || 'unknown'} | | ${img.url} | |`;
|
|
243
|
-
}
|
|
244
|
-
}).join('\n');
|
|
245
|
-
|
|
246
|
-
const count = successfulImages.length;
|
|
247
|
-
|
|
248
|
-
// Make the success message very explicit so the agent knows files were created and added to collection
|
|
249
|
-
// This format matches availableFiles so the agent can reference them by hash/filename
|
|
250
|
-
const message = `Slide/infographic generation completed successfully. ${count} image${count > 1 ? 's have' : ' has'} been generated, uploaded to cloud storage, and added to your file collection. The image${count > 1 ? 's are' : ' is'} now available in your file collection:\n\n${imageList}\n\nYou can reference these images by their hash, filename, or URL in future tool calls.`;
|
|
251
|
-
|
|
252
|
-
// Return JSON object with imageUrls (kept for backward compatibility, but explicit message should prevent looping)
|
|
253
|
-
// This prevents the agent from looping because it can't see the generated images
|
|
254
|
-
return JSON.stringify({
|
|
255
|
-
success: true,
|
|
256
|
-
message: message,
|
|
257
|
-
imageUrls: imageUrls
|
|
231
|
+
return buildFileCreationResponse(successfulImages, {
|
|
232
|
+
mediaType: 'image',
|
|
233
|
+
action: 'Slide/infographic generation',
|
|
234
|
+
legacyUrls: imageUrls
|
|
258
235
|
});
|
|
259
236
|
} else {
|
|
260
237
|
throw new Error('Slide generation failed: Content was generated but could not be uploaded to storage');
|
|
@@ -58,8 +58,6 @@ export default {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// pathwayResolver.executePathway should have already extracted contextId from agentContext,
|
|
62
|
-
// but validate it's present as a safety check
|
|
63
61
|
const { contextId, contextKey } = args;
|
|
64
62
|
if (!contextId) {
|
|
65
63
|
return JSON.stringify({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sys_tool_video_veo.js
|
|
2
2
|
// Entity tool that generates videos using Google Veo 3.1 Fast for the entity to show to the user
|
|
3
3
|
import { callPathway } from '../../../../lib/pathwayTools.js';
|
|
4
|
-
import { uploadFileToCloud, addFileToCollection, resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
4
|
+
import { uploadFileToCloud, addFileToCollection, resolveFileParameter, buildFileCreationResponse } from '../../../../lib/fileUtils.js';
|
|
5
5
|
import { config } from '../../../../config.js';
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
|
|
@@ -345,34 +345,10 @@ export default {
|
|
|
345
345
|
};
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// Use the file entry data from addFileToCollection
|
|
353
|
-
const fe = vid.fileEntry;
|
|
354
|
-
const dateStr = fe.addedDate
|
|
355
|
-
? new Date(fe.addedDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
356
|
-
: '';
|
|
357
|
-
const tagsStr = Array.isArray(fe.tags) ? fe.tags.join(',') : '';
|
|
358
|
-
return `${fe.hash || ''} | ${fe.displayFilename || ''} | ${fe.url || vid.url} | ${dateStr} | ${tagsStr}`;
|
|
359
|
-
} else {
|
|
360
|
-
// Fallback if file collection wasn't available
|
|
361
|
-
return `${vid.hash || 'unknown'} | | ${vid.url} | |`;
|
|
362
|
-
}
|
|
363
|
-
}).join('\n');
|
|
364
|
-
|
|
365
|
-
const count = successfulVideos.length;
|
|
366
|
-
|
|
367
|
-
// Make the success message very explicit so the agent knows files were created and added to collection
|
|
368
|
-
// This format matches availableFiles so the agent can reference them by hash/filename
|
|
369
|
-
const message = `Video generation completed successfully. ${count} video${count > 1 ? 's have' : ' has'} been generated, uploaded to cloud storage, and added to your file collection. The video${count > 1 ? 's are' : ' is'} now available in your file collection:\n\n${videoList}\n\nYou can reference these videos by their hash, filename, or URL in future tool calls. Videos can be displayed using markdown image syntax, e.g. `;
|
|
370
|
-
|
|
371
|
-
// Return JSON object with imageUrls (kept for backward compatibility, but explicit message should prevent looping)
|
|
372
|
-
return JSON.stringify({
|
|
373
|
-
success: true,
|
|
374
|
-
message: message,
|
|
375
|
-
imageUrls: imageUrls
|
|
348
|
+
return buildFileCreationResponse(successfulVideos, {
|
|
349
|
+
mediaType: 'video',
|
|
350
|
+
action: 'Video generation',
|
|
351
|
+
legacyUrls: imageUrls
|
|
376
352
|
});
|
|
377
353
|
} else {
|
|
378
354
|
// All videos failed to upload
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sys_tool_view_image.js
|
|
2
2
|
// Tool pathway that allows agents to view image files from the file collection
|
|
3
3
|
import logger from '../../../../lib/logger.js';
|
|
4
|
-
import { loadMergedFileCollection, findFileInCollection, ensureShortLivedUrl
|
|
4
|
+
import { loadMergedFileCollection, findFileInCollection, ensureShortLivedUrl } from '../../../../lib/fileUtils.js';
|
|
5
5
|
import { config } from '../../../../config.js';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
@@ -74,8 +74,7 @@ export default {
|
|
|
74
74
|
|
|
75
75
|
// Resolve to short-lived URL if possible
|
|
76
76
|
const fileHandlerUrl = config.get('whisperMediaApiUrl');
|
|
77
|
-
const
|
|
78
|
-
const fileWithShortLivedUrl = await ensureShortLivedUrl(foundFile, fileHandlerUrl, defaultCtx?.contextId || null);
|
|
77
|
+
const fileWithShortLivedUrl = await ensureShortLivedUrl(foundFile, fileHandlerUrl, args.contextId || null);
|
|
79
78
|
|
|
80
79
|
// Add to imageUrls array
|
|
81
80
|
imageUrls.push({
|
|
@@ -57,7 +57,7 @@ export default {
|
|
|
57
57
|
},
|
|
58
58
|
|
|
59
59
|
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
60
|
-
const { content, filename, tags = [], notes = '', contextId, contextKey } = args;
|
|
60
|
+
const { content, filename, tags = [], notes = '', contextId, contextKey, chatId } = args;
|
|
61
61
|
|
|
62
62
|
// Validate inputs and return JSON error if invalid
|
|
63
63
|
if (content === undefined || content === null) {
|
|
@@ -176,7 +176,8 @@ export default {
|
|
|
176
176
|
uploadResult.hash || null,
|
|
177
177
|
null, // fileUrl - not needed since we already uploaded
|
|
178
178
|
resolver,
|
|
179
|
-
true // permanent => retention=permanent
|
|
179
|
+
true, // permanent => retention=permanent
|
|
180
|
+
chatId || null
|
|
180
181
|
);
|
|
181
182
|
} catch (collectionError) {
|
|
182
183
|
// Log but don't fail - file collection is optional
|
|
@@ -185,6 +186,19 @@ export default {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
const fileSize = Buffer.byteLength(content, 'utf8');
|
|
189
|
+
|
|
190
|
+
// Create explicit success message that clearly indicates completion and file collection status
|
|
191
|
+
// This format matches image generation tools to prevent agent loops
|
|
192
|
+
let message;
|
|
193
|
+
if (fileEntry) {
|
|
194
|
+
// File was added to collection - provide explicit completion message with reference info
|
|
195
|
+
const fileRef = fileEntry.hash || fileEntry.id || filename;
|
|
196
|
+
message = `File creation completed successfully. The file "${filename}" (${formatFileSize(fileSize)}) has been written, uploaded to cloud storage, and added to your file collection. The file is now available in your file collection and can be referenced by its hash (${fileEntry.hash || 'N/A'}), filename (${filename}), fileId (${fileEntry.id || 'N/A'}), or URL in future tool calls.`;
|
|
197
|
+
} else {
|
|
198
|
+
// File was uploaded but not added to collection (no contextId provided)
|
|
199
|
+
message = `File "${filename}" written and uploaded successfully (${formatFileSize(fileSize)}). File URL: ${uploadResult.url}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
188
202
|
const result = {
|
|
189
203
|
success: true,
|
|
190
204
|
filename: filename,
|
|
@@ -194,7 +208,7 @@ export default {
|
|
|
194
208
|
fileId: fileEntry?.id || null,
|
|
195
209
|
size: fileSize,
|
|
196
210
|
sizeFormatted: formatFileSize(fileSize),
|
|
197
|
-
message:
|
|
211
|
+
message: message
|
|
198
212
|
};
|
|
199
213
|
|
|
200
214
|
resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
|
|
@@ -2389,3 +2389,324 @@ test('loadMergedFileCollection should dedupe files present in both contexts', as
|
|
|
2389
2389
|
}
|
|
2390
2390
|
}
|
|
2391
2391
|
});
|
|
2392
|
+
|
|
2393
|
+
test('File collection: SearchFileCollection filters by chatId by default', async t => {
|
|
2394
|
+
const contextId = createTestContext();
|
|
2395
|
+
const chatId1 = 'chat-1';
|
|
2396
|
+
const chatId2 = 'chat-2';
|
|
2397
|
+
|
|
2398
|
+
try {
|
|
2399
|
+
const { addFileToCollection } = await import('../../../../lib/fileUtils.js');
|
|
2400
|
+
|
|
2401
|
+
// Add file to chat-1
|
|
2402
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat1-file.pdf', null, 'chat1-file.pdf', [], '', 'hash-chat1', null, null, false, chatId1);
|
|
2403
|
+
|
|
2404
|
+
// Add file to chat-2
|
|
2405
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat2-file.pdf', null, 'chat2-file.pdf', [], '', 'hash-chat2', null, null, false, chatId2);
|
|
2406
|
+
|
|
2407
|
+
// Add global file (no chatId)
|
|
2408
|
+
await addFileToCollection(contextId, null, 'https://example.com/global-file.pdf', null, 'global-file.pdf', [], '', 'hash-global', null, null, false, null);
|
|
2409
|
+
|
|
2410
|
+
// Search from chat-1 - should only see chat-1 file and global file
|
|
2411
|
+
const result1 = await callPathway('sys_tool_file_collection', {
|
|
2412
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2413
|
+
chatId: chatId1,
|
|
2414
|
+
query: 'file',
|
|
2415
|
+
userMessage: 'Search from chat-1'
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
const parsed1 = JSON.parse(result1);
|
|
2419
|
+
t.is(parsed1.success, true);
|
|
2420
|
+
t.is(parsed1.count, 2, 'Should find chat-1 file and global file');
|
|
2421
|
+
const filenames1 = parsed1.files.map(f => f.displayFilename);
|
|
2422
|
+
t.true(filenames1.includes('chat1-file.pdf'), 'Should include chat-1 file');
|
|
2423
|
+
t.true(filenames1.includes('global-file.pdf'), 'Should include global file');
|
|
2424
|
+
t.false(filenames1.includes('chat2-file.pdf'), 'Should not include chat-2 file');
|
|
2425
|
+
|
|
2426
|
+
// Search from chat-2 - should only see chat-2 file and global file
|
|
2427
|
+
const result2 = await callPathway('sys_tool_file_collection', {
|
|
2428
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2429
|
+
chatId: chatId2,
|
|
2430
|
+
query: 'file',
|
|
2431
|
+
userMessage: 'Search from chat-2'
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
const parsed2 = JSON.parse(result2);
|
|
2435
|
+
t.is(parsed2.success, true);
|
|
2436
|
+
t.is(parsed2.count, 2, 'Should find chat-2 file and global file');
|
|
2437
|
+
const filenames2 = parsed2.files.map(f => f.displayFilename);
|
|
2438
|
+
t.true(filenames2.includes('chat2-file.pdf'), 'Should include chat-2 file');
|
|
2439
|
+
t.true(filenames2.includes('global-file.pdf'), 'Should include global file');
|
|
2440
|
+
t.false(filenames2.includes('chat1-file.pdf'), 'Should not include chat-1 file');
|
|
2441
|
+
|
|
2442
|
+
// Search without chatId - should see all files (no filtering when chatId not provided)
|
|
2443
|
+
const result3 = await callPathway('sys_tool_file_collection', {
|
|
2444
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2445
|
+
query: 'file',
|
|
2446
|
+
userMessage: 'Search without chatId'
|
|
2447
|
+
});
|
|
2448
|
+
|
|
2449
|
+
const parsed3 = JSON.parse(result3);
|
|
2450
|
+
t.is(parsed3.success, true);
|
|
2451
|
+
t.is(parsed3.count, 3, 'Should find all files when chatId not provided');
|
|
2452
|
+
const filenames3 = parsed3.files.map(f => f.displayFilename);
|
|
2453
|
+
t.true(filenames3.includes('chat1-file.pdf'), 'Should include chat-1 file');
|
|
2454
|
+
t.true(filenames3.includes('chat2-file.pdf'), 'Should include chat-2 file');
|
|
2455
|
+
t.true(filenames3.includes('global-file.pdf'), 'Should include global file');
|
|
2456
|
+
} finally {
|
|
2457
|
+
await cleanup(contextId);
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
test('File collection: SearchFileCollection with includeAllChats=true shows all files', async t => {
|
|
2462
|
+
const contextId = createTestContext();
|
|
2463
|
+
const chatId1 = 'chat-1';
|
|
2464
|
+
const chatId2 = 'chat-2';
|
|
2465
|
+
|
|
2466
|
+
try {
|
|
2467
|
+
const { addFileToCollection } = await import('../../../../lib/fileUtils.js');
|
|
2468
|
+
|
|
2469
|
+
// Add file to chat-1
|
|
2470
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat1-file.pdf', null, 'chat1-file.pdf', [], '', 'hash-chat1', null, null, false, chatId1);
|
|
2471
|
+
|
|
2472
|
+
// Add file to chat-2
|
|
2473
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat2-file.pdf', null, 'chat2-file.pdf', [], '', 'hash-chat2', null, null, false, chatId2);
|
|
2474
|
+
|
|
2475
|
+
// Add global file
|
|
2476
|
+
await addFileToCollection(contextId, null, 'https://example.com/global-file.pdf', null, 'global-file.pdf', [], '', 'hash-global', null, null, false, null);
|
|
2477
|
+
|
|
2478
|
+
// Search from chat-1 with includeAllChats=true - should see all files
|
|
2479
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
2480
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2481
|
+
chatId: chatId1,
|
|
2482
|
+
query: 'file',
|
|
2483
|
+
includeAllChats: true,
|
|
2484
|
+
userMessage: 'Search all chats from chat-1'
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
const parsed = JSON.parse(result);
|
|
2488
|
+
t.is(parsed.success, true);
|
|
2489
|
+
t.is(parsed.count, 3, 'Should find all files from all chats');
|
|
2490
|
+
const filenames = parsed.files.map(f => f.displayFilename);
|
|
2491
|
+
t.true(filenames.includes('chat1-file.pdf'), 'Should include chat-1 file');
|
|
2492
|
+
t.true(filenames.includes('chat2-file.pdf'), 'Should include chat-2 file');
|
|
2493
|
+
t.true(filenames.includes('global-file.pdf'), 'Should include global file');
|
|
2494
|
+
} finally {
|
|
2495
|
+
await cleanup(contextId);
|
|
2496
|
+
}
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
test('File collection: ListFileCollection filters by chatId by default', async t => {
|
|
2500
|
+
const contextId = createTestContext();
|
|
2501
|
+
const chatId1 = 'chat-1';
|
|
2502
|
+
const chatId2 = 'chat-2';
|
|
2503
|
+
|
|
2504
|
+
try {
|
|
2505
|
+
const { addFileToCollection } = await import('../../../../lib/fileUtils.js');
|
|
2506
|
+
|
|
2507
|
+
// Add file to chat-1
|
|
2508
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat1-file.pdf', null, 'chat1-file.pdf', [], '', 'hash-chat1', null, null, false, chatId1);
|
|
2509
|
+
|
|
2510
|
+
// Add file to chat-2
|
|
2511
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat2-file.pdf', null, 'chat2-file.pdf', [], '', 'hash-chat2', null, null, false, chatId2);
|
|
2512
|
+
|
|
2513
|
+
// Add global file
|
|
2514
|
+
await addFileToCollection(contextId, null, 'https://example.com/global-file.pdf', null, 'global-file.pdf', [], '', 'hash-global', null, null, false, null);
|
|
2515
|
+
|
|
2516
|
+
// List from chat-1 - should only see chat-1 file and global file
|
|
2517
|
+
const result1 = await callPathway('sys_tool_file_collection', {
|
|
2518
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2519
|
+
chatId: chatId1,
|
|
2520
|
+
userMessage: 'List files from chat-1'
|
|
2521
|
+
});
|
|
2522
|
+
|
|
2523
|
+
const parsed1 = JSON.parse(result1);
|
|
2524
|
+
t.is(parsed1.success, true);
|
|
2525
|
+
t.is(parsed1.count, 2, 'Should find chat-1 file and global file');
|
|
2526
|
+
t.is(parsed1.totalFiles, 2, 'Total should match count');
|
|
2527
|
+
const filenames1 = parsed1.files.map(f => f.displayFilename);
|
|
2528
|
+
t.true(filenames1.includes('chat1-file.pdf'), 'Should include chat-1 file');
|
|
2529
|
+
t.true(filenames1.includes('global-file.pdf'), 'Should include global file');
|
|
2530
|
+
t.false(filenames1.includes('chat2-file.pdf'), 'Should not include chat-2 file');
|
|
2531
|
+
|
|
2532
|
+
// List from chat-2 - should only see chat-2 file and global file
|
|
2533
|
+
const result2 = await callPathway('sys_tool_file_collection', {
|
|
2534
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2535
|
+
chatId: chatId2,
|
|
2536
|
+
userMessage: 'List files from chat-2'
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
const parsed2 = JSON.parse(result2);
|
|
2540
|
+
t.is(parsed2.success, true);
|
|
2541
|
+
t.is(parsed2.count, 2, 'Should find chat-2 file and global file');
|
|
2542
|
+
const filenames2 = parsed2.files.map(f => f.displayFilename);
|
|
2543
|
+
t.true(filenames2.includes('chat2-file.pdf'), 'Should include chat-2 file');
|
|
2544
|
+
t.true(filenames2.includes('global-file.pdf'), 'Should include global file');
|
|
2545
|
+
t.false(filenames2.includes('chat1-file.pdf'), 'Should not include chat-1 file');
|
|
2546
|
+
} finally {
|
|
2547
|
+
await cleanup(contextId);
|
|
2548
|
+
}
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
test('File collection: ListFileCollection with includeAllChats=true shows all files', async t => {
|
|
2552
|
+
const contextId = createTestContext();
|
|
2553
|
+
const chatId1 = 'chat-1';
|
|
2554
|
+
const chatId2 = 'chat-2';
|
|
2555
|
+
|
|
2556
|
+
try {
|
|
2557
|
+
const { addFileToCollection } = await import('../../../../lib/fileUtils.js');
|
|
2558
|
+
|
|
2559
|
+
// Add file to chat-1
|
|
2560
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat1-file.pdf', null, 'chat1-file.pdf', [], '', 'hash-chat1', null, null, false, chatId1);
|
|
2561
|
+
|
|
2562
|
+
// Add file to chat-2
|
|
2563
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat2-file.pdf', null, 'chat2-file.pdf', [], '', 'hash-chat2', null, null, false, chatId2);
|
|
2564
|
+
|
|
2565
|
+
// Add global file
|
|
2566
|
+
await addFileToCollection(contextId, null, 'https://example.com/global-file.pdf', null, 'global-file.pdf', [], '', 'hash-global', null, null, false, null);
|
|
2567
|
+
|
|
2568
|
+
// List from chat-1 with includeAllChats=true - should see all files
|
|
2569
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
2570
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2571
|
+
chatId: chatId1,
|
|
2572
|
+
includeAllChats: true,
|
|
2573
|
+
userMessage: 'List all files from chat-1'
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
const parsed = JSON.parse(result);
|
|
2577
|
+
t.is(parsed.success, true);
|
|
2578
|
+
t.is(parsed.count, 3, 'Should find all files from all chats');
|
|
2579
|
+
t.is(parsed.totalFiles, 3, 'Total should match count');
|
|
2580
|
+
const filenames = parsed.files.map(f => f.displayFilename);
|
|
2581
|
+
t.true(filenames.includes('chat1-file.pdf'), 'Should include chat-1 file');
|
|
2582
|
+
t.true(filenames.includes('chat2-file.pdf'), 'Should include chat-2 file');
|
|
2583
|
+
t.true(filenames.includes('global-file.pdf'), 'Should include global file');
|
|
2584
|
+
} finally {
|
|
2585
|
+
await cleanup(contextId);
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
test('File collection: RemoveFileFromCollection can remove files from any chat', async t => {
|
|
2590
|
+
const contextId = createTestContext();
|
|
2591
|
+
const chatId1 = 'chat-1';
|
|
2592
|
+
const chatId2 = 'chat-2';
|
|
2593
|
+
|
|
2594
|
+
try {
|
|
2595
|
+
const { addFileToCollection } = await import('../../../../lib/fileUtils.js');
|
|
2596
|
+
|
|
2597
|
+
// Add file to chat-1
|
|
2598
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat1-file.pdf', null, 'chat1-file.pdf', [], '', 'hash-chat1', null, null, false, chatId1);
|
|
2599
|
+
|
|
2600
|
+
// Add file to chat-2
|
|
2601
|
+
await addFileToCollection(contextId, null, 'https://example.com/chat2-file.pdf', null, 'chat2-file.pdf', [], '', 'hash-chat2', null, null, false, chatId2);
|
|
2602
|
+
|
|
2603
|
+
// Add global file
|
|
2604
|
+
await addFileToCollection(contextId, null, 'https://example.com/global-file.pdf', null, 'global-file.pdf', [], '', 'hash-global', null, null, false, null);
|
|
2605
|
+
|
|
2606
|
+
// Search from chat-1 to get the file ID for chat-2's file
|
|
2607
|
+
const searchResult = await callPathway('sys_tool_file_collection', {
|
|
2608
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2609
|
+
chatId: chatId1,
|
|
2610
|
+
query: 'chat2',
|
|
2611
|
+
includeAllChats: true,
|
|
2612
|
+
userMessage: 'Search all chats to find chat-2 file'
|
|
2613
|
+
});
|
|
2614
|
+
const searchParsed = JSON.parse(searchResult);
|
|
2615
|
+
t.is(searchParsed.success, true);
|
|
2616
|
+
const chat2File = searchParsed.files.find(f => f.displayFilename === 'chat2-file.pdf');
|
|
2617
|
+
t.truthy(chat2File, 'Should find chat-2 file when searching all chats');
|
|
2618
|
+
const chat2FileId = chat2File.id || chat2File.hash || chat2File.url;
|
|
2619
|
+
|
|
2620
|
+
// Remove chat-2's file from chat-1 context (cross-chat removal)
|
|
2621
|
+
const removeResult = await callPathway('sys_tool_file_collection', {
|
|
2622
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2623
|
+
chatId: chatId1, // Calling from chat-1
|
|
2624
|
+
fileIds: [chat2FileId],
|
|
2625
|
+
userMessage: 'Remove chat-2 file from chat-1'
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
const removeParsed = JSON.parse(removeResult);
|
|
2629
|
+
t.is(removeParsed.success, true);
|
|
2630
|
+
t.is(removeParsed.removedCount, 1);
|
|
2631
|
+
t.is(removeParsed.removedFiles.length, 1);
|
|
2632
|
+
t.is(removeParsed.removedFiles[0].displayFilename, 'chat2-file.pdf');
|
|
2633
|
+
|
|
2634
|
+
// Verify chat-2 file is gone (search from chat-2)
|
|
2635
|
+
const verifyResult = await callPathway('sys_tool_file_collection', {
|
|
2636
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2637
|
+
chatId: chatId2,
|
|
2638
|
+
userMessage: 'List files from chat-2'
|
|
2639
|
+
});
|
|
2640
|
+
const verifyParsed = JSON.parse(verifyResult);
|
|
2641
|
+
t.is(verifyParsed.success, true);
|
|
2642
|
+
t.is(verifyParsed.count, 1, 'Should only have global file left');
|
|
2643
|
+
t.true(verifyParsed.files.some(f => f.displayFilename === 'global-file.pdf'));
|
|
2644
|
+
t.false(verifyParsed.files.some(f => f.displayFilename === 'chat2-file.pdf'));
|
|
2645
|
+
|
|
2646
|
+
// Verify chat-1 file is still there
|
|
2647
|
+
const chat1Result = await callPathway('sys_tool_file_collection', {
|
|
2648
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2649
|
+
chatId: chatId1,
|
|
2650
|
+
userMessage: 'List files from chat-1'
|
|
2651
|
+
});
|
|
2652
|
+
const chat1Parsed = JSON.parse(chat1Result);
|
|
2653
|
+
t.is(chat1Parsed.success, true);
|
|
2654
|
+
t.is(chat1Parsed.count, 2, 'Should have chat-1 file and global file');
|
|
2655
|
+
t.true(chat1Parsed.files.some(f => f.displayFilename === 'chat1-file.pdf'));
|
|
2656
|
+
t.true(chat1Parsed.files.some(f => f.displayFilename === 'global-file.pdf'));
|
|
2657
|
+
} finally {
|
|
2658
|
+
await cleanup(contextId);
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
test('File collection: SearchFileCollection normalizes separators (space/dash/underscore matching)', async t => {
|
|
2663
|
+
const contextId = createTestContext();
|
|
2664
|
+
|
|
2665
|
+
try {
|
|
2666
|
+
const { addFileToCollection } = await import('../../../../lib/fileUtils.js');
|
|
2667
|
+
|
|
2668
|
+
// Add files with different separator conventions
|
|
2669
|
+
await addFileToCollection(contextId, null, 'https://example.com/news-corp-report.pdf', null, 'News-Corp-Report.pdf', [], '', 'hash-dashes', null, null, false, null);
|
|
2670
|
+
await addFileToCollection(contextId, null, 'https://example.com/news_corp_annual.pdf', null, 'News_Corp_Annual.pdf', [], '', 'hash-underscores', null, null, false, null);
|
|
2671
|
+
await addFileToCollection(contextId, null, 'https://example.com/unrelated.pdf', null, 'Unrelated-File.pdf', [], '', 'hash-unrelated', null, null, false, null);
|
|
2672
|
+
|
|
2673
|
+
// Search with space: "News Corp" should match both "News-Corp" and "News_Corp"
|
|
2674
|
+
const result = await callPathway('sys_tool_file_collection', {
|
|
2675
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2676
|
+
query: 'News Corp',
|
|
2677
|
+
userMessage: 'Search with spaces'
|
|
2678
|
+
});
|
|
2679
|
+
|
|
2680
|
+
const parsed = JSON.parse(result);
|
|
2681
|
+
t.is(parsed.success, true);
|
|
2682
|
+
t.is(parsed.count, 2, 'Should find both files with different separators');
|
|
2683
|
+
const filenames = parsed.files.map(f => f.displayFilename);
|
|
2684
|
+
t.true(filenames.includes('News-Corp-Report.pdf'), 'Should match dash-separated');
|
|
2685
|
+
t.true(filenames.includes('News_Corp_Annual.pdf'), 'Should match underscore-separated');
|
|
2686
|
+
t.false(filenames.includes('Unrelated-File.pdf'), 'Should not match unrelated file');
|
|
2687
|
+
|
|
2688
|
+
// Search with dash: "News-Corp" should also match both
|
|
2689
|
+
const result2 = await callPathway('sys_tool_file_collection', {
|
|
2690
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2691
|
+
query: 'News-Corp',
|
|
2692
|
+
userMessage: 'Search with dashes'
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
const parsed2 = JSON.parse(result2);
|
|
2696
|
+
t.is(parsed2.success, true);
|
|
2697
|
+
t.is(parsed2.count, 2, 'Dash search should also find both');
|
|
2698
|
+
|
|
2699
|
+
// Search with underscore: "News_Corp" should also match both
|
|
2700
|
+
const result3 = await callPathway('sys_tool_file_collection', {
|
|
2701
|
+
agentContext: [{ contextId, contextKey: null, default: true }],
|
|
2702
|
+
query: 'News_Corp',
|
|
2703
|
+
userMessage: 'Search with underscores'
|
|
2704
|
+
});
|
|
2705
|
+
|
|
2706
|
+
const parsed3 = JSON.parse(result3);
|
|
2707
|
+
t.is(parsed3.success, true);
|
|
2708
|
+
t.is(parsed3.count, 2, 'Underscore search should also find both');
|
|
2709
|
+
} finally {
|
|
2710
|
+
await cleanup(contextId);
|
|
2711
|
+
}
|
|
2712
|
+
});
|
|
@@ -73,7 +73,7 @@ test('WriteFile: Write and upload text file', async t => {
|
|
|
73
73
|
t.is(parsed.filename, filename);
|
|
74
74
|
t.truthy(parsed.url);
|
|
75
75
|
t.is(parsed.size, Buffer.byteLength(content, 'utf8'));
|
|
76
|
-
t.true(parsed.message.includes('written
|
|
76
|
+
t.true(parsed.message.includes('written') && parsed.message.includes('uploaded'));
|
|
77
77
|
} finally {
|
|
78
78
|
await cleanup(contextId);
|
|
79
79
|
}
|
|
@@ -70,7 +70,7 @@ test('WriteFile: Write and upload text file', async t => {
|
|
|
70
70
|
t.is(parsed.filename, filename);
|
|
71
71
|
t.truthy(parsed.url);
|
|
72
72
|
t.is(parsed.size, Buffer.byteLength(content, 'utf8'));
|
|
73
|
-
t.true(parsed.message.includes('written
|
|
73
|
+
t.true(parsed.message.includes('written') && parsed.message.includes('uploaded'));
|
|
74
74
|
|
|
75
75
|
// Verify it was added to file collection
|
|
76
76
|
const collection = await loadFileCollection(contextId, null, false);
|