@aj-archipelago/cortex 1.4.6 → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/helper-apps/cortex-file-handler/package-lock.json +2 -2
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/src/index.js +27 -4
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +74 -10
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +23 -2
- package/helper-apps/cortex-file-handler/src/start.js +2 -0
- package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +287 -0
- package/helper-apps/cortex-file-handler/tests/start.test.js +1 -1
- package/lib/entityConstants.js +1 -1
- package/lib/fileUtils.js +1481 -0
- package/lib/pathwayTools.js +7 -1
- package/lib/util.js +2 -313
- package/package.json +4 -3
- package/pathways/image_qwen.js +1 -1
- package/pathways/system/entity/memory/sys_read_memory.js +17 -3
- package/pathways/system/entity/memory/sys_save_memory.js +22 -6
- package/pathways/system/entity/sys_entity_agent.js +21 -4
- package/pathways/system/entity/tools/sys_tool_analyzefile.js +171 -0
- package/pathways/system/entity/tools/sys_tool_codingagent.js +38 -4
- package/pathways/system/entity/tools/sys_tool_editfile.js +403 -0
- package/pathways/system/entity/tools/sys_tool_file_collection.js +433 -0
- package/pathways/system/entity/tools/sys_tool_image.js +172 -10
- package/pathways/system/entity/tools/sys_tool_image_gemini.js +123 -10
- package/pathways/system/entity/tools/sys_tool_readfile.js +217 -124
- package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
- package/pathways/system/entity/tools/sys_tool_writefile.js +211 -0
- package/pathways/system/workspaces/run_workspace_prompt.js +4 -3
- package/pathways/transcribe_gemini.js +2 -1
- package/server/executeWorkspace.js +1 -1
- package/server/plugins/neuralSpacePlugin.js +2 -6
- package/server/plugins/openAiWhisperPlugin.js +2 -1
- package/server/plugins/replicateApiPlugin.js +4 -14
- package/server/typeDef.js +10 -1
- package/tests/integration/features/tools/fileCollection.test.js +858 -0
- package/tests/integration/features/tools/fileOperations.test.js +851 -0
- package/tests/integration/features/tools/writefile.test.js +350 -0
- package/tests/unit/core/fileCollection.test.js +259 -0
- package/tests/unit/core/util.test.js +320 -1
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// sys_tool_file_collection.js
|
|
2
|
+
// Tool pathway that manages user file collections (add, search, list files)
|
|
3
|
+
// Uses memory system endpoints (memoryFiles section) for storage
|
|
4
|
+
import logger from '../../../../lib/logger.js';
|
|
5
|
+
import { addFileToCollection, loadFileCollection, saveFileCollection, findFileInCollection, deleteFileByHash, modifyFileCollectionWithLock } from '../../../../lib/fileUtils.js';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
prompt: [],
|
|
9
|
+
timeout: 30,
|
|
10
|
+
toolDefinition: [
|
|
11
|
+
{
|
|
12
|
+
type: "function",
|
|
13
|
+
icon: "📁",
|
|
14
|
+
function: {
|
|
15
|
+
name: "AddFileToCollection",
|
|
16
|
+
description: "Add a file to your persistent file collection. This tool can upload a file from a URL to cloud storage (checking for duplicates by hash) and then store it in your collection with metadata. You can also add files that are already in cloud storage by providing the cloud URL directly.",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
fileUrl: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Optional: The URL of a file to upload to cloud storage (e.g., https://example.com/file.pdf). If provided, the file will be uploaded and then added to the collection. If not provided, you must provide the 'url' parameter for an already-uploaded file."
|
|
23
|
+
},
|
|
24
|
+
url: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Optional: The cloud storage URL of an already-uploaded file (Azure URL). Use this if the file is already in cloud storage. If 'fileUrl' is provided, this will be ignored."
|
|
27
|
+
},
|
|
28
|
+
gcs: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Optional: The Google Cloud Storage URL of the file (GCS URL). Only needed if the file is already in cloud storage and you're providing 'url'."
|
|
31
|
+
},
|
|
32
|
+
filename: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "The filename or title for this file"
|
|
35
|
+
},
|
|
36
|
+
tags: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: "string" },
|
|
39
|
+
description: "Optional: Array of tags to help organize and search for this file (e.g., ['pdf', 'report', '2024'])"
|
|
40
|
+
},
|
|
41
|
+
notes: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Optional: Notes or description about this file to help you remember what it contains"
|
|
44
|
+
},
|
|
45
|
+
hash: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Optional: File hash for deduplication and identification (usually computed automatically during upload)"
|
|
48
|
+
},
|
|
49
|
+
userMessage: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
required: ["filename", "userMessage"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: "function",
|
|
60
|
+
icon: "🔍",
|
|
61
|
+
function: {
|
|
62
|
+
name: "SearchFileCollection",
|
|
63
|
+
description: "Search your file collection to find files by filename, tags, notes, or date. Returns matching files with their cloud URLs and metadata.",
|
|
64
|
+
parameters: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
query: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Search query - can search by filename, tags, or notes content"
|
|
70
|
+
},
|
|
71
|
+
tags: {
|
|
72
|
+
type: "array",
|
|
73
|
+
items: { type: "string" },
|
|
74
|
+
description: "Optional: Filter results by specific tags (all tags must match)"
|
|
75
|
+
},
|
|
76
|
+
limit: {
|
|
77
|
+
type: "number",
|
|
78
|
+
description: "Optional: Maximum number of results to return (default: 20)"
|
|
79
|
+
},
|
|
80
|
+
userMessage: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
required: ["query", "userMessage"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: "function",
|
|
91
|
+
icon: "📋",
|
|
92
|
+
function: {
|
|
93
|
+
name: "ListFileCollection",
|
|
94
|
+
description: "List all files in your collection, optionally filtered by tags or sorted by date. Useful for getting an overview of your stored files.",
|
|
95
|
+
parameters: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
tags: {
|
|
99
|
+
type: "array",
|
|
100
|
+
items: { type: "string" },
|
|
101
|
+
description: "Optional: Filter results by specific tags (all tags must match)"
|
|
102
|
+
},
|
|
103
|
+
sortBy: {
|
|
104
|
+
type: "string",
|
|
105
|
+
enum: ["date", "filename"],
|
|
106
|
+
description: "Optional: Sort results by date (newest first) or filename (alphabetical). Default: date"
|
|
107
|
+
},
|
|
108
|
+
limit: {
|
|
109
|
+
type: "number",
|
|
110
|
+
description: "Optional: Maximum number of results to return (default: 50)"
|
|
111
|
+
},
|
|
112
|
+
userMessage: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
required: ["userMessage"]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: "function",
|
|
123
|
+
icon: "🗑️",
|
|
124
|
+
function: {
|
|
125
|
+
name: "RemoveFileFromCollection",
|
|
126
|
+
description: "Remove one or more files from your collection and delete them from cloud storage. Use a file ID to remove a specific file, or use '*' to remove all files. The file will be deleted from cloud storage (if a hash is available) and removed from your collection.",
|
|
127
|
+
parameters: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
fileId: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "The file to remove (from ListFileCollection or SearchFileCollection): can be the hash, the filename, the URL, or the GCS URL, or '*' to remove all files."
|
|
133
|
+
},
|
|
134
|
+
userMessage: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
required: ["fileId", "userMessage"]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
|
|
145
|
+
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
146
|
+
const { contextId, contextKey } = args;
|
|
147
|
+
|
|
148
|
+
// Determine which function was called based on which parameters are present
|
|
149
|
+
const isAdd = args.fileUrl !== undefined || args.url !== undefined;
|
|
150
|
+
const isSearch = args.query !== undefined;
|
|
151
|
+
const isRemove = args.fileId !== undefined;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
if (!contextId) {
|
|
155
|
+
throw new Error("contextId is required for file collection operations");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isAdd) {
|
|
159
|
+
// Add file to collection
|
|
160
|
+
const { fileUrl, url, gcs, filename, tags = [], notes = '', hash = null } = args;
|
|
161
|
+
|
|
162
|
+
if (!filename) {
|
|
163
|
+
throw new Error("filename is required");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!fileUrl && !url) {
|
|
167
|
+
throw new Error("Either fileUrl (to upload) or url (already uploaded) is required");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Use the centralized utility function (it will handle upload if fileUrl is provided)
|
|
171
|
+
const fileEntry = await addFileToCollection(
|
|
172
|
+
contextId,
|
|
173
|
+
contextKey,
|
|
174
|
+
url,
|
|
175
|
+
gcs,
|
|
176
|
+
filename,
|
|
177
|
+
tags,
|
|
178
|
+
notes,
|
|
179
|
+
hash,
|
|
180
|
+
fileUrl,
|
|
181
|
+
resolver
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
resolver.tool = JSON.stringify({ toolUsed: "AddFileToCollection" });
|
|
185
|
+
return JSON.stringify({
|
|
186
|
+
success: true,
|
|
187
|
+
fileId: fileEntry.id,
|
|
188
|
+
message: `File "${filename}" added to collection`
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
} else if (isSearch) {
|
|
192
|
+
// Search collection
|
|
193
|
+
const { query, tags: filterTags = [], limit = 20 } = args;
|
|
194
|
+
|
|
195
|
+
if (!query || typeof query !== 'string') {
|
|
196
|
+
throw new Error("query is required and must be a string");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const queryLower = query.toLowerCase();
|
|
200
|
+
|
|
201
|
+
// Use optimistic locking to update lastAccessed
|
|
202
|
+
await modifyFileCollectionWithLock(contextId, contextKey, (collection) => {
|
|
203
|
+
// Find matching files and update their lastAccessed
|
|
204
|
+
const fileIds = new Set();
|
|
205
|
+
collection.forEach(file => {
|
|
206
|
+
// Search in filename, tags, and notes
|
|
207
|
+
const filenameMatch = file.filename.toLowerCase().includes(queryLower);
|
|
208
|
+
const notesMatch = file.notes && file.notes.toLowerCase().includes(queryLower);
|
|
209
|
+
const tagMatch = file.tags.some(tag => tag.toLowerCase().includes(queryLower));
|
|
210
|
+
|
|
211
|
+
const matchesQuery = filenameMatch || notesMatch || tagMatch;
|
|
212
|
+
|
|
213
|
+
// Filter by tags if provided
|
|
214
|
+
const matchesTags = filterTags.length === 0 ||
|
|
215
|
+
filterTags.every(filterTag =>
|
|
216
|
+
file.tags.some(tag => tag.toLowerCase() === filterTag.toLowerCase())
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (matchesQuery && matchesTags) {
|
|
220
|
+
fileIds.add(file.id);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Update lastAccessed for found files
|
|
225
|
+
collection.forEach(file => {
|
|
226
|
+
if (fileIds.has(file.id)) {
|
|
227
|
+
file.lastAccessed = new Date().toISOString();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return collection;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Reload collection to get results (after update)
|
|
235
|
+
const collection = await loadFileCollection(contextId, contextKey, false);
|
|
236
|
+
|
|
237
|
+
// Filter and sort results (for display only, not modifying)
|
|
238
|
+
let results = collection.filter(file => {
|
|
239
|
+
const filenameMatch = file.filename.toLowerCase().includes(queryLower);
|
|
240
|
+
const notesMatch = file.notes && file.notes.toLowerCase().includes(queryLower);
|
|
241
|
+
const tagMatch = file.tags.some(tag => tag.toLowerCase().includes(queryLower));
|
|
242
|
+
|
|
243
|
+
const matchesQuery = filenameMatch || notesMatch || tagMatch;
|
|
244
|
+
|
|
245
|
+
const matchesTags = filterTags.length === 0 ||
|
|
246
|
+
filterTags.every(filterTag =>
|
|
247
|
+
file.tags.some(tag => tag.toLowerCase() === filterTag.toLowerCase())
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return matchesQuery && matchesTags;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Sort by relevance (filename matches first, then by date)
|
|
254
|
+
results.sort((a, b) => {
|
|
255
|
+
const aFilenameMatch = a.filename.toLowerCase().includes(queryLower);
|
|
256
|
+
const bFilenameMatch = b.filename.toLowerCase().includes(queryLower);
|
|
257
|
+
if (aFilenameMatch && !bFilenameMatch) return -1;
|
|
258
|
+
if (!aFilenameMatch && bFilenameMatch) return 1;
|
|
259
|
+
return new Date(b.addedDate) - new Date(a.addedDate);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Limit results
|
|
263
|
+
results = results.slice(0, limit);
|
|
264
|
+
|
|
265
|
+
resolver.tool = JSON.stringify({ toolUsed: "SearchFileCollection" });
|
|
266
|
+
return JSON.stringify({
|
|
267
|
+
success: true,
|
|
268
|
+
count: results.length,
|
|
269
|
+
files: results.map(f => ({
|
|
270
|
+
id: f.id,
|
|
271
|
+
filename: f.filename,
|
|
272
|
+
url: f.url,
|
|
273
|
+
gcs: f.gcs || null,
|
|
274
|
+
tags: f.tags,
|
|
275
|
+
notes: f.notes,
|
|
276
|
+
addedDate: f.addedDate
|
|
277
|
+
}))
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
} else if (isRemove) {
|
|
281
|
+
// Remove file(s) from collection and delete from cloud storage
|
|
282
|
+
const { fileId } = args;
|
|
283
|
+
|
|
284
|
+
if (!fileId || typeof fileId !== 'string') {
|
|
285
|
+
throw new Error("fileId is required and must be a string");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let removedCount = 0;
|
|
289
|
+
let removedFiles = [];
|
|
290
|
+
let deletedFromCloud = 0;
|
|
291
|
+
let deletionErrors = [];
|
|
292
|
+
let filesToRemove = [];
|
|
293
|
+
|
|
294
|
+
// First, identify files to remove (before locking)
|
|
295
|
+
if (fileId === '*') {
|
|
296
|
+
// Load collection to get all files
|
|
297
|
+
const collection = await loadFileCollection(contextId, contextKey, false);
|
|
298
|
+
filesToRemove = collection.map(f => ({
|
|
299
|
+
id: f.id,
|
|
300
|
+
filename: f.filename,
|
|
301
|
+
hash: f.hash || null
|
|
302
|
+
}));
|
|
303
|
+
} else {
|
|
304
|
+
// Load collection and find specific file
|
|
305
|
+
const collection = await loadFileCollection(contextId, contextKey, false);
|
|
306
|
+
const foundFile = findFileInCollection(fileId, collection);
|
|
307
|
+
|
|
308
|
+
if (!foundFile) {
|
|
309
|
+
throw new Error(`File with ID, filename, URL, or hash "${fileId}" not found in collection`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
filesToRemove = [{
|
|
313
|
+
id: foundFile.id,
|
|
314
|
+
filename: foundFile.filename,
|
|
315
|
+
hash: foundFile.hash || null
|
|
316
|
+
}];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Delete files from cloud storage (outside lock - idempotent operation)
|
|
320
|
+
for (const fileInfo of filesToRemove) {
|
|
321
|
+
if (fileInfo.hash) {
|
|
322
|
+
try {
|
|
323
|
+
logger.info(`Deleting file from cloud storage: ${fileInfo.filename} (hash: ${fileInfo.hash})`);
|
|
324
|
+
const deleted = await deleteFileByHash(fileInfo.hash, resolver);
|
|
325
|
+
if (deleted) {
|
|
326
|
+
deletedFromCloud++;
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const errorMsg = error?.message || String(error);
|
|
330
|
+
logger.warn(`Failed to delete file ${fileInfo.filename} (hash: ${fileInfo.hash}) from cloud storage: ${errorMsg}`);
|
|
331
|
+
deletionErrors.push({ filename: fileInfo.filename, error: errorMsg });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Use optimistic locking to remove files from collection
|
|
337
|
+
const fileIdsToRemove = new Set(filesToRemove.map(f => f.id));
|
|
338
|
+
const finalCollection = await modifyFileCollectionWithLock(contextId, contextKey, (collection) => {
|
|
339
|
+
// Remove files by ID
|
|
340
|
+
return collection.filter(file => !fileIdsToRemove.has(file.id));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
removedCount = filesToRemove.length;
|
|
344
|
+
removedFiles = filesToRemove;
|
|
345
|
+
|
|
346
|
+
// Build result message
|
|
347
|
+
let message;
|
|
348
|
+
if (fileId === '*') {
|
|
349
|
+
message = `All ${removedCount} file(s) removed from collection`;
|
|
350
|
+
if (deletedFromCloud > 0) {
|
|
351
|
+
message += ` (${deletedFromCloud} deleted from cloud storage)`;
|
|
352
|
+
}
|
|
353
|
+
if (deletionErrors.length > 0) {
|
|
354
|
+
message += `. ${deletionErrors.length} deletion error(s) occurred`;
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
message = `File "${removedFiles[0]?.filename || fileId}" removed from collection`;
|
|
358
|
+
if (deletedFromCloud > 0) {
|
|
359
|
+
message += ` and deleted from cloud storage`;
|
|
360
|
+
} else if (removedFiles[0]?.hash) {
|
|
361
|
+
message += ` (cloud storage deletion failed or file not found)`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
resolver.tool = JSON.stringify({ toolUsed: "RemoveFileFromCollection" });
|
|
366
|
+
return JSON.stringify({
|
|
367
|
+
success: true,
|
|
368
|
+
removedCount: removedCount,
|
|
369
|
+
deletedFromCloud: deletedFromCloud,
|
|
370
|
+
remainingFiles: finalCollection.length,
|
|
371
|
+
message: message,
|
|
372
|
+
removedFiles: removedFiles,
|
|
373
|
+
deletionErrors: deletionErrors.length > 0 ? deletionErrors : undefined
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
} else {
|
|
377
|
+
// List collection (read-only, no locking needed)
|
|
378
|
+
const { tags: filterTags = [], sortBy = 'date', limit = 50 } = args;
|
|
379
|
+
|
|
380
|
+
const collection = await loadFileCollection(contextId, contextKey, true);
|
|
381
|
+
let results = collection;
|
|
382
|
+
|
|
383
|
+
// Filter by tags if provided
|
|
384
|
+
if (filterTags.length > 0) {
|
|
385
|
+
results = results.filter(file =>
|
|
386
|
+
filterTags.every(filterTag =>
|
|
387
|
+
file.tags.some(tag => tag.toLowerCase() === filterTag.toLowerCase())
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Sort results
|
|
393
|
+
if (sortBy === 'date') {
|
|
394
|
+
results.sort((a, b) => new Date(b.addedDate) - new Date(a.addedDate));
|
|
395
|
+
} else if (sortBy === 'filename') {
|
|
396
|
+
results.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Limit results
|
|
400
|
+
results = results.slice(0, limit);
|
|
401
|
+
|
|
402
|
+
resolver.tool = JSON.stringify({ toolUsed: "ListFileCollection" });
|
|
403
|
+
return JSON.stringify({
|
|
404
|
+
success: true,
|
|
405
|
+
count: results.length,
|
|
406
|
+
totalFiles: collection.length,
|
|
407
|
+
files: results.map(f => ({
|
|
408
|
+
id: f.id,
|
|
409
|
+
filename: f.filename,
|
|
410
|
+
url: f.url,
|
|
411
|
+
gcs: f.gcs || null,
|
|
412
|
+
tags: f.tags,
|
|
413
|
+
notes: f.notes,
|
|
414
|
+
addedDate: f.addedDate,
|
|
415
|
+
lastAccessed: f.lastAccessed
|
|
416
|
+
}))
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
} catch (e) {
|
|
421
|
+
logger.error(`Error in file collection operation: ${e.message}`);
|
|
422
|
+
|
|
423
|
+
const errorResult = {
|
|
424
|
+
success: false,
|
|
425
|
+
error: e.message || "Unknown error occurred"
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
resolver.tool = JSON.stringify({ toolUsed: "FileCollection" });
|
|
429
|
+
return JSON.stringify(errorResult);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
@@ -1,6 +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
5
|
|
|
5
6
|
export default {
|
|
6
7
|
prompt: [],
|
|
@@ -24,6 +25,17 @@ export default {
|
|
|
24
25
|
type: "string",
|
|
25
26
|
description: "A very detailed prompt describing the image you want to create. You should be very specific - explaining subject matter, style, and details about the image including things like camera angle, lens types, lighting, photographic techniques, etc. Any details you can provide to the image creation engine will help it create the most accurate and useful images. The more detailed and descriptive the prompt, the better the result."
|
|
26
27
|
},
|
|
28
|
+
filenamePrefix: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Optional: A descriptive prefix to use for the generated image filename (e.g., 'portrait', 'landscape', 'logo'). If not provided, defaults to 'generated-image'."
|
|
31
|
+
},
|
|
32
|
+
tags: {
|
|
33
|
+
type: "array",
|
|
34
|
+
items: {
|
|
35
|
+
type: "string"
|
|
36
|
+
},
|
|
37
|
+
description: "Optional: Array of tags to categorize the image (e.g., ['portrait', 'art', 'photography']). Will be merged with default tags ['image', 'generated']."
|
|
38
|
+
},
|
|
27
39
|
userMessage: {
|
|
28
40
|
type: "string",
|
|
29
41
|
description: "A user-friendly message that describes what you're doing with this tool"
|
|
@@ -44,20 +56,31 @@ export default {
|
|
|
44
56
|
properties: {
|
|
45
57
|
inputImage: {
|
|
46
58
|
type: "string",
|
|
47
|
-
description: "
|
|
59
|
+
description: "An image from your available files (from Available Files section or ListFileCollection or SearchFileCollection) to use as a reference for the image modification."
|
|
48
60
|
},
|
|
49
61
|
inputImage2: {
|
|
50
62
|
type: "string",
|
|
51
|
-
description: "
|
|
63
|
+
description: "A second image from your available files (from Available Files section or ListFileCollection or SearchFileCollection) to use as a reference for the image modification if there is one."
|
|
52
64
|
},
|
|
53
65
|
inputImage3: {
|
|
54
66
|
type: "string",
|
|
55
|
-
description: "
|
|
67
|
+
description: "A third image from your available files (from Available Files section or ListFileCollection or SearchFileCollection) to use as a reference for the image modification if there is one."
|
|
56
68
|
},
|
|
57
69
|
detailedInstructions: {
|
|
58
70
|
type: "string",
|
|
59
71
|
description: "A very detailed prompt describing how you want to modify the image. Be specific about the changes you want to make, including style changes, artistic effects, or specific modifications. The more detailed and descriptive the prompt, the better the result."
|
|
60
72
|
},
|
|
73
|
+
filenamePrefix: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Optional: A prefix to use for the modified image filename (e.g., 'edited', 'stylized', 'enhanced'). If not provided, defaults to 'modified-image'."
|
|
76
|
+
},
|
|
77
|
+
tags: {
|
|
78
|
+
type: "array",
|
|
79
|
+
items: {
|
|
80
|
+
type: "string"
|
|
81
|
+
},
|
|
82
|
+
description: "Optional: Array of tags to categorize the image (e.g., ['edited', 'art', 'stylized']). Will be merged with default tags ['image', 'modified']."
|
|
83
|
+
},
|
|
61
84
|
userMessage: {
|
|
62
85
|
type: "string",
|
|
63
86
|
description: "A user-friendly message that describes what you're doing with this tool"
|
|
@@ -82,6 +105,45 @@ export default {
|
|
|
82
105
|
|
|
83
106
|
pathwayResolver.tool = JSON.stringify({ toolUsed: "image" });
|
|
84
107
|
|
|
108
|
+
// Resolve all input images to URLs using the common utility
|
|
109
|
+
// Fail early if any provided image parameter cannot be resolved
|
|
110
|
+
if (args.inputImage) {
|
|
111
|
+
if (!args.contextId) {
|
|
112
|
+
throw new Error("contextId is required when using the 'inputImage' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
|
|
113
|
+
}
|
|
114
|
+
const resolved = await resolveFileParameter(args.inputImage, args.contextId, args.contextKey);
|
|
115
|
+
if (!resolved) {
|
|
116
|
+
throw new Error(`File not found: "${args.inputImage}". Use ListFileCollection or SearchFileCollection to find available files.`);
|
|
117
|
+
}
|
|
118
|
+
args.inputImage = resolved;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (args.inputImage2) {
|
|
122
|
+
if (!args.contextId) {
|
|
123
|
+
throw new Error("contextId is required when using the 'inputImage2' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
|
|
124
|
+
}
|
|
125
|
+
const resolved = await resolveFileParameter(args.inputImage2, args.contextId, args.contextKey);
|
|
126
|
+
if (!resolved) {
|
|
127
|
+
throw new Error(`File not found: "${args.inputImage2}". Use ListFileCollection or SearchFileCollection to find available files.`);
|
|
128
|
+
}
|
|
129
|
+
args.inputImage2 = resolved;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (args.inputImage3) {
|
|
133
|
+
if (!args.contextId) {
|
|
134
|
+
throw new Error("contextId is required when using the 'inputImage3' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
|
|
135
|
+
}
|
|
136
|
+
const resolved = await resolveFileParameter(args.inputImage3, args.contextId, args.contextKey);
|
|
137
|
+
if (!resolved) {
|
|
138
|
+
throw new Error(`File not found: "${args.inputImage3}". Use ListFileCollection or SearchFileCollection to find available files.`);
|
|
139
|
+
}
|
|
140
|
+
args.inputImage3 = resolved;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const resolvedInputImage = args.inputImage;
|
|
144
|
+
const resolvedInputImage2 = args.inputImage2;
|
|
145
|
+
const resolvedInputImage3 = args.inputImage3;
|
|
146
|
+
|
|
85
147
|
// Build parameters object, only including image parameters if they have non-empty values
|
|
86
148
|
const params = {
|
|
87
149
|
...args,
|
|
@@ -90,19 +152,119 @@ export default {
|
|
|
90
152
|
stream: false,
|
|
91
153
|
};
|
|
92
154
|
|
|
93
|
-
if (
|
|
94
|
-
params.input_image =
|
|
155
|
+
if (resolvedInputImage) {
|
|
156
|
+
params.input_image = resolvedInputImage;
|
|
95
157
|
}
|
|
96
|
-
if (
|
|
97
|
-
params.input_image_2 =
|
|
158
|
+
if (resolvedInputImage2) {
|
|
159
|
+
params.input_image_2 = resolvedInputImage2;
|
|
98
160
|
}
|
|
99
|
-
if (
|
|
100
|
-
params.input_image_3 =
|
|
161
|
+
if (resolvedInputImage3) {
|
|
162
|
+
params.input_image_3 = resolvedInputImage3;
|
|
101
163
|
}
|
|
102
164
|
|
|
103
165
|
// Call appropriate pathway based on model
|
|
104
166
|
const pathwayName = model.includes('seedream') ? 'image_seedream4' : 'image_qwen';
|
|
105
|
-
|
|
167
|
+
let result = await callPathway(pathwayName, params, pathwayResolver);
|
|
168
|
+
|
|
169
|
+
// Process artifacts from Replicate (which come as URLs, not base64 data)
|
|
170
|
+
if (pathwayResolver.pathwayResultData) {
|
|
171
|
+
if (pathwayResolver.pathwayResultData.artifacts && Array.isArray(pathwayResolver.pathwayResultData.artifacts)) {
|
|
172
|
+
const uploadedImages = [];
|
|
173
|
+
|
|
174
|
+
// Process each image artifact
|
|
175
|
+
for (const artifact of pathwayResolver.pathwayResultData.artifacts) {
|
|
176
|
+
if (artifact.type === 'image' && artifact.url) {
|
|
177
|
+
try {
|
|
178
|
+
// Replicate artifacts have URLs, not base64 data
|
|
179
|
+
// Download the image and upload it to cloud storage
|
|
180
|
+
const imageUrl = artifact.url;
|
|
181
|
+
const mimeType = artifact.mimeType || 'image/png';
|
|
182
|
+
|
|
183
|
+
// Upload image to cloud storage (downloads from URL, computes hash, uploads)
|
|
184
|
+
const uploadResult = await uploadFileToCloud(
|
|
185
|
+
imageUrl,
|
|
186
|
+
mimeType,
|
|
187
|
+
null, // filename will be generated
|
|
188
|
+
pathwayResolver
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const uploadedUrl = uploadResult.url || uploadResult;
|
|
192
|
+
const uploadedGcs = uploadResult.gcs || null;
|
|
193
|
+
const uploadedHash = uploadResult.hash || null;
|
|
194
|
+
|
|
195
|
+
uploadedImages.push({
|
|
196
|
+
type: 'image',
|
|
197
|
+
url: uploadedUrl,
|
|
198
|
+
gcs: uploadedGcs,
|
|
199
|
+
hash: uploadedHash,
|
|
200
|
+
mimeType: mimeType
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Add uploaded image to file collection if contextId is available
|
|
204
|
+
if (args.contextId && uploadedUrl) {
|
|
205
|
+
try {
|
|
206
|
+
// Generate filename from mimeType (e.g., "image/png" -> "png")
|
|
207
|
+
const extension = mimeType.split('/')[1] || 'png';
|
|
208
|
+
// Use hash for uniqueness if available, otherwise use timestamp and index
|
|
209
|
+
const uniqueId = uploadedHash ? uploadedHash.substring(0, 8) : `${Date.now()}-${uploadedImages.length}`;
|
|
210
|
+
|
|
211
|
+
// Determine filename prefix based on whether this is a modification or generation
|
|
212
|
+
const isModification = args.inputImage || args.inputImage2 || args.inputImage3;
|
|
213
|
+
const defaultPrefix = isModification ? 'modified-image' : 'generated-image';
|
|
214
|
+
const filenamePrefix = args.filenamePrefix || defaultPrefix;
|
|
215
|
+
|
|
216
|
+
// Sanitize the prefix to ensure it's a valid filename component
|
|
217
|
+
const sanitizedPrefix = filenamePrefix.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
218
|
+
const filename = `${sanitizedPrefix}-${uniqueId}.${extension}`;
|
|
219
|
+
|
|
220
|
+
// Merge provided tags with default tags
|
|
221
|
+
const defaultTags = ['image', isModification ? 'modified' : 'generated'];
|
|
222
|
+
const providedTags = Array.isArray(args.tags) ? args.tags : [];
|
|
223
|
+
const allTags = [...defaultTags, ...providedTags.filter(tag => !defaultTags.includes(tag))];
|
|
224
|
+
|
|
225
|
+
// Use the centralized utility function to add to collection
|
|
226
|
+
await addFileToCollection(
|
|
227
|
+
args.contextId,
|
|
228
|
+
args.contextKey || '',
|
|
229
|
+
uploadedUrl,
|
|
230
|
+
uploadedGcs,
|
|
231
|
+
filename,
|
|
232
|
+
allTags,
|
|
233
|
+
isModification
|
|
234
|
+
? `Modified image from prompt: ${args.detailedInstructions || 'image modification'}`
|
|
235
|
+
: `Generated image from prompt: ${args.detailedInstructions || 'image generation'}`,
|
|
236
|
+
uploadedHash
|
|
237
|
+
);
|
|
238
|
+
} catch (collectionError) {
|
|
239
|
+
// Log but don't fail - file collection is optional
|
|
240
|
+
pathwayResolver.logWarning(`Failed to add image to file collection: ${collectionError.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch (uploadError) {
|
|
244
|
+
pathwayResolver.logError(`Failed to upload image from Replicate: ${uploadError.message}`);
|
|
245
|
+
// Keep original URL as fallback
|
|
246
|
+
uploadedImages.push({
|
|
247
|
+
type: 'image',
|
|
248
|
+
url: artifact.url,
|
|
249
|
+
mimeType: artifact.mimeType || 'image/png'
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// Keep non-image artifacts as-is
|
|
254
|
+
uploadedImages.push(artifact);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Return the URLs of the uploaded images as text in the result
|
|
259
|
+
// Replace the result with uploaded cloud URLs (not the original Replicate URLs)
|
|
260
|
+
if (uploadedImages.length > 0) {
|
|
261
|
+
const imageUrls = uploadedImages.map(image => image.url || image).filter(Boolean);
|
|
262
|
+
result = imageUrls.join('\n');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return result;
|
|
106
268
|
|
|
107
269
|
} catch (e) {
|
|
108
270
|
pathwayResolver.logError(e.message ?? e);
|