@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
|
@@ -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 } from '../../../../lib/
|
|
4
|
+
import { uploadImageToCloud, addFileToCollection, resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
prompt: [],
|
|
@@ -9,6 +9,8 @@ export default {
|
|
|
9
9
|
enableDuplicateRequests: false,
|
|
10
10
|
inputParameters: {
|
|
11
11
|
model: 'oai-gpt4o',
|
|
12
|
+
contextId: '',
|
|
13
|
+
contextKey: '',
|
|
12
14
|
},
|
|
13
15
|
timeout: 300,
|
|
14
16
|
toolDefinition: [{
|
|
@@ -25,6 +27,17 @@ export default {
|
|
|
25
27
|
type: "string",
|
|
26
28
|
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."
|
|
27
29
|
},
|
|
30
|
+
filenamePrefix: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Optional: A descriptive prefix to use for the generated image filename (e.g., 'portrait', 'landscape', 'logo'). If not provided, defaults to 'generated-image'."
|
|
33
|
+
},
|
|
34
|
+
tags: {
|
|
35
|
+
type: "array",
|
|
36
|
+
items: {
|
|
37
|
+
type: "string"
|
|
38
|
+
},
|
|
39
|
+
description: "Optional: Array of tags to categorize the image (e.g., ['portrait', 'art', 'photography']). Will be merged with default tags ['image', 'generated']."
|
|
40
|
+
},
|
|
28
41
|
userMessage: {
|
|
29
42
|
type: "string",
|
|
30
43
|
description: "A user-friendly message that describes what you're doing with this tool"
|
|
@@ -46,20 +59,31 @@ export default {
|
|
|
46
59
|
properties: {
|
|
47
60
|
inputImage: {
|
|
48
61
|
type: "string",
|
|
49
|
-
description: "
|
|
62
|
+
description: "An image from your available files (from Available Files section or ListFileCollection or SearchFileCollection) to use as a reference for the image modification."
|
|
50
63
|
},
|
|
51
64
|
inputImage2: {
|
|
52
65
|
type: "string",
|
|
53
|
-
description: "
|
|
66
|
+
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."
|
|
54
67
|
},
|
|
55
68
|
inputImage3: {
|
|
56
69
|
type: "string",
|
|
57
|
-
description: "
|
|
70
|
+
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."
|
|
58
71
|
},
|
|
59
72
|
detailedInstructions: {
|
|
60
73
|
type: "string",
|
|
61
74
|
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."
|
|
62
75
|
},
|
|
76
|
+
filenamePrefix: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Optional: A prefix to use for the modified image filename (e.g., 'edited', 'stylized', 'enhanced'). If not provided, defaults to 'modified-image'."
|
|
79
|
+
},
|
|
80
|
+
tags: {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: {
|
|
83
|
+
type: "string"
|
|
84
|
+
},
|
|
85
|
+
description: "Optional: Array of tags to categorize the image (e.g., ['edited', 'art', 'stylized']). Will be merged with default tags ['image', 'modified']."
|
|
86
|
+
},
|
|
63
87
|
userMessage: {
|
|
64
88
|
type: "string",
|
|
65
89
|
description: "A user-friendly message that describes what you're doing with this tool"
|
|
@@ -76,15 +100,55 @@ export default {
|
|
|
76
100
|
let model = "gemini-25-flash-image";
|
|
77
101
|
let prompt = args.detailedInstructions || "";
|
|
78
102
|
|
|
103
|
+
// Resolve input images to URLs using the common utility
|
|
104
|
+
// For Gemini, prefer GCS URLs over Azure URLs
|
|
105
|
+
// Fail early if any provided image parameter cannot be resolved
|
|
106
|
+
if (args.inputImage) {
|
|
107
|
+
if (!args.contextId) {
|
|
108
|
+
throw new Error("contextId is required when using the 'inputImage' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
|
|
109
|
+
}
|
|
110
|
+
const resolved = await resolveFileParameter(args.inputImage, args.contextId, args.contextKey, { preferGcs: true });
|
|
111
|
+
if (!resolved) {
|
|
112
|
+
throw new Error(`File not found: "${args.inputImage}". Use ListFileCollection or SearchFileCollection to find available files.`);
|
|
113
|
+
}
|
|
114
|
+
args.inputImage = resolved;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (args.inputImage2) {
|
|
118
|
+
if (!args.contextId) {
|
|
119
|
+
throw new Error("contextId is required when using the 'inputImage2' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
|
|
120
|
+
}
|
|
121
|
+
const resolved = await resolveFileParameter(args.inputImage2, args.contextId, args.contextKey, { preferGcs: true });
|
|
122
|
+
if (!resolved) {
|
|
123
|
+
throw new Error(`File not found: "${args.inputImage2}". Use ListFileCollection or SearchFileCollection to find available files.`);
|
|
124
|
+
}
|
|
125
|
+
args.inputImage2 = resolved;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (args.inputImage3) {
|
|
129
|
+
if (!args.contextId) {
|
|
130
|
+
throw new Error("contextId is required when using the 'inputImage3' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
|
|
131
|
+
}
|
|
132
|
+
const resolved = await resolveFileParameter(args.inputImage3, args.contextId, args.contextKey, { preferGcs: true });
|
|
133
|
+
if (!resolved) {
|
|
134
|
+
throw new Error(`File not found: "${args.inputImage3}". Use ListFileCollection or SearchFileCollection to find available files.`);
|
|
135
|
+
}
|
|
136
|
+
args.inputImage3 = resolved;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const resolvedInputImage = args.inputImage;
|
|
140
|
+
const resolvedInputImage2 = args.inputImage2;
|
|
141
|
+
const resolvedInputImage3 = args.inputImage3;
|
|
142
|
+
|
|
79
143
|
// Call the image generation pathway
|
|
80
144
|
let result = await callPathway('image_gemini_25', {
|
|
81
145
|
...args,
|
|
82
146
|
text: prompt,
|
|
83
147
|
model,
|
|
84
148
|
stream: false,
|
|
85
|
-
input_image:
|
|
86
|
-
input_image_2:
|
|
87
|
-
input_image_3:
|
|
149
|
+
input_image: resolvedInputImage,
|
|
150
|
+
input_image_2: resolvedInputImage2,
|
|
151
|
+
input_image_3: resolvedInputImage3,
|
|
88
152
|
optimizePrompt: true,
|
|
89
153
|
}, pathwayResolver);
|
|
90
154
|
|
|
@@ -98,13 +162,62 @@ export default {
|
|
|
98
162
|
for (const artifact of pathwayResolver.pathwayResultData.artifacts) {
|
|
99
163
|
if (artifact.type === 'image' && artifact.data && artifact.mimeType) {
|
|
100
164
|
try {
|
|
101
|
-
// Upload image to cloud storage
|
|
102
|
-
const
|
|
165
|
+
// Upload image to cloud storage (returns {url, gcs, hash})
|
|
166
|
+
const uploadResult = await uploadImageToCloud(artifact.data, artifact.mimeType, pathwayResolver);
|
|
167
|
+
|
|
168
|
+
const imageUrl = uploadResult.url || uploadResult;
|
|
169
|
+
const imageGcs = uploadResult.gcs || null;
|
|
170
|
+
const imageHash = uploadResult.hash || null;
|
|
171
|
+
|
|
103
172
|
uploadedImages.push({
|
|
104
173
|
type: 'image',
|
|
105
174
|
url: imageUrl,
|
|
175
|
+
gcs: imageGcs,
|
|
176
|
+
hash: imageHash,
|
|
106
177
|
mimeType: artifact.mimeType
|
|
107
178
|
});
|
|
179
|
+
|
|
180
|
+
// Add uploaded image to file collection if contextId is available
|
|
181
|
+
if (args.contextId && imageUrl) {
|
|
182
|
+
try {
|
|
183
|
+
// Generate filename from mimeType (e.g., "image/png" -> "png")
|
|
184
|
+
const extension = artifact.mimeType.split('/')[1] || 'png';
|
|
185
|
+
// Use hash for uniqueness if available, otherwise use timestamp and index
|
|
186
|
+
const uniqueId = imageHash ? imageHash.substring(0, 8) : `${Date.now()}-${uploadedImages.length}`;
|
|
187
|
+
|
|
188
|
+
// Determine filename prefix based on whether this is a modification or generation
|
|
189
|
+
// If inputImage exists, it's a modification; otherwise it's a generation
|
|
190
|
+
const isModification = args.inputImage || args.inputImage2 || args.inputImage3;
|
|
191
|
+
const defaultPrefix = isModification ? 'modified-image' : 'generated-image';
|
|
192
|
+
const filenamePrefix = args.filenamePrefix || defaultPrefix;
|
|
193
|
+
|
|
194
|
+
// Sanitize the prefix to ensure it's a valid filename component
|
|
195
|
+
const sanitizedPrefix = filenamePrefix.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
196
|
+
const filename = `${sanitizedPrefix}-${uniqueId}.${extension}`;
|
|
197
|
+
|
|
198
|
+
// Merge provided tags with default tags
|
|
199
|
+
const defaultTags = ['image', isModification ? 'modified' : 'generated'];
|
|
200
|
+
const providedTags = Array.isArray(args.tags) ? args.tags : [];
|
|
201
|
+
const allTags = [...defaultTags, ...providedTags.filter(tag => !defaultTags.includes(tag))];
|
|
202
|
+
|
|
203
|
+
// Use the centralized utility function to add to collection
|
|
204
|
+
await addFileToCollection(
|
|
205
|
+
args.contextId,
|
|
206
|
+
args.contextKey || '',
|
|
207
|
+
imageUrl,
|
|
208
|
+
imageGcs,
|
|
209
|
+
filename,
|
|
210
|
+
allTags,
|
|
211
|
+
isModification
|
|
212
|
+
? `Modified image from prompt: ${args.detailedInstructions || 'image modification'}`
|
|
213
|
+
: `Generated image from prompt: ${args.detailedInstructions || 'image generation'}`,
|
|
214
|
+
imageHash
|
|
215
|
+
);
|
|
216
|
+
} catch (collectionError) {
|
|
217
|
+
// Log but don't fail - file collection is optional
|
|
218
|
+
pathwayResolver.logWarning(`Failed to add image to file collection: ${collectionError.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
108
221
|
} catch (uploadError) {
|
|
109
222
|
pathwayResolver.logError(`Failed to upload artifact: ${uploadError.message}`);
|
|
110
223
|
// Keep original artifact as fallback
|
|
@@ -117,7 +230,7 @@ export default {
|
|
|
117
230
|
}
|
|
118
231
|
|
|
119
232
|
// Return the urls of the uploaded images as text in the result
|
|
120
|
-
result = result + '\n' + uploadedImages.map(image => image.url).join('\n');
|
|
233
|
+
result = result ? result + '\n' + uploadedImages.map(image => image.url || image).join('\n') : uploadedImages.map(image => image.url || image).join('\n');
|
|
121
234
|
}
|
|
122
235
|
} else {
|
|
123
236
|
// If result is not a CortexResponse, log a warning but return as-is
|
|
@@ -1,148 +1,241 @@
|
|
|
1
1
|
// sys_tool_readfile.js
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
2
|
+
// Tool pathway that reads text files with line number support
|
|
3
|
+
import logger from '../../../../lib/logger.js';
|
|
4
|
+
import { axios } from '../../../../lib/requestExecutor.js';
|
|
5
|
+
import { resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
|
-
prompt:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
{"role": "system", "content": `You are the part of an AI entity named {{aiName}} that can view, hear, and understand files of all sorts (images, videos, audio, pdfs, text, etc.) - you provide the capability to view and analyze files that the user provides.\nThe user has provided you with one or more files in this conversation - you should consider them for context when you respond.\nIf you don't see any files, something has gone wrong in the upload and you should inform the user and have them try again.\n{{renderTemplate AI_DATETIME}}`},
|
|
11
|
-
"{{chatHistory}}",
|
|
12
|
-
]}),
|
|
13
|
-
],
|
|
14
|
-
inputParameters: {
|
|
15
|
-
chatHistory: [{role: '', content: []}],
|
|
16
|
-
contextId: ``,
|
|
17
|
-
aiName: "Jarvis",
|
|
18
|
-
language: "English",
|
|
19
|
-
},
|
|
20
|
-
max_tokens: 8192,
|
|
21
|
-
model: 'gemini-flash-20-vision',
|
|
22
|
-
useInputChunking: false,
|
|
23
|
-
enableDuplicateRequests: false,
|
|
24
|
-
timeout: 600,
|
|
25
|
-
geminiSafetySettings: [{category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH'},
|
|
26
|
-
{category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH'},
|
|
27
|
-
{category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH'},
|
|
28
|
-
{category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH'}],
|
|
29
|
-
toolDefinition: [{
|
|
8
|
+
prompt: [],
|
|
9
|
+
timeout: 60,
|
|
10
|
+
toolDefinition: {
|
|
30
11
|
type: "function",
|
|
31
|
-
icon: "
|
|
12
|
+
icon: "📖",
|
|
32
13
|
function: {
|
|
33
|
-
name: "
|
|
34
|
-
description: "Use
|
|
14
|
+
name: "ReadFile",
|
|
15
|
+
description: "Read text content from a file. Can read the entire file or specific line ranges. Use this to access and analyze text files from your file collection. Supports text files, markdown files, html, csv, and other document formats that can be converted to text, but not images, videos, or audio files or pdfs.",
|
|
35
16
|
parameters: {
|
|
36
17
|
type: "object",
|
|
37
18
|
properties: {
|
|
38
|
-
|
|
19
|
+
file: {
|
|
39
20
|
type: "string",
|
|
40
|
-
description: "
|
|
21
|
+
description: "The file to read: can be the file ID, filename, URL, or hash from your file collection. You can find available files in the Available Files section or ListFileCollection or SearchFileCollection."
|
|
22
|
+
},
|
|
23
|
+
startLine: {
|
|
24
|
+
type: "number",
|
|
25
|
+
description: "Optional: Starting line number (1-indexed). If not provided, reads from the beginning."
|
|
26
|
+
},
|
|
27
|
+
endLine: {
|
|
28
|
+
type: "number",
|
|
29
|
+
description: "Optional: Ending line number (1-indexed). If not provided, reads to the end. Must be >= startLine if startLine is provided."
|
|
30
|
+
},
|
|
31
|
+
maxLines: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Optional: Maximum number of lines to read (default: 1000). Use this to limit the size of the response."
|
|
41
34
|
},
|
|
42
35
|
userMessage: {
|
|
43
36
|
type: "string",
|
|
44
37
|
description: "A user-friendly message that describes what you're doing with this tool"
|
|
45
38
|
}
|
|
46
39
|
},
|
|
47
|
-
required: ["
|
|
40
|
+
required: ["userMessage"]
|
|
48
41
|
}
|
|
49
42
|
}
|
|
50
43
|
},
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
44
|
+
|
|
45
|
+
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
46
|
+
try {
|
|
47
|
+
let { cloudUrl, file, startLine, endLine, maxLines = 1000, contextId, contextKey } = args;
|
|
48
|
+
|
|
49
|
+
// If file parameter is provided, resolve it to a URL using the common utility
|
|
50
|
+
if (file) {
|
|
51
|
+
if (!contextId) {
|
|
52
|
+
const errorResult = {
|
|
53
|
+
success: false,
|
|
54
|
+
error: "contextId is required when using the 'file' parameter. Use ListFileCollection or SearchFileCollection to find available files."
|
|
55
|
+
};
|
|
56
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
57
|
+
return JSON.stringify(errorResult);
|
|
58
|
+
}
|
|
59
|
+
const resolvedUrl = await resolveFileParameter(file, contextId, contextKey);
|
|
60
|
+
if (!resolvedUrl) {
|
|
61
|
+
const errorResult = {
|
|
62
|
+
success: false,
|
|
63
|
+
error: `File not found: "${file}". Use ListFileCollection or SearchFileCollection to find available files.`
|
|
64
|
+
};
|
|
65
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
66
|
+
return JSON.stringify(errorResult);
|
|
67
|
+
}
|
|
68
|
+
cloudUrl = resolvedUrl;
|
|
70
69
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
parameters: {
|
|
80
|
-
type: "object",
|
|
81
|
-
properties: {
|
|
82
|
-
detailedInstructions: {
|
|
83
|
-
type: "string",
|
|
84
|
-
description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
|
|
85
|
-
},
|
|
86
|
-
userMessage: {
|
|
87
|
-
type: "string",
|
|
88
|
-
description: "A user-friendly message that describes what you're doing with this tool"
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
required: ["detailedInstructions", "userMessage"]
|
|
70
|
+
|
|
71
|
+
if (!cloudUrl || typeof cloudUrl !== 'string') {
|
|
72
|
+
const errorResult = {
|
|
73
|
+
success: false,
|
|
74
|
+
error: "Either cloudUrl or file parameter is required and must be a string"
|
|
75
|
+
};
|
|
76
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
77
|
+
return JSON.stringify(errorResult);
|
|
92
78
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
properties: {
|
|
104
|
-
detailedInstructions: {
|
|
105
|
-
type: "string",
|
|
106
|
-
description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
|
|
107
|
-
},
|
|
108
|
-
userMessage: {
|
|
109
|
-
type: "string",
|
|
110
|
-
description: "A user-friendly message that describes what you're doing with this tool"
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
required: ["detailedInstructions", "userMessage"]
|
|
79
|
+
|
|
80
|
+
if (startLine !== undefined) {
|
|
81
|
+
if (typeof startLine !== 'number' || !Number.isInteger(startLine) || startLine < 1) {
|
|
82
|
+
const errorResult = {
|
|
83
|
+
success: false,
|
|
84
|
+
error: "startLine must be a positive integer (1-indexed)"
|
|
85
|
+
};
|
|
86
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
87
|
+
return JSON.stringify(errorResult);
|
|
88
|
+
}
|
|
114
89
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
properties: {
|
|
126
|
-
detailedInstructions: {
|
|
127
|
-
type: "string",
|
|
128
|
-
description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
|
|
129
|
-
},
|
|
130
|
-
userMessage: {
|
|
131
|
-
type: "string",
|
|
132
|
-
description: "A user-friendly message that describes what you're doing with this tool"
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
required: ["detailedInstructions", "userMessage"]
|
|
90
|
+
|
|
91
|
+
if (endLine !== undefined) {
|
|
92
|
+
if (typeof endLine !== 'number' || !Number.isInteger(endLine) || endLine < 1) {
|
|
93
|
+
const errorResult = {
|
|
94
|
+
success: false,
|
|
95
|
+
error: "endLine must be a positive integer (1-indexed)"
|
|
96
|
+
};
|
|
97
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
98
|
+
return JSON.stringify(errorResult);
|
|
99
|
+
}
|
|
136
100
|
}
|
|
101
|
+
|
|
102
|
+
if (startLine !== undefined && endLine !== undefined && endLine < startLine) {
|
|
103
|
+
const errorResult = {
|
|
104
|
+
success: false,
|
|
105
|
+
error: "endLine must be >= startLine"
|
|
106
|
+
};
|
|
107
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
108
|
+
return JSON.stringify(errorResult);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (maxLines !== undefined) {
|
|
112
|
+
if (typeof maxLines !== 'number' || !Number.isInteger(maxLines) || maxLines < 1) {
|
|
113
|
+
const errorResult = {
|
|
114
|
+
success: false,
|
|
115
|
+
error: "maxLines must be a positive integer"
|
|
116
|
+
};
|
|
117
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
118
|
+
return JSON.stringify(errorResult);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Download file content directly from the URL (don't use file handler for content)
|
|
122
|
+
// Use arraybuffer and explicitly decode as UTF-8 to avoid encoding issues
|
|
123
|
+
const response = await axios.get(cloudUrl, {
|
|
124
|
+
responseType: 'arraybuffer',
|
|
125
|
+
timeout: 30000,
|
|
126
|
+
validateStatus: (status) => status >= 200 && status < 400
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (response.status !== 200 || !response.data) {
|
|
130
|
+
throw new Error(`Failed to download file content: ${response.status}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Explicitly decode as UTF-8 to prevent mojibake (encoding corruption)
|
|
134
|
+
const textContent = Buffer.from(response.data).toString('utf8');
|
|
135
|
+
const allLines = textContent.split(/\r?\n/);
|
|
136
|
+
const totalLines = allLines.length;
|
|
137
|
+
|
|
138
|
+
// Handle empty file
|
|
139
|
+
if (totalLines === 0 || (totalLines === 1 && allLines[0] === '')) {
|
|
140
|
+
const result = {
|
|
141
|
+
success: true,
|
|
142
|
+
cloudUrl: cloudUrl,
|
|
143
|
+
totalLines: 0,
|
|
144
|
+
returnedLines: 0,
|
|
145
|
+
startLine: 1,
|
|
146
|
+
endLine: 0,
|
|
147
|
+
content: '',
|
|
148
|
+
truncated: false,
|
|
149
|
+
isEmpty: true
|
|
150
|
+
};
|
|
151
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
152
|
+
return JSON.stringify(result);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Apply line range filtering
|
|
156
|
+
let selectedLines = allLines;
|
|
157
|
+
let actualStartLine = 1;
|
|
158
|
+
let actualEndLine = totalLines;
|
|
159
|
+
let wasTruncatedByRange = false;
|
|
160
|
+
|
|
161
|
+
if (startLine !== undefined || endLine !== undefined) {
|
|
162
|
+
const start = startLine !== undefined ? Math.max(1, Math.min(startLine, totalLines)) - 1 : 0; // Convert to 0-indexed, clamp to valid range
|
|
163
|
+
const end = endLine !== undefined ? Math.min(totalLines, Math.max(1, endLine)) : totalLines; // Clamp to valid range
|
|
164
|
+
|
|
165
|
+
if (startLine !== undefined && startLine > totalLines) {
|
|
166
|
+
const errorResult = {
|
|
167
|
+
success: false,
|
|
168
|
+
error: `startLine (${startLine}) exceeds file length (${totalLines} lines)`
|
|
169
|
+
};
|
|
170
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
171
|
+
return JSON.stringify(errorResult);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
selectedLines = allLines.slice(start, end);
|
|
175
|
+
actualStartLine = start + 1; // Convert back to 1-indexed
|
|
176
|
+
actualEndLine = end;
|
|
177
|
+
wasTruncatedByRange = (endLine !== undefined && endLine < totalLines) || (startLine !== undefined && startLine > 1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Apply maxLines limit
|
|
181
|
+
let wasTruncatedByMaxLines = false;
|
|
182
|
+
if (selectedLines.length > maxLines) {
|
|
183
|
+
selectedLines = selectedLines.slice(0, maxLines);
|
|
184
|
+
wasTruncatedByMaxLines = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const result = {
|
|
188
|
+
success: true,
|
|
189
|
+
cloudUrl: cloudUrl,
|
|
190
|
+
totalLines: totalLines,
|
|
191
|
+
returnedLines: selectedLines.length,
|
|
192
|
+
startLine: actualStartLine,
|
|
193
|
+
endLine: actualEndLine,
|
|
194
|
+
content: selectedLines.join('\n'),
|
|
195
|
+
truncated: wasTruncatedByRange || wasTruncatedByMaxLines,
|
|
196
|
+
truncatedByRange: wasTruncatedByRange,
|
|
197
|
+
truncatedByMaxLines: wasTruncatedByMaxLines
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
201
|
+
return JSON.stringify(result);
|
|
202
|
+
|
|
203
|
+
} catch (e) {
|
|
204
|
+
let errorMsg;
|
|
205
|
+
if (e?.message) {
|
|
206
|
+
errorMsg = e.message;
|
|
207
|
+
} else if (e?.response) {
|
|
208
|
+
// Handle HTTP errors
|
|
209
|
+
const status = e.response.status;
|
|
210
|
+
const statusText = e.response.statusText || '';
|
|
211
|
+
errorMsg = `HTTP ${status}${statusText ? ` ${statusText}` : ''}: Failed to download file`;
|
|
212
|
+
} else if (e?.code === 'ECONNABORTED' || e?.code === 'ETIMEDOUT') {
|
|
213
|
+
errorMsg = 'Request timeout: File download took too long';
|
|
214
|
+
} else if (e?.code === 'ENOTFOUND' || e?.code === 'ECONNREFUSED') {
|
|
215
|
+
errorMsg = `Connection error: ${e.message || 'Unable to reach file server'}`;
|
|
216
|
+
} else if (typeof e === 'string') {
|
|
217
|
+
errorMsg = e;
|
|
218
|
+
} else if (e?.errors && Array.isArray(e.errors)) {
|
|
219
|
+
// Handle AggregateError
|
|
220
|
+
errorMsg = e.errors.map(err => err?.message || String(err)).join('; ');
|
|
221
|
+
} else if (e) {
|
|
222
|
+
errorMsg = String(e);
|
|
223
|
+
} else {
|
|
224
|
+
errorMsg = 'Unknown error occurred while reading file';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
logger.error(`Error reading cloud file ${cloudUrl || file || 'unknown'}: ${errorMsg}`);
|
|
228
|
+
|
|
229
|
+
const errorResult = {
|
|
230
|
+
success: false,
|
|
231
|
+
cloudUrl: cloudUrl || null,
|
|
232
|
+
file: file || null,
|
|
233
|
+
error: errorMsg
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
|
|
237
|
+
return JSON.stringify(errorResult);
|
|
137
238
|
}
|
|
138
|
-
}],
|
|
139
|
-
|
|
140
|
-
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
141
|
-
if (args.detailedInstructions) {
|
|
142
|
-
args.chatHistory.push({role: "user", content: args.detailedInstructions});
|
|
143
|
-
}
|
|
144
|
-
const result = await runAllPrompts({ ...args });
|
|
145
|
-
resolver.tool = JSON.stringify({ toolUsed: "vision" });
|
|
146
|
-
return result;
|
|
147
239
|
}
|
|
148
|
-
}
|
|
240
|
+
};
|
|
241
|
+
|