@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.
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 +1481 -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 +403 -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 +217 -124
  25. package/pathways/system/entity/tools/sys_tool_validate_url.js +137 -0
  26. package/pathways/system/entity/tools/sys_tool_writefile.js +211 -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 +858 -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 +320 -1
@@ -0,0 +1,137 @@
1
+ // sys_tool_validate_url.js
2
+ // Tool pathway that validates URLs by performing HEAD requests to check if they are accessible
3
+ import logger from '../../../../lib/logger.js';
4
+
5
+ export default {
6
+ prompt: [],
7
+ timeout: 30,
8
+ toolDefinition: {
9
+ type: "function",
10
+ icon: "🔗",
11
+ function: {
12
+ name: "ValidateUrl",
13
+ description: "This tool validates URLs by performing a HEAD request to check if they are accessible and return valid responses. Use this to verify that links and image URLs are valid before including them in responses.",
14
+ parameters: {
15
+ type: "object",
16
+ properties: {
17
+ url: {
18
+ type: "string",
19
+ description: "The URL to validate (can be a link or image URL)"
20
+ },
21
+ userMessage: {
22
+ type: "string",
23
+ description: "A user-friendly message that describes what you're doing with this tool"
24
+ }
25
+ },
26
+ required: ["url", "userMessage"]
27
+ }
28
+ }
29
+ },
30
+
31
+ executePathway: async ({args, runAllPrompts, resolver}) => {
32
+ const { url } = args;
33
+
34
+ if (!url || typeof url !== 'string') {
35
+ throw new Error("URL parameter is required and must be a string");
36
+ }
37
+
38
+ // Basic URL format validation
39
+ try {
40
+ new URL(url);
41
+ } catch (e) {
42
+ return JSON.stringify({
43
+ valid: false,
44
+ error: "Invalid URL format",
45
+ url: url,
46
+ statusCode: null,
47
+ contentType: null
48
+ });
49
+ }
50
+
51
+ try {
52
+ // Perform HEAD request to validate the URL
53
+ // Use a timeout to avoid hanging on slow/unresponsive servers
54
+ const controller = new AbortController();
55
+ const timeoutId = setTimeout(() => controller.abort(), 2000); // 2 second timeout for HEAD
56
+
57
+ let response;
58
+ try {
59
+ response = await fetch(url, {
60
+ method: 'HEAD',
61
+ signal: controller.signal,
62
+ redirect: 'follow',
63
+ headers: {
64
+ 'User-Agent': 'Mozilla/5.0 (compatible; Cortex/1.0)'
65
+ }
66
+ });
67
+ clearTimeout(timeoutId);
68
+ } catch (fetchError) {
69
+ clearTimeout(timeoutId);
70
+
71
+ // If HEAD fails, try GET with range request (more compatible)
72
+ if (fetchError.name === 'AbortError') {
73
+ throw new Error("Request timeout - URL did not respond in time");
74
+ }
75
+
76
+ // Some servers don't support HEAD, try GET with range
77
+ try {
78
+ const getController = new AbortController();
79
+ const getTimeoutId = setTimeout(() => getController.abort(), 25000);
80
+
81
+ response = await fetch(url, {
82
+ method: 'GET',
83
+ signal: getController.signal,
84
+ redirect: 'follow',
85
+ headers: {
86
+ 'User-Agent': 'Mozilla/5.0 (compatible; Cortex/1.0)',
87
+ 'Range': 'bytes=0-0' // Only request first byte to minimize data transfer
88
+ }
89
+ });
90
+ clearTimeout(getTimeoutId);
91
+ } catch (getError) {
92
+ if (getError.name === 'AbortError') {
93
+ throw new Error("Request timeout - URL did not respond in time");
94
+ }
95
+ throw getError;
96
+ }
97
+ }
98
+
99
+ const statusCode = response.status;
100
+ const contentType = response.headers.get('content-type') || '';
101
+ const contentLength = response.headers.get('content-length');
102
+ const finalUrl = response.url || url; // Get final URL after redirects
103
+
104
+ // Consider 2xx and 3xx status codes as valid
105
+ const isValid = statusCode >= 200 && statusCode < 400;
106
+
107
+ const result = {
108
+ valid: isValid,
109
+ url: finalUrl,
110
+ statusCode: statusCode,
111
+ contentType: contentType,
112
+ contentLength: contentLength ? parseInt(contentLength, 10) : null,
113
+ message: isValid
114
+ ? `URL is valid and accessible (HTTP ${statusCode})`
115
+ : `URL returned error status (HTTP ${statusCode})`
116
+ };
117
+
118
+ resolver.tool = JSON.stringify({ toolUsed: "ValidateUrl" });
119
+ return JSON.stringify(result);
120
+
121
+ } catch (e) {
122
+ logger.error(`Error validating URL ${url}: ${e.message}`);
123
+
124
+ const errorResult = {
125
+ valid: false,
126
+ url: url,
127
+ statusCode: null,
128
+ contentType: null,
129
+ error: e.message || "Unknown error occurred while validating URL"
130
+ };
131
+
132
+ resolver.tool = JSON.stringify({ toolUsed: "ValidateUrl" });
133
+ return JSON.stringify(errorResult);
134
+ }
135
+ }
136
+ };
137
+
@@ -0,0 +1,211 @@
1
+ // sys_tool_writefile.js
2
+ // Entity tool that writes content to a file and uploads it to cloud storage
3
+ import logger from '../../../../lib/logger.js';
4
+ import { uploadFileToCloud, addFileToCollection, getMimeTypeFromFilename } from '../../../../lib/fileUtils.js';
5
+
6
+ // Helper function to format file size
7
+ function formatFileSize(bytes) {
8
+ if (bytes === 0) return '0 B';
9
+ const k = 1024;
10
+ const sizes = ['B', 'KB', 'MB', 'GB'];
11
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
12
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
13
+ }
14
+
15
+ export default {
16
+ prompt: [],
17
+ timeout: 60,
18
+ toolDefinition: {
19
+ type: "function",
20
+ icon: "✍️",
21
+ function: {
22
+ name: "WriteFile",
23
+ description: "Write content to a file and upload it to cloud storage. The file will be added to your file collection for future reference. Use this to save text, code, data, or any content you generate to a file.",
24
+ parameters: {
25
+ type: "object",
26
+ properties: {
27
+ content: {
28
+ type: "string",
29
+ description: "The content to write to the file"
30
+ },
31
+ filename: {
32
+ type: "string",
33
+ description: "The filename for the file (e.g., 'output.txt', 'data.json', 'script.py'). Include the file extension."
34
+ },
35
+ tags: {
36
+ type: "array",
37
+ items: {
38
+ type: "string"
39
+ },
40
+ description: "Optional: Array of tags to categorize the file (e.g., ['code', 'output', 'data'])"
41
+ },
42
+ notes: {
43
+ type: "string",
44
+ description: "Optional: Notes or description about the file"
45
+ },
46
+ userMessage: {
47
+ type: "string",
48
+ description: "A user-friendly message that describes what you're doing with this tool"
49
+ }
50
+ },
51
+ required: ["content", "filename", "userMessage"]
52
+ }
53
+ }
54
+ },
55
+
56
+ executePathway: async ({args, runAllPrompts, resolver}) => {
57
+ const { content, filename, tags = [], notes = '', contextId, contextKey } = args;
58
+
59
+ // Validate inputs and return JSON error if invalid
60
+ if (content === undefined || content === null) {
61
+ const errorResult = {
62
+ success: false,
63
+ filename: filename || 'unknown',
64
+ error: "content is required and cannot be null or undefined"
65
+ };
66
+ resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
67
+ return JSON.stringify(errorResult);
68
+ }
69
+
70
+ if (typeof content !== 'string') {
71
+ const errorResult = {
72
+ success: false,
73
+ filename: filename || 'unknown',
74
+ error: `content must be a string, got ${typeof content}`
75
+ };
76
+ resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
77
+ return JSON.stringify(errorResult);
78
+ }
79
+
80
+ if (!filename || typeof filename !== 'string') {
81
+ const errorResult = {
82
+ success: false,
83
+ filename: 'unknown',
84
+ error: "filename is required and must be a non-empty string"
85
+ };
86
+ resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
87
+ return JSON.stringify(errorResult);
88
+ }
89
+
90
+ // Validate filename doesn't contain invalid characters
91
+ if (filename.trim().length === 0) {
92
+ const errorResult = {
93
+ success: false,
94
+ filename: filename,
95
+ error: "filename cannot be empty or whitespace only"
96
+ };
97
+ resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
98
+ return JSON.stringify(errorResult);
99
+ }
100
+
101
+ // Validate tags if provided
102
+ if (tags !== undefined && !Array.isArray(tags)) {
103
+ const errorResult = {
104
+ success: false,
105
+ filename: filename,
106
+ error: "tags must be an array if provided"
107
+ };
108
+ resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
109
+ return JSON.stringify(errorResult);
110
+ }
111
+
112
+ // Validate notes if provided
113
+ if (notes !== undefined && typeof notes !== 'string') {
114
+ const errorResult = {
115
+ success: false,
116
+ filename: filename,
117
+ error: "notes must be a string if provided"
118
+ };
119
+ resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
120
+ return JSON.stringify(errorResult);
121
+ }
122
+
123
+ try {
124
+ // Convert content to buffer
125
+ const fileBuffer = Buffer.from(content, 'utf8');
126
+ logger.info(`Prepared content buffer for file: ${filename} (${fileBuffer.length} bytes)`);
127
+
128
+ // Determine MIME type from filename using utility function
129
+ let mimeType = getMimeTypeFromFilename(filename, 'text/plain');
130
+
131
+ // Add charset=utf-8 for text-based MIME types to ensure proper encoding
132
+ if (mimeType.startsWith('text/') || mimeType === 'application/json' ||
133
+ mimeType === 'application/javascript' || mimeType === 'application/typescript' ||
134
+ mimeType === 'application/xml') {
135
+ mimeType = `${mimeType}; charset=utf-8`;
136
+ }
137
+
138
+ // Upload file to cloud storage (this will compute hash and check for duplicates)
139
+ const uploadResult = await uploadFileToCloud(
140
+ fileBuffer,
141
+ mimeType,
142
+ filename,
143
+ resolver
144
+ );
145
+
146
+ if (!uploadResult || !uploadResult.url) {
147
+ throw new Error('Failed to upload file to cloud storage');
148
+ }
149
+
150
+ // Add to file collection if contextId is provided
151
+ let fileEntry = null;
152
+ if (contextId) {
153
+ try {
154
+ fileEntry = await addFileToCollection(
155
+ contextId,
156
+ contextKey || null,
157
+ uploadResult.url,
158
+ uploadResult.gcs || null,
159
+ filename,
160
+ tags,
161
+ notes,
162
+ uploadResult.hash || null,
163
+ null, // fileUrl - not needed since we already uploaded
164
+ resolver
165
+ );
166
+ } catch (collectionError) {
167
+ // Log but don't fail - file collection is optional
168
+ logger.warn(`Failed to add file to collection: ${collectionError.message}`);
169
+ }
170
+ }
171
+
172
+ const fileSize = Buffer.byteLength(content, 'utf8');
173
+ const result = {
174
+ success: true,
175
+ filename: filename,
176
+ url: uploadResult.url,
177
+ gcs: uploadResult.gcs || null,
178
+ hash: uploadResult.hash || null,
179
+ fileId: fileEntry?.id || null,
180
+ size: fileSize,
181
+ sizeFormatted: formatFileSize(fileSize),
182
+ message: `File "${filename}" written and uploaded successfully (${formatFileSize(fileSize)})`
183
+ };
184
+
185
+ resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
186
+ return JSON.stringify(result);
187
+
188
+ } catch (error) {
189
+ let errorMsg;
190
+ if (error?.message) {
191
+ errorMsg = error.message;
192
+ } else if (error?.errors && Array.isArray(error.errors)) {
193
+ // Handle AggregateError
194
+ errorMsg = error.errors.map(e => e?.message || String(e)).join('; ');
195
+ } else {
196
+ errorMsg = String(error);
197
+ }
198
+ logger.error(`Error writing file ${filename}: ${errorMsg}`);
199
+
200
+ const errorResult = {
201
+ success: false,
202
+ filename: filename,
203
+ error: errorMsg
204
+ };
205
+
206
+ resolver.tool = JSON.stringify({ toolUsed: "WriteFile" });
207
+ return JSON.stringify(errorResult);
208
+ }
209
+ }
210
+ };
211
+
@@ -1,5 +1,6 @@
1
1
  import { config } from '../../../config.js';
2
- import { chatArgsHasImageUrl, chatArgsHasType, getAvailableFiles, removeOldImageAndFileContent } from '../../../lib/util.js';
2
+ import { chatArgsHasImageUrl, chatArgsHasType, removeOldImageAndFileContent } from '../../../lib/util.js';
3
+ import { getAvailableFiles } from '../../../lib/fileUtils.js';
3
4
  import { loadEntityConfig } from '../../../pathways/system/entity/tools/shared/sys_entity_tools.js';
4
5
  import { Prompt } from '../../../server/prompt.js';
5
6
 
@@ -48,8 +49,8 @@ export default {
48
49
  model
49
50
  };
50
51
 
51
- // Extract available files from chat history
52
- const availableFiles = getAvailableFiles(args.chatHistory);
52
+ // Extract available files from chat history (syncs to collection if contextId available)
53
+ const availableFiles = await getAvailableFiles(args.chatHistory, args.contextId, args.contextKey);
53
54
 
54
55
  // Check for both image and file content (CSV files have type 'file', not 'image_url')
55
56
  const hasImageContent = chatArgsHasImageUrl(args);
@@ -1,6 +1,7 @@
1
1
  import logger from "../lib/logger.js";
2
2
  import { publishRequestProgress } from "../lib/redisSubscription.js";
3
- import { alignSubtitles, getMediaChunks } from "../lib/util.js";
3
+ import { alignSubtitles } from "../lib/util.js";
4
+ import { getMediaChunks } from "../lib/fileUtils.js";
4
5
  import { Prompt } from "../server/prompt.js";
5
6
 
6
7
  const OFFSET_CHUNK = 500; //seconds of each chunk offset, only used if helper does not provide
@@ -24,7 +24,7 @@ const resolveAndAddFileContent = async (pathways, pathwayArgs, requestId, config
24
24
  for (const pathway of pathwaysWithFiles) {
25
25
  if (pathway.fileHashes && pathway.fileHashes.length > 0) {
26
26
  try {
27
- const { resolveFileHashesToContent } = await import('../lib/util.js');
27
+ const { resolveFileHashesToContent } = await import('../lib/fileUtils.js');
28
28
  const fileContent = await resolveFileHashesToContent(pathway.fileHashes, config);
29
29
 
30
30
  // Add file content to chatHistory if not already present (only do this once)
@@ -2,12 +2,8 @@ import ModelPlugin from "./modelPlugin.js";
2
2
  import fs from "fs";
3
3
  import FormData from "form-data";
4
4
  import logger from "../../lib/logger.js";
5
- import {
6
- alignSubtitles,
7
- deleteTempPath,
8
- downloadFile,
9
- getMediaChunks,
10
- } from "../../lib/util.js";
5
+ import { alignSubtitles } from "../../lib/util.js";
6
+ import { deleteTempPath, downloadFile, getMediaChunks } from "../../lib/fileUtils.js";
11
7
  import CortexRequest from "../../lib/cortexRequest.js";
12
8
  import { publishRequestProgress } from "../../lib/redisSubscription.js";
13
9
 
@@ -6,7 +6,8 @@ import fs from 'fs';
6
6
  import { publishRequestProgress } from '../../lib/redisSubscription.js';
7
7
  import logger from '../../lib/logger.js';
8
8
  import CortexRequest from '../../lib/cortexRequest.js';
9
- import { downloadFile, deleteTempPath, convertSrtToText, alignSubtitles, getMediaChunks, markCompletedForCleanUp } from '../../lib/util.js';
9
+ import { convertSrtToText, alignSubtitles } from '../../lib/util.js';
10
+ import { downloadFile, deleteTempPath, getMediaChunks, markCompletedForCleanUp } from '../../lib/fileUtils.js';
10
11
 
11
12
 
12
13
  const OFFSET_CHUNK = 500; //seconds of each chunk offset, only used if helper does not provide
@@ -3,6 +3,7 @@ import ModelPlugin from "./modelPlugin.js";
3
3
  import CortexResponse from "../../lib/cortexResponse.js";
4
4
  import logger from "../../lib/logger.js";
5
5
  import axios from "axios";
6
+ import mime from "mime-types";
6
7
 
7
8
  class ReplicateApiPlugin extends ModelPlugin {
8
9
  constructor(pathway, model) {
@@ -475,20 +476,9 @@ class ReplicateApiPlugin extends ModelPlugin {
475
476
 
476
477
  // Helper method to determine MIME type from URL extension
477
478
  getMimeTypeFromUrl(url) {
478
- const extension = url.split('.').pop().toLowerCase();
479
- switch (extension) {
480
- case 'jpg':
481
- case 'jpeg':
482
- return 'image/jpeg';
483
- case 'png':
484
- return 'image/png';
485
- case 'gif':
486
- return 'image/gif';
487
- case 'webp':
488
- return 'image/webp';
489
- default:
490
- return 'image/jpeg'; // Default fallback
491
- }
479
+ // Extract path from URL (remove query params and fragments)
480
+ const urlPath = url.split('?')[0].split('#')[0];
481
+ return mime.lookup(urlPath) || 'image/jpeg'; // Default fallback for images
492
482
  }
493
483
 
494
484
  // Override the logging function to display the request and response
package/server/typeDef.js CHANGED
@@ -90,6 +90,10 @@ const getGraphQlType = (value) => {
90
90
  // Check if it's an integer or float
91
91
  return Number.isInteger(value) ? {type: 'Int', defaultValue: value} : {type: 'Float', defaultValue: value};
92
92
  case 'object':
93
+ // Handle null explicitly (typeof null === 'object' in JavaScript)
94
+ if (value === null) {
95
+ return {type: 'String', defaultValue: '""'};
96
+ }
93
97
  if (Array.isArray(value)) {
94
98
  if (value.length > 0 && typeof(value[0]) === 'string') {
95
99
  return {type: '[String]', defaultValue: JSON.stringify(value)};
@@ -104,7 +108,12 @@ const getGraphQlType = (value) => {
104
108
  }
105
109
  }
106
110
  } else {
107
- return {type: `[${value.objName}]`, defaultValue: JSON.stringify(value)};
111
+ // Check if it has objName property (for custom object types)
112
+ if (value && value.objName) {
113
+ return {type: `[${value.objName}]`, defaultValue: JSON.stringify(value)};
114
+ }
115
+ // Otherwise treat as generic object (stringify it)
116
+ return {type: 'String', defaultValue: `"${JSON.stringify(value).replace(/"/g, '\\"')}"`};
108
117
  }
109
118
  default:
110
119
  return {type: 'String', defaultValue: `"${value}"`};