@aj-archipelago/cortex 1.4.6 → 1.4.8

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.
Files changed (38) hide show
  1. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  2. package/helper-apps/cortex-file-handler/package.json +1 -1
  3. package/helper-apps/cortex-file-handler/src/index.js +27 -4
  4. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +74 -10
  5. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +23 -2
  6. package/helper-apps/cortex-file-handler/src/start.js +2 -0
  7. package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +287 -0
  8. package/helper-apps/cortex-file-handler/tests/start.test.js +1 -1
  9. package/lib/entityConstants.js +1 -1
  10. package/lib/fileUtils.js +1491 -0
  11. package/lib/pathwayTools.js +7 -1
  12. package/lib/util.js +2 -313
  13. package/package.json +4 -3
  14. package/pathways/image_qwen.js +1 -1
  15. package/pathways/system/entity/memory/sys_read_memory.js +17 -3
  16. package/pathways/system/entity/memory/sys_save_memory.js +22 -6
  17. package/pathways/system/entity/sys_entity_agent.js +21 -4
  18. package/pathways/system/entity/tools/sys_tool_analyzefile.js +171 -0
  19. package/pathways/system/entity/tools/sys_tool_codingagent.js +38 -4
  20. package/pathways/system/entity/tools/sys_tool_editfile.js +401 -0
  21. package/pathways/system/entity/tools/sys_tool_file_collection.js +433 -0
  22. package/pathways/system/entity/tools/sys_tool_image.js +172 -10
  23. package/pathways/system/entity/tools/sys_tool_image_gemini.js +123 -10
  24. package/pathways/system/entity/tools/sys_tool_readfile.js +265 -123
  25. package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
  26. package/pathways/system/entity/tools/sys_tool_writefile.js +209 -0
  27. package/pathways/system/workspaces/run_workspace_prompt.js +4 -3
  28. package/pathways/transcribe_gemini.js +2 -1
  29. package/server/executeWorkspace.js +1 -1
  30. package/server/plugins/neuralSpacePlugin.js +2 -6
  31. package/server/plugins/openAiWhisperPlugin.js +2 -1
  32. package/server/plugins/replicateApiPlugin.js +4 -14
  33. package/server/typeDef.js +10 -1
  34. package/tests/integration/features/tools/fileCollection.test.js +857 -0
  35. package/tests/integration/features/tools/fileOperations.test.js +851 -0
  36. package/tests/integration/features/tools/writefile.test.js +350 -0
  37. package/tests/unit/core/fileCollection.test.js +259 -0
  38. package/tests/unit/core/util.test.js +318 -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/util.js';
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: "The first image URL copied exactly from an image_url field in your chat context."
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: "The second input image URL copied exactly from an image_url field in your chat context if there is one."
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: "The third input image URL copied exactly from an image_url field in your chat context if there is one."
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: args.inputImage,
86
- input_image_2: args.inputImage2,
87
- input_image_3: args.inputImage3,
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 imageUrl = await uploadImageToCloud(artifact.data, artifact.mimeType, pathwayResolver);
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,290 @@
1
1
  // sys_tool_readfile.js
2
- // Entity tool that reads one or more files and answers questions about them
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, getMimeTypeFromFilename, isTextMimeType } from '../../../../lib/fileUtils.js';
3
6
 
4
- import { Prompt } from '../../../../server/prompt.js';
7
+ /**
8
+ * Check if a file is a text file type that can be read
9
+ * @param {string} url - File URL or path
10
+ * @returns {boolean} - Returns true if it's a text file, false otherwise
11
+ */
12
+ function isTextFile(url) {
13
+ if (!url || typeof url !== 'string') {
14
+ return false;
15
+ }
16
+
17
+ // Extract filename from URL (remove query string and fragment)
18
+ const urlPath = url.split('?')[0].split('#')[0];
19
+
20
+ // Use existing MIME utility to get MIME type
21
+ const mimeType = getMimeTypeFromFilename(urlPath);
22
+
23
+ if (!mimeType || mimeType === 'application/octet-stream') {
24
+ // Unknown MIME type, reject to be safe
25
+ return false;
26
+ }
27
+
28
+ // Use shared utility function for consistency with other tools
29
+ return isTextMimeType(mimeType);
30
+ }
5
31
 
6
32
  export default {
7
- prompt:
8
- [
9
- new Prompt({ messages: [
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: [{
33
+ prompt: [],
34
+ timeout: 60,
35
+ toolDefinition: {
30
36
  type: "function",
31
- icon: "📄",
37
+ icon: "📖",
32
38
  function: {
33
- name: "AnalyzePDF",
34
- description: "Use specifically for reading, analyzing, and answering questions about PDF file content. Do not use this tool for analyzing and answering questions about other file types.",
39
+ name: "ReadTextFile",
40
+ 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. DOES NOT support binary files, images, videos, or audio files or pdfs.",
35
41
  parameters: {
36
42
  type: "object",
37
43
  properties: {
38
- detailedInstructions: {
44
+ file: {
39
45
  type: "string",
40
- description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
46
+ 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."
47
+ },
48
+ startLine: {
49
+ type: "number",
50
+ description: "Optional: Starting line number (1-indexed). If not provided, reads from the beginning."
51
+ },
52
+ endLine: {
53
+ type: "number",
54
+ description: "Optional: Ending line number (1-indexed). If not provided, reads to the end. Must be >= startLine if startLine is provided."
55
+ },
56
+ maxLines: {
57
+ type: "number",
58
+ description: "Optional: Maximum number of lines to read (default: 1000). Use this to limit the size of the response."
41
59
  },
42
60
  userMessage: {
43
61
  type: "string",
44
62
  description: "A user-friendly message that describes what you're doing with this tool"
45
63
  }
46
64
  },
47
- required: ["detailedInstructions", "userMessage"]
65
+ required: ["userMessage"]
48
66
  }
49
67
  }
50
68
  },
51
- {
52
- type: "function",
53
- icon: "📝",
54
- function: {
55
- name: "AnalyzeText",
56
- description: "Use specifically for reading, analyzing, and answering questions about text files (including csv, json, html, etc.).",
57
- parameters: {
58
- type: "object",
59
- properties: {
60
- detailedInstructions: {
61
- type: "string",
62
- description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
63
- },
64
- userMessage: {
65
- type: "string",
66
- description: "A user-friendly message that describes what you're doing with this tool"
67
- }
68
- },
69
- required: ["detailedInstructions", "userMessage"]
69
+
70
+ executePathway: async ({args, runAllPrompts, resolver}) => {
71
+ try {
72
+ let { cloudUrl, file, startLine, endLine, maxLines = 1000, contextId, contextKey } = args;
73
+
74
+ // If file parameter is provided, resolve it to a URL using the common utility
75
+ if (file) {
76
+ if (!contextId) {
77
+ const errorResult = {
78
+ success: false,
79
+ error: "contextId is required when using the 'file' parameter. Use ListFileCollection or SearchFileCollection to find available files."
80
+ };
81
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
82
+ return JSON.stringify(errorResult);
83
+ }
84
+ const resolvedUrl = await resolveFileParameter(file, contextId, contextKey);
85
+ if (!resolvedUrl) {
86
+ const errorResult = {
87
+ success: false,
88
+ error: `File not found: "${file}". Use ListFileCollection or SearchFileCollection to find available files.`
89
+ };
90
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
91
+ return JSON.stringify(errorResult);
92
+ }
93
+ cloudUrl = resolvedUrl;
70
94
  }
71
- }
72
- },
73
- {
74
- type: "function",
75
- icon: "📝",
76
- function: {
77
- name: "AnalyzeMarkdown",
78
- description: "Use specifically for reading, analyzing, and answering questions about markdown files.",
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"]
95
+
96
+ if (!cloudUrl || typeof cloudUrl !== 'string') {
97
+ const errorResult = {
98
+ success: false,
99
+ error: "Either cloudUrl or file parameter is required and must be a string"
100
+ };
101
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
102
+ return JSON.stringify(errorResult);
92
103
  }
93
- }
94
- },
95
- {
96
- type: "function",
97
- icon: "🖼️",
98
- function: {
99
- name: "AnalyzeImage",
100
- description: "Use specifically for reading, analyzing, and answering questions about image files (jpg, gif, bmp, png, etc). This cannot be used for creating or transforming images.",
101
- parameters: {
102
- type: "object",
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"]
104
+
105
+ // Check if file is a text type before attempting to read
106
+ if (!isTextFile(cloudUrl)) {
107
+ const mimeType = getMimeTypeFromFilename(cloudUrl.split('?')[0].split('#')[0]);
108
+ const detectedType = mimeType || 'unknown type';
109
+
110
+ const errorResult = {
111
+ success: false,
112
+ error: `This tool only supports text files. The file appears to be a non-text file (MIME type: ${detectedType}). For images, PDFs, videos, or other non-text files, please use the AnalyzeImage, AnalyzePDF, or AnalyzeVideo tools instead.`
113
+ };
114
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
115
+ return JSON.stringify(errorResult);
114
116
  }
115
- }
116
- },
117
- {
118
- type: "function",
119
- icon: "🎥",
120
- function: {
121
- name: "AnalyzeVideo",
122
- description: "Use specifically for reading, analyzing, and answering questions about video or audio file content. You MUST use this tool to look at video or audio files.",
123
- parameters: {
124
- type: "object",
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"]
117
+
118
+ if (startLine !== undefined) {
119
+ if (typeof startLine !== 'number' || !Number.isInteger(startLine) || startLine < 1) {
120
+ const errorResult = {
121
+ success: false,
122
+ error: "startLine must be a positive integer (1-indexed)"
123
+ };
124
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
125
+ return JSON.stringify(errorResult);
126
+ }
136
127
  }
128
+
129
+ if (endLine !== undefined) {
130
+ if (typeof endLine !== 'number' || !Number.isInteger(endLine) || endLine < 1) {
131
+ const errorResult = {
132
+ success: false,
133
+ error: "endLine must be a positive integer (1-indexed)"
134
+ };
135
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
136
+ return JSON.stringify(errorResult);
137
+ }
138
+ }
139
+
140
+ if (startLine !== undefined && endLine !== undefined && endLine < startLine) {
141
+ const errorResult = {
142
+ success: false,
143
+ error: "endLine must be >= startLine"
144
+ };
145
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
146
+ return JSON.stringify(errorResult);
147
+ }
148
+
149
+ if (maxLines !== undefined) {
150
+ if (typeof maxLines !== 'number' || !Number.isInteger(maxLines) || maxLines < 1) {
151
+ const errorResult = {
152
+ success: false,
153
+ error: "maxLines must be a positive integer"
154
+ };
155
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
156
+ return JSON.stringify(errorResult);
157
+ }
158
+ }
159
+ // Download file content directly from the URL (don't use file handler for content)
160
+ // Use arraybuffer and explicitly decode as UTF-8 to avoid encoding issues
161
+ const response = await axios.get(cloudUrl, {
162
+ responseType: 'arraybuffer',
163
+ timeout: 30000,
164
+ validateStatus: (status) => status >= 200 && status < 400
165
+ });
166
+
167
+ if (response.status !== 200 || !response.data) {
168
+ throw new Error(`Failed to download file content: ${response.status}`);
169
+ }
170
+
171
+ // Secondary check: verify content-type header if available
172
+ const contentType = response.headers['content-type'] || response.headers['Content-Type'];
173
+ if (contentType && !isTextMimeType(contentType)) {
174
+ const errorResult = {
175
+ success: false,
176
+ error: `This tool only supports text files. The file appears to be a non-text file (Content-Type: ${contentType}). For images, PDFs, videos, or other non-text files, please use the AnalyzeImage, AnalyzePDF, or AnalyzeVideo tools instead.`
177
+ };
178
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
179
+ return JSON.stringify(errorResult);
180
+ }
181
+
182
+ // Explicitly decode as UTF-8 to prevent mojibake (encoding corruption)
183
+ const textContent = Buffer.from(response.data).toString('utf8');
184
+ const allLines = textContent.split(/\r?\n/);
185
+ const totalLines = allLines.length;
186
+
187
+ // Handle empty file
188
+ if (totalLines === 0 || (totalLines === 1 && allLines[0] === '')) {
189
+ const result = {
190
+ success: true,
191
+ cloudUrl: cloudUrl,
192
+ totalLines: 0,
193
+ returnedLines: 0,
194
+ startLine: 1,
195
+ endLine: 0,
196
+ content: '',
197
+ truncated: false,
198
+ isEmpty: true
199
+ };
200
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
201
+ return JSON.stringify(result);
202
+ }
203
+
204
+ // Apply line range filtering
205
+ let selectedLines = allLines;
206
+ let actualStartLine = 1;
207
+ let actualEndLine = totalLines;
208
+ let wasTruncatedByRange = false;
209
+
210
+ if (startLine !== undefined || endLine !== undefined) {
211
+ const start = startLine !== undefined ? Math.max(1, Math.min(startLine, totalLines)) - 1 : 0; // Convert to 0-indexed, clamp to valid range
212
+ const end = endLine !== undefined ? Math.min(totalLines, Math.max(1, endLine)) : totalLines; // Clamp to valid range
213
+
214
+ if (startLine !== undefined && startLine > totalLines) {
215
+ const errorResult = {
216
+ success: false,
217
+ error: `startLine (${startLine}) exceeds file length (${totalLines} lines)`
218
+ };
219
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
220
+ return JSON.stringify(errorResult);
221
+ }
222
+
223
+ selectedLines = allLines.slice(start, end);
224
+ actualStartLine = start + 1; // Convert back to 1-indexed
225
+ actualEndLine = end;
226
+ wasTruncatedByRange = (endLine !== undefined && endLine < totalLines) || (startLine !== undefined && startLine > 1);
227
+ }
228
+
229
+ // Apply maxLines limit
230
+ let wasTruncatedByMaxLines = false;
231
+ if (selectedLines.length > maxLines) {
232
+ selectedLines = selectedLines.slice(0, maxLines);
233
+ wasTruncatedByMaxLines = true;
234
+ }
235
+
236
+ const result = {
237
+ success: true,
238
+ cloudUrl: cloudUrl,
239
+ totalLines: totalLines,
240
+ returnedLines: selectedLines.length,
241
+ startLine: actualStartLine,
242
+ endLine: actualEndLine,
243
+ content: selectedLines.join('\n'),
244
+ truncated: wasTruncatedByRange || wasTruncatedByMaxLines,
245
+ truncatedByRange: wasTruncatedByRange,
246
+ truncatedByMaxLines: wasTruncatedByMaxLines
247
+ };
248
+
249
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
250
+ return JSON.stringify(result);
251
+
252
+ } catch (e) {
253
+ let errorMsg;
254
+ if (e?.message) {
255
+ errorMsg = e.message;
256
+ } else if (e?.response) {
257
+ // Handle HTTP errors
258
+ const status = e.response.status;
259
+ const statusText = e.response.statusText || '';
260
+ errorMsg = `HTTP ${status}${statusText ? ` ${statusText}` : ''}: Failed to download file`;
261
+ } else if (e?.code === 'ECONNABORTED' || e?.code === 'ETIMEDOUT') {
262
+ errorMsg = 'Request timeout: File download took too long';
263
+ } else if (e?.code === 'ENOTFOUND' || e?.code === 'ECONNREFUSED') {
264
+ errorMsg = `Connection error: ${e.message || 'Unable to reach file server'}`;
265
+ } else if (typeof e === 'string') {
266
+ errorMsg = e;
267
+ } else if (e?.errors && Array.isArray(e.errors)) {
268
+ // Handle AggregateError
269
+ errorMsg = e.errors.map(err => err?.message || String(err)).join('; ');
270
+ } else if (e) {
271
+ errorMsg = String(e);
272
+ } else {
273
+ errorMsg = 'Unknown error occurred while reading file';
274
+ }
275
+
276
+ logger.error(`Error reading cloud file ${cloudUrl || file || 'unknown'}: ${errorMsg}`);
277
+
278
+ const errorResult = {
279
+ success: false,
280
+ cloudUrl: cloudUrl || null,
281
+ file: file || null,
282
+ error: errorMsg
283
+ };
284
+
285
+ resolver.tool = JSON.stringify({ toolUsed: "ReadFile" });
286
+ return JSON.stringify(errorResult);
137
287
  }
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
288
  }
148
- }
289
+ };
290
+