@aj-archipelago/cortex 1.4.27 → 1.4.29
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 +96 -12
- package/package.json +1 -1
- package/pathways/system/entity/tools/sys_tool_codingagent.js +15 -6
- package/pathways/system/entity/tools/sys_tool_editfile.js +4 -6
- package/pathways/system/entity/tools/sys_tool_file_collection.js +100 -30
- 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 +11 -4
- 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
|
@@ -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.29",
|
|
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": {
|
|
@@ -81,19 +81,21 @@ export default {
|
|
|
81
81
|
|
|
82
82
|
executePathway: async ({args, resolver}) => {
|
|
83
83
|
try {
|
|
84
|
-
const { codingTask, userMessage, inputFiles, codingTaskKeywords } = args;
|
|
84
|
+
const { codingTask, userMessage, inputFiles, codingTaskKeywords, contextId } = args;
|
|
85
|
+
|
|
86
|
+
if (!contextId) {
|
|
87
|
+
throw new Error("contextId is required. It should be provided via agentContext or contextId parameter.");
|
|
88
|
+
}
|
|
85
89
|
|
|
86
90
|
let taskSuffix = "";
|
|
87
|
-
if (inputFiles) {
|
|
91
|
+
if (inputFiles && Array.isArray(inputFiles) && inputFiles.length > 0) {
|
|
88
92
|
if (!args.agentContext || !Array.isArray(args.agentContext) || args.agentContext.length === 0) {
|
|
89
93
|
throw new Error("agentContext is required when using the 'inputFiles' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
// Resolve file parameters to URLs
|
|
93
97
|
// inputFiles is an array of strings (file hashes or filenames)
|
|
94
|
-
const fileReferences =
|
|
95
|
-
? inputFiles.map(ref => String(ref).trim()).filter(ref => ref.length > 0)
|
|
96
|
-
: [];
|
|
98
|
+
const fileReferences = inputFiles.map(ref => String(ref).trim()).filter(ref => ref.length > 0);
|
|
97
99
|
|
|
98
100
|
const resolvedUrls = [];
|
|
99
101
|
const failedFiles = [];
|
|
@@ -142,8 +144,15 @@ export default {
|
|
|
142
144
|
const statusMessage = "⚠️ **Task Status**: The coding task has been started and is now running in the background. Don't make up any information about the task or task results - just say that it has been started and is running. The user will be able to see the progress and results of the task, but you will not receive the response. No further action is required from you or the user.";
|
|
143
145
|
return statusMessage;
|
|
144
146
|
} catch (error) {
|
|
147
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
145
148
|
logger.error(`Error in coding agent tool: ${error instanceof Error ? error.stack || error.message : JSON.stringify(error)}`);
|
|
146
|
-
|
|
149
|
+
|
|
150
|
+
// Return error as JSON string instead of throwing, so it can be properly detected by the tool execution layer
|
|
151
|
+
resolver.tool = JSON.stringify({ toolUsed: "coding" });
|
|
152
|
+
return JSON.stringify({
|
|
153
|
+
error: errorMessage,
|
|
154
|
+
recoveryMessage: "The coding task could not be started. Please check that all required parameters are provided and try again."
|
|
155
|
+
});
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
};
|
|
@@ -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, 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
|
|
@@ -356,7 +362,7 @@ export default {
|
|
|
356
362
|
|
|
357
363
|
} else if (isSearch) {
|
|
358
364
|
// Search collection
|
|
359
|
-
const { query, tags: filterTags = [], limit = 20 } = args;
|
|
365
|
+
const { query, tags: filterTags = [], limit = 20, includeAllChats = false } = args;
|
|
360
366
|
|
|
361
367
|
if (!query || typeof query !== 'string') {
|
|
362
368
|
throw new Error("query is required and must be a string");
|
|
@@ -366,8 +372,15 @@ export default {
|
|
|
366
372
|
const safeFilterTags = Array.isArray(filterTags) ? filterTags : [];
|
|
367
373
|
const queryLower = query.toLowerCase();
|
|
368
374
|
|
|
375
|
+
// Normalize query for flexible matching: treat spaces, dashes, underscores as equivalent
|
|
376
|
+
const normalizeForSearch = (str) => str.toLowerCase().replace(/[-_\s]+/g, ' ').trim();
|
|
377
|
+
const queryNormalized = normalizeForSearch(query);
|
|
378
|
+
|
|
379
|
+
// Determine which chatId to use for filtering (null if includeAllChats is true)
|
|
380
|
+
const filterChatId = includeAllChats ? null : chatId;
|
|
381
|
+
|
|
369
382
|
// Load primary collection for lastAccessed updates (only update files in primary context)
|
|
370
|
-
const primaryFiles = await loadFileCollection(contextId, contextKey, false);
|
|
383
|
+
const primaryFiles = await loadFileCollection(contextId, contextKey, false, filterChatId);
|
|
371
384
|
const now = new Date().toISOString();
|
|
372
385
|
|
|
373
386
|
// Find matching files in primary collection and update lastAccessed directly
|
|
@@ -376,9 +389,9 @@ export default {
|
|
|
376
389
|
|
|
377
390
|
// Fallback to filename if displayFilename is not set (for files uploaded before displayFilename was added)
|
|
378
391
|
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
|
|
392
|
+
const filenameMatch = normalizeForSearch(displayFilename).includes(queryNormalized);
|
|
393
|
+
const notesMatch = file.notes && normalizeForSearch(file.notes).includes(queryNormalized);
|
|
394
|
+
const tagMatch = Array.isArray(file.tags) && file.tags.some(tag => normalizeForSearch(tag).includes(queryNormalized));
|
|
382
395
|
const matchesQuery = filenameMatch || notesMatch || tagMatch;
|
|
383
396
|
|
|
384
397
|
const matchesTags = safeFilterTags.length === 0 ||
|
|
@@ -396,20 +409,26 @@ export default {
|
|
|
396
409
|
}
|
|
397
410
|
|
|
398
411
|
// Load merged collection for search results (includes all agentContext files)
|
|
399
|
-
|
|
412
|
+
// Filter by chatId if includeAllChats is false and chatId is available
|
|
413
|
+
// loadMergedFileCollection now handles inCollection filtering centrally
|
|
414
|
+
const updatedFiles = await loadMergedFileCollection(args.agentContext, filterChatId);
|
|
400
415
|
|
|
401
416
|
// Filter and sort results (for display only, not modifying)
|
|
402
417
|
let results = updatedFiles.filter(file => {
|
|
418
|
+
// Filter by query and tags
|
|
403
419
|
// Fallback to filename if displayFilename is not set
|
|
404
420
|
const displayFilename = file.displayFilename || file.filename || '';
|
|
405
421
|
const filename = file.filename || '';
|
|
406
422
|
|
|
407
423
|
// Check both displayFilename and filename for matches
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
const
|
|
424
|
+
// Use normalized matching (treating spaces, dashes, underscores as equivalent)
|
|
425
|
+
// so "News Corp" matches "News-Corp" and "News_Corp"
|
|
426
|
+
const displayFilenameNorm = normalizeForSearch(displayFilename);
|
|
427
|
+
const filenameNorm = normalizeForSearch(filename);
|
|
428
|
+
const filenameMatch = displayFilenameNorm.includes(queryNormalized) ||
|
|
429
|
+
(filename && filename !== displayFilename && filenameNorm.includes(queryNormalized));
|
|
430
|
+
const notesMatch = file.notes && normalizeForSearch(file.notes).includes(queryNormalized);
|
|
431
|
+
const tagMatch = Array.isArray(file.tags) && file.tags.some(tag => normalizeForSearch(tag).includes(queryNormalized));
|
|
413
432
|
|
|
414
433
|
const matchesQuery = filenameMatch || notesMatch || tagMatch;
|
|
415
434
|
|
|
@@ -426,8 +445,8 @@ export default {
|
|
|
426
445
|
// Fallback to filename if displayFilename is not set
|
|
427
446
|
const aDisplayFilename = a.displayFilename || a.filename || '';
|
|
428
447
|
const bDisplayFilename = b.displayFilename || b.filename || '';
|
|
429
|
-
const aFilenameMatch = aDisplayFilename
|
|
430
|
-
const bFilenameMatch = bDisplayFilename
|
|
448
|
+
const aFilenameMatch = normalizeForSearch(aDisplayFilename).includes(queryNormalized);
|
|
449
|
+
const bFilenameMatch = normalizeForSearch(bDisplayFilename).includes(queryNormalized);
|
|
431
450
|
if (aFilenameMatch && !bFilenameMatch) return -1;
|
|
432
451
|
if (!aFilenameMatch && bFilenameMatch) return 1;
|
|
433
452
|
return new Date(b.addedDate) - new Date(a.addedDate);
|
|
@@ -437,11 +456,28 @@ export default {
|
|
|
437
456
|
results = results.slice(0, limit);
|
|
438
457
|
|
|
439
458
|
resolver.tool = JSON.stringify({ toolUsed: "SearchFileCollection" });
|
|
459
|
+
|
|
460
|
+
// Build helpful message when no results found
|
|
461
|
+
let message;
|
|
462
|
+
if (results.length === 0) {
|
|
463
|
+
const suggestions = [];
|
|
464
|
+
if (chatId && !includeAllChats) {
|
|
465
|
+
suggestions.push('try includeAllChats=true to search across all chats');
|
|
466
|
+
}
|
|
467
|
+
suggestions.push('use ListFileCollection to see all available files');
|
|
468
|
+
|
|
469
|
+
message = `No files found matching "${query}". Count: 0. Suggestions: ${suggestions.join('; ')}.`;
|
|
470
|
+
} else {
|
|
471
|
+
message = `Found ${results.length} file(s) matching "${query}". Use the hash or displayFilename to reference files in other tools.`;
|
|
472
|
+
}
|
|
473
|
+
|
|
440
474
|
return JSON.stringify({
|
|
441
475
|
success: true,
|
|
442
476
|
count: results.length,
|
|
477
|
+
message,
|
|
443
478
|
files: results.map(f => ({
|
|
444
479
|
id: f.id,
|
|
480
|
+
hash: f.hash || null,
|
|
445
481
|
displayFilename: f.displayFilename || f.filename || null,
|
|
446
482
|
url: f.url,
|
|
447
483
|
gcs: f.gcs || null,
|
|
@@ -472,8 +508,9 @@ export default {
|
|
|
472
508
|
let filesToProcess = [];
|
|
473
509
|
|
|
474
510
|
// Load collection ONCE to find all files and their data
|
|
475
|
-
//
|
|
476
|
-
|
|
511
|
+
// Do NOT filter by chatId - remove should be able to delete files from any chat
|
|
512
|
+
// Use merged collection to include files from all agentContext contexts
|
|
513
|
+
const collection = await loadMergedFileCollection(args.agentContext, null);
|
|
477
514
|
|
|
478
515
|
// Resolve all files and collect their info in a single pass
|
|
479
516
|
for (const target of targetFiles) {
|
|
@@ -498,7 +535,7 @@ export default {
|
|
|
498
535
|
}
|
|
499
536
|
|
|
500
537
|
if (filesToProcess.length === 0 && notFoundFiles.length > 0) {
|
|
501
|
-
throw new Error(`No files found matching: ${notFoundFiles.join(', ')}
|
|
538
|
+
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
539
|
}
|
|
503
540
|
|
|
504
541
|
// Import helpers for reference counting
|
|
@@ -523,15 +560,23 @@ export default {
|
|
|
523
560
|
// No chatId context - fully remove
|
|
524
561
|
filesToFullyDelete.push(fileInfo);
|
|
525
562
|
} else {
|
|
526
|
-
//
|
|
527
|
-
const
|
|
563
|
+
// Check if current chatId is in the file's inCollection
|
|
564
|
+
const currentChatInCollection = Array.isArray(fileInfo.inCollection) && fileInfo.inCollection.includes(chatId);
|
|
528
565
|
|
|
529
|
-
if (
|
|
530
|
-
//
|
|
566
|
+
if (!currentChatInCollection) {
|
|
567
|
+
// File doesn't belong to current chat - fully remove it (cross-chat removal)
|
|
531
568
|
filesToFullyDelete.push(fileInfo);
|
|
532
569
|
} else {
|
|
533
|
-
//
|
|
534
|
-
|
|
570
|
+
// Remove this chatId from inCollection
|
|
571
|
+
const updatedInCollection = removeChatIdFromInCollection(fileInfo.inCollection, chatId);
|
|
572
|
+
|
|
573
|
+
if (updatedInCollection.length === 0) {
|
|
574
|
+
// No more references - fully delete
|
|
575
|
+
filesToFullyDelete.push(fileInfo);
|
|
576
|
+
} else {
|
|
577
|
+
// Still has references from other chats - just update inCollection
|
|
578
|
+
filesToUpdate.push({ ...fileInfo, updatedInCollection });
|
|
579
|
+
}
|
|
535
580
|
}
|
|
536
581
|
}
|
|
537
582
|
}
|
|
@@ -615,10 +660,15 @@ export default {
|
|
|
615
660
|
|
|
616
661
|
} else {
|
|
617
662
|
// List collection (read-only, no locking needed)
|
|
618
|
-
const { tags: filterTags = [], sortBy = 'date', limit = 50 } = args;
|
|
663
|
+
const { tags: filterTags = [], sortBy = 'date', limit = 50, includeAllChats = false } = args;
|
|
664
|
+
|
|
665
|
+
// Determine which chatId to use for filtering (null if includeAllChats is true)
|
|
666
|
+
const filterChatId = includeAllChats ? null : chatId;
|
|
619
667
|
|
|
620
668
|
// Use merged collection to include files from all agentContext contexts
|
|
621
|
-
|
|
669
|
+
// Filter by chatId if includeAllChats is false and chatId is available
|
|
670
|
+
// loadMergedFileCollection now handles inCollection filtering centrally
|
|
671
|
+
const collection = await loadMergedFileCollection(args.agentContext, filterChatId);
|
|
622
672
|
let results = collection;
|
|
623
673
|
|
|
624
674
|
// Filter by tags if provided
|
|
@@ -646,12 +696,32 @@ export default {
|
|
|
646
696
|
results = results.slice(0, limit);
|
|
647
697
|
|
|
648
698
|
resolver.tool = JSON.stringify({ toolUsed: "ListFileCollection" });
|
|
699
|
+
|
|
700
|
+
// Build helpful message
|
|
701
|
+
let message;
|
|
702
|
+
if (results.length === 0) {
|
|
703
|
+
const suggestions = [];
|
|
704
|
+
if (chatId && !includeAllChats) {
|
|
705
|
+
suggestions.push('try includeAllChats=true to see files from all chats');
|
|
706
|
+
}
|
|
707
|
+
if (filterTags.length > 0) {
|
|
708
|
+
suggestions.push('remove tag filters to see more files');
|
|
709
|
+
}
|
|
710
|
+
message = suggestions.length > 0
|
|
711
|
+
? `No files in collection. Suggestions: ${suggestions.join('; ')}.`
|
|
712
|
+
: 'No files in collection.';
|
|
713
|
+
} else {
|
|
714
|
+
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.`;
|
|
715
|
+
}
|
|
716
|
+
|
|
649
717
|
return JSON.stringify({
|
|
650
718
|
success: true,
|
|
651
719
|
count: results.length,
|
|
652
720
|
totalFiles: collection.length,
|
|
721
|
+
message,
|
|
653
722
|
files: results.map(f => ({
|
|
654
723
|
id: f.id,
|
|
724
|
+
hash: f.hash || null,
|
|
655
725
|
displayFilename: f.displayFilename || f.filename || null,
|
|
656
726
|
url: f.url,
|
|
657
727
|
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,6 +58,13 @@ export default {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
const { contextId, contextKey } = args;
|
|
62
|
+
if (!contextId) {
|
|
63
|
+
return JSON.stringify({
|
|
64
|
+
error: 'contextId is required. It should be provided via agentContext or contextId parameter.'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
// Validate memories array
|
|
62
69
|
if (!args.memories || !Array.isArray(args.memories) || args.memories.length === 0) {
|
|
63
70
|
return JSON.stringify({ error: 'memories must be a non-empty array' });
|
|
@@ -102,9 +109,9 @@ export default {
|
|
|
102
109
|
for (const [section, memoryLines] of Object.entries(memoriesBySection)) {
|
|
103
110
|
// Read current memory for the section
|
|
104
111
|
let currentMemory = await callPathway('sys_read_memory', {
|
|
105
|
-
contextId:
|
|
112
|
+
contextId: contextId,
|
|
106
113
|
section: section,
|
|
107
|
-
contextKey:
|
|
114
|
+
contextKey: contextKey
|
|
108
115
|
});
|
|
109
116
|
|
|
110
117
|
// Combine existing memory with new memories
|
|
@@ -114,10 +121,10 @@ export default {
|
|
|
114
121
|
|
|
115
122
|
// Save directly to memory
|
|
116
123
|
const result = await callPathway('sys_save_memory', {
|
|
117
|
-
contextId:
|
|
124
|
+
contextId: contextId,
|
|
118
125
|
section: section,
|
|
119
126
|
aiMemory: updatedMemory,
|
|
120
|
-
contextKey:
|
|
127
|
+
contextKey: contextKey
|
|
121
128
|
});
|
|
122
129
|
|
|
123
130
|
results[section] = result;
|
|
@@ -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);
|