@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,171 @@
|
|
|
1
|
+
// sys_tool_analyzefile.js
|
|
2
|
+
// Entity tool that analyzes one or more files and answers questions about them
|
|
3
|
+
|
|
4
|
+
import { Prompt } from '../../../../server/prompt.js';
|
|
5
|
+
import { generateFileMessageContent, injectFileIntoChatHistory } from '../../../../lib/fileUtils.js';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
prompt:
|
|
9
|
+
[
|
|
10
|
+
new Prompt({ messages: [
|
|
11
|
+
{"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}}`},
|
|
12
|
+
"{{chatHistory}}",
|
|
13
|
+
]}),
|
|
14
|
+
],
|
|
15
|
+
inputParameters: {
|
|
16
|
+
chatHistory: [{role: '', content: []}],
|
|
17
|
+
contextId: ``,
|
|
18
|
+
contextKey: ``,
|
|
19
|
+
aiName: "Jarvis",
|
|
20
|
+
language: "English",
|
|
21
|
+
},
|
|
22
|
+
max_tokens: 8192,
|
|
23
|
+
model: 'gemini-flash-20-vision',
|
|
24
|
+
useInputChunking: false,
|
|
25
|
+
enableDuplicateRequests: false,
|
|
26
|
+
timeout: 600,
|
|
27
|
+
geminiSafetySettings: [{category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH'},
|
|
28
|
+
{category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH'},
|
|
29
|
+
{category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH'},
|
|
30
|
+
{category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH'}],
|
|
31
|
+
toolDefinition: [{
|
|
32
|
+
type: "function",
|
|
33
|
+
icon: "📄",
|
|
34
|
+
function: {
|
|
35
|
+
name: "AnalyzePDF",
|
|
36
|
+
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.",
|
|
37
|
+
parameters: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
detailedInstructions: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
|
|
43
|
+
},
|
|
44
|
+
file: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Optional: The file to analyze (from ListFileCollection or SearchFileCollection): can be the hash, the filename, the URL, or the GCS URL. You can find available files in the availableFiles section."
|
|
47
|
+
},
|
|
48
|
+
userMessage: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
required: ["detailedInstructions", "userMessage"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "function",
|
|
59
|
+
icon: "📝",
|
|
60
|
+
function: {
|
|
61
|
+
name: "AnalyzeText",
|
|
62
|
+
description: "Use specifically for reading, analyzing, and answering questions about text files (including csv, json, html, etc.).",
|
|
63
|
+
parameters: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
detailedInstructions: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
|
|
69
|
+
},
|
|
70
|
+
userMessage: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
required: ["detailedInstructions", "userMessage"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "function",
|
|
81
|
+
icon: "📝",
|
|
82
|
+
function: {
|
|
83
|
+
name: "AnalyzeMarkdown",
|
|
84
|
+
description: "Use specifically for reading, analyzing, and answering questions about markdown files.",
|
|
85
|
+
parameters: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
detailedInstructions: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
|
|
91
|
+
},
|
|
92
|
+
userMessage: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
required: ["detailedInstructions", "userMessage"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: "function",
|
|
103
|
+
icon: "🖼️",
|
|
104
|
+
function: {
|
|
105
|
+
name: "AnalyzeImage",
|
|
106
|
+
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.",
|
|
107
|
+
parameters: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
detailedInstructions: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
|
|
113
|
+
},
|
|
114
|
+
file: {
|
|
115
|
+
type: "string",
|
|
116
|
+
description: "Optional: The file to analyze (from ListFileCollection or SearchFileCollection): can be the hash, the filename, the URL, or the GCS URL. You can find available files in the availableFiles section."
|
|
117
|
+
},
|
|
118
|
+
userMessage: {
|
|
119
|
+
type: "string",
|
|
120
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
required: ["detailedInstructions", "userMessage"]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: "function",
|
|
129
|
+
icon: "🎥",
|
|
130
|
+
function: {
|
|
131
|
+
name: "AnalyzeVideo",
|
|
132
|
+
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.",
|
|
133
|
+
parameters: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
detailedInstructions: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Detailed instructions about what you need the tool to do - questions you need answered about the files, etc."
|
|
139
|
+
},
|
|
140
|
+
file: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Optional: The file to analyze (from ListFileCollection or SearchFileCollection): can be the hash, the filename, the URL, or the GCS URL. You can find available files in the availableFiles section."
|
|
143
|
+
},
|
|
144
|
+
userMessage: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
required: ["detailedInstructions", "userMessage"]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}],
|
|
153
|
+
|
|
154
|
+
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
155
|
+
// Generate file message content and inject file if provided
|
|
156
|
+
if (args.file) {
|
|
157
|
+
const fileContent = await generateFileMessageContent(args.file, args.contextId, args.contextKey);
|
|
158
|
+
if (!fileContent) {
|
|
159
|
+
throw new Error(`File not found: "${args.file}". Use ListFileCollection or SearchFileCollection to find available files.`);
|
|
160
|
+
}
|
|
161
|
+
args.chatHistory = injectFileIntoChatHistory(args.chatHistory, fileContent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (args.detailedInstructions) {
|
|
165
|
+
args.chatHistory.push({role: "user", content: args.detailedInstructions});
|
|
166
|
+
}
|
|
167
|
+
const result = await runAllPrompts({ ...args });
|
|
168
|
+
resolver.tool = JSON.stringify({ toolUsed: "vision" });
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { QueueServiceClient } from '@azure/storage-queue';
|
|
5
5
|
import logger from '../../../../lib/logger.js';
|
|
6
|
+
import { resolveFileParameter } from '../../../../lib/fileUtils.js';
|
|
6
7
|
|
|
7
8
|
const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
|
|
8
9
|
let queueClient;
|
|
@@ -58,7 +59,7 @@ export default {
|
|
|
58
59
|
},
|
|
59
60
|
inputFiles: {
|
|
60
61
|
type: "string",
|
|
61
|
-
description: "A list of input files that the coding agent must use to complete the task. Each file should be the
|
|
62
|
+
description: "A list of input files (from Available Files section or ListFileCollection or SearchFileCollection) that the coding agent must use to complete the task. Each file should be the hash or filename. Omit this parameter if no input files are needed."
|
|
62
63
|
},
|
|
63
64
|
userMessage: {
|
|
64
65
|
type: "string",
|
|
@@ -76,12 +77,45 @@ export default {
|
|
|
76
77
|
|
|
77
78
|
executePathway: async ({args, resolver}) => {
|
|
78
79
|
try {
|
|
79
|
-
const { codingTask, userMessage, inputFiles, codingTaskKeywords } = args;
|
|
80
|
-
const { contextId } = args;
|
|
80
|
+
const { codingTask, userMessage, inputFiles, codingTaskKeywords, contextId, contextKey } = args;
|
|
81
81
|
|
|
82
82
|
let taskSuffix = "";
|
|
83
83
|
if (inputFiles) {
|
|
84
|
-
|
|
84
|
+
if (!contextId) {
|
|
85
|
+
throw new Error("contextId is required when using the 'inputFiles' parameter. Use ListFileCollection or SearchFileCollection to find available files.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Resolve file parameters to URLs
|
|
89
|
+
// inputFiles can be a comma-separated, newline-separated, or space-separated list
|
|
90
|
+
const fileReferences = inputFiles
|
|
91
|
+
.split(/[,\n\r]+/)
|
|
92
|
+
.map(ref => ref.trim())
|
|
93
|
+
.filter(ref => ref.length > 0);
|
|
94
|
+
|
|
95
|
+
const resolvedUrls = [];
|
|
96
|
+
const failedFiles = [];
|
|
97
|
+
|
|
98
|
+
for (const fileRef of fileReferences) {
|
|
99
|
+
// Try to resolve each file reference
|
|
100
|
+
const resolvedUrl = await resolveFileParameter(fileRef, contextId, contextKey);
|
|
101
|
+
if (resolvedUrl) {
|
|
102
|
+
resolvedUrls.push(resolvedUrl);
|
|
103
|
+
} else {
|
|
104
|
+
failedFiles.push(fileRef);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fail early if any files couldn't be resolved
|
|
109
|
+
if (failedFiles.length > 0) {
|
|
110
|
+
const fileList = failedFiles.length === 1
|
|
111
|
+
? `"${failedFiles[0]}"`
|
|
112
|
+
: failedFiles.map(f => `"${f}"`).join(', ');
|
|
113
|
+
throw new Error(`File(s) not found: ${fileList}. Use ListFileCollection or SearchFileCollection to find available files.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (resolvedUrls.length > 0) {
|
|
117
|
+
taskSuffix = `You must use the following files as input to complete the task: ${resolvedUrls.join(', ')}.`
|
|
118
|
+
}
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// sys_tool_editfile.js
|
|
2
|
+
// Entity tool that modifies existing files by replacing line ranges or exact string matches
|
|
3
|
+
import logger from '../../../../lib/logger.js';
|
|
4
|
+
import { axios } from '../../../../lib/requestExecutor.js';
|
|
5
|
+
import { uploadFileToCloud, findFileInCollection, loadFileCollection, saveFileCollection, getMimeTypeFromFilename, resolveFileParameter, deleteFileByHash, modifyFileCollectionWithLock } from '../../../../lib/fileUtils.js';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
prompt: [],
|
|
9
|
+
timeout: 120,
|
|
10
|
+
toolDefinition: [
|
|
11
|
+
{
|
|
12
|
+
type: "function",
|
|
13
|
+
icon: "✏️",
|
|
14
|
+
function: {
|
|
15
|
+
name: "EditFileByLine",
|
|
16
|
+
description: "Modify an existing file by replacing a range of lines. Use this for line-based edits where you know the exact line numbers to replace. The file must exist in your file collection and must be a text-type file (text, markdown, html, csv, etc.). After modification, the file is re-uploaded and the collection entry is updated.",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
file: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "The file to modify: 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."
|
|
23
|
+
},
|
|
24
|
+
startLine: {
|
|
25
|
+
type: "number",
|
|
26
|
+
description: "Starting line number (1-indexed) to replace. The line range is inclusive (both startLine and endLine are replaced)."
|
|
27
|
+
},
|
|
28
|
+
endLine: {
|
|
29
|
+
type: "number",
|
|
30
|
+
description: "Ending line number (1-indexed) to replace. Must be >= startLine. The line range is inclusive (both startLine and endLine are replaced)."
|
|
31
|
+
},
|
|
32
|
+
content: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "New content to replace the specified line range. This will replace lines startLine through endLine (inclusive)."
|
|
35
|
+
},
|
|
36
|
+
userMessage: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ["file", "startLine", "endLine", "content", "userMessage"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "function",
|
|
47
|
+
icon: "🔍",
|
|
48
|
+
function: {
|
|
49
|
+
name: "EditFileBySearchAndReplace",
|
|
50
|
+
description: "Search and replace exact string matches in a file. Use this when you know the exact text to find and replace. The file must exist in your file collection and must be a text-type file (text, markdown, html, csv, etc.). After modification, the old file version is deleted from cloud storage and the new version is uploaded. The collection entry is updated with the new URL and hash.",
|
|
51
|
+
parameters: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
file: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "The file to modify: 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."
|
|
57
|
+
},
|
|
58
|
+
oldString: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Exact string to replace. Must match the exact text in the file (including whitespace and newlines). The search is case-sensitive and must match exactly."
|
|
61
|
+
},
|
|
62
|
+
newString: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "New content to replace oldString with."
|
|
65
|
+
},
|
|
66
|
+
replaceAll: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description: "Optional: If true, replace all occurrences of oldString. Default: false (replace only first occurrence)."
|
|
69
|
+
},
|
|
70
|
+
userMessage: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "A user-friendly message that describes what you're doing with this tool"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
required: ["file", "oldString", "newString", "userMessage"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
executePathway: async ({args, runAllPrompts, resolver}) => {
|
|
82
|
+
const { file, startLine, endLine, content, oldString, newString, replaceAll = false, contextId, contextKey } = args;
|
|
83
|
+
|
|
84
|
+
// Determine which tool was called based on parameters
|
|
85
|
+
const isSearchReplace = oldString !== undefined && newString !== undefined;
|
|
86
|
+
const isEditByLine = startLine !== undefined && endLine !== undefined && content !== undefined;
|
|
87
|
+
const toolName = isSearchReplace ? "EditFileBySearchAndReplace" : "EditFileByLine";
|
|
88
|
+
|
|
89
|
+
// Validate basic inputs
|
|
90
|
+
if (!file || typeof file !== 'string') {
|
|
91
|
+
const errorResult = {
|
|
92
|
+
success: false,
|
|
93
|
+
error: "file parameter is required and must be a string"
|
|
94
|
+
};
|
|
95
|
+
resolver.tool = JSON.stringify({ toolUsed: toolName });
|
|
96
|
+
return JSON.stringify(errorResult);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!contextId) {
|
|
100
|
+
const errorResult = {
|
|
101
|
+
success: false,
|
|
102
|
+
error: "contextId is required for file modification"
|
|
103
|
+
};
|
|
104
|
+
resolver.tool = JSON.stringify({ toolUsed: toolName });
|
|
105
|
+
return JSON.stringify(errorResult);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate that we have the right parameters for the tool being used
|
|
109
|
+
if (!isSearchReplace && !isEditByLine) {
|
|
110
|
+
const errorResult = {
|
|
111
|
+
success: false,
|
|
112
|
+
error: "Either use EditFileByLine (with startLine/endLine/content) or EditFileBySearchAndReplace (with oldString/newString)"
|
|
113
|
+
};
|
|
114
|
+
resolver.tool = JSON.stringify({ toolUsed: toolName });
|
|
115
|
+
return JSON.stringify(errorResult);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Validate EditFileByLine parameters
|
|
119
|
+
if (isEditByLine) {
|
|
120
|
+
if (typeof startLine !== 'number' || startLine < 1) {
|
|
121
|
+
const errorResult = {
|
|
122
|
+
success: false,
|
|
123
|
+
error: "startLine must be a positive integer (1-indexed)"
|
|
124
|
+
};
|
|
125
|
+
resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
|
|
126
|
+
return JSON.stringify(errorResult);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (typeof endLine !== 'number' || endLine < 1) {
|
|
130
|
+
const errorResult = {
|
|
131
|
+
success: false,
|
|
132
|
+
error: "endLine must be a positive integer (1-indexed)"
|
|
133
|
+
};
|
|
134
|
+
resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
|
|
135
|
+
return JSON.stringify(errorResult);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (endLine < startLine) {
|
|
139
|
+
const errorResult = {
|
|
140
|
+
success: false,
|
|
141
|
+
error: "endLine must be >= startLine"
|
|
142
|
+
};
|
|
143
|
+
resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
|
|
144
|
+
return JSON.stringify(errorResult);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof content !== 'string') {
|
|
148
|
+
const errorResult = {
|
|
149
|
+
success: false,
|
|
150
|
+
error: "content is required and must be a string"
|
|
151
|
+
};
|
|
152
|
+
resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
|
|
153
|
+
return JSON.stringify(errorResult);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Validate EditFileBySearchAndReplace parameters
|
|
158
|
+
if (isSearchReplace) {
|
|
159
|
+
if (typeof oldString !== 'string') {
|
|
160
|
+
const errorResult = {
|
|
161
|
+
success: false,
|
|
162
|
+
error: "oldString is required and must be a string"
|
|
163
|
+
};
|
|
164
|
+
resolver.tool = JSON.stringify({ toolUsed: "EditFileBySearchAndReplace" });
|
|
165
|
+
return JSON.stringify(errorResult);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof newString !== 'string') {
|
|
169
|
+
const errorResult = {
|
|
170
|
+
success: false,
|
|
171
|
+
error: "newString is required and must be a string"
|
|
172
|
+
};
|
|
173
|
+
resolver.tool = JSON.stringify({ toolUsed: "EditFileBySearchAndReplace" });
|
|
174
|
+
return JSON.stringify(errorResult);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
// Resolve the file parameter to a URL using the common utility
|
|
180
|
+
const fileUrl = await resolveFileParameter(file, contextId, contextKey);
|
|
181
|
+
|
|
182
|
+
if (!fileUrl) {
|
|
183
|
+
const errorResult = {
|
|
184
|
+
success: false,
|
|
185
|
+
error: `File not found: "${file}". Use ListFileCollection or SearchFileCollection to find available files.`
|
|
186
|
+
};
|
|
187
|
+
resolver.tool = JSON.stringify({ toolUsed: toolName });
|
|
188
|
+
return JSON.stringify(errorResult);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Find the file in the collection to get metadata (for updating later)
|
|
192
|
+
// We'll load it again inside the lock, but need to verify it exists first
|
|
193
|
+
const collection = await loadFileCollection(contextId, contextKey, true);
|
|
194
|
+
const foundFile = findFileInCollection(file, collection);
|
|
195
|
+
|
|
196
|
+
if (!foundFile) {
|
|
197
|
+
const errorResult = {
|
|
198
|
+
success: false,
|
|
199
|
+
error: `File not found in collection: "${file}"`
|
|
200
|
+
};
|
|
201
|
+
resolver.tool = JSON.stringify({ toolUsed: toolName });
|
|
202
|
+
return JSON.stringify(errorResult);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Store the file ID for updating inside the lock
|
|
206
|
+
const fileIdToUpdate = foundFile.id;
|
|
207
|
+
|
|
208
|
+
// Download the current file content
|
|
209
|
+
logger.info(`Downloading file for modification: ${fileUrl}`);
|
|
210
|
+
const downloadResponse = await axios.get(fileUrl, {
|
|
211
|
+
responseType: 'arraybuffer',
|
|
212
|
+
timeout: 60000,
|
|
213
|
+
validateStatus: (status) => status >= 200 && status < 400
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (downloadResponse.status !== 200 || !downloadResponse.data) {
|
|
217
|
+
throw new Error(`Failed to download file: ${downloadResponse.status}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Explicitly decode as UTF-8 to prevent mojibake (encoding corruption)
|
|
221
|
+
const originalContent = Buffer.from(downloadResponse.data).toString('utf8');
|
|
222
|
+
let modifiedContent;
|
|
223
|
+
let modificationInfo = {};
|
|
224
|
+
|
|
225
|
+
if (isEditByLine) {
|
|
226
|
+
// Line-based replacement mode
|
|
227
|
+
const allLines = originalContent.split(/\r?\n/);
|
|
228
|
+
const totalLines = allLines.length;
|
|
229
|
+
|
|
230
|
+
// Validate line range
|
|
231
|
+
if (startLine > totalLines) {
|
|
232
|
+
const errorResult = {
|
|
233
|
+
success: false,
|
|
234
|
+
error: `startLine (${startLine}) exceeds file length (${totalLines} lines)`
|
|
235
|
+
};
|
|
236
|
+
resolver.tool = JSON.stringify({ toolUsed: "EditFileByLine" });
|
|
237
|
+
return JSON.stringify(errorResult);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Perform the line replacement
|
|
241
|
+
const startIndex = startLine - 1;
|
|
242
|
+
const endIndex = Math.min(endLine, totalLines);
|
|
243
|
+
|
|
244
|
+
// Split the replacement content into lines
|
|
245
|
+
const replacementLines = content.split(/\r?\n/);
|
|
246
|
+
|
|
247
|
+
// Build the modified content
|
|
248
|
+
const beforeLines = allLines.slice(0, startIndex);
|
|
249
|
+
const afterLines = allLines.slice(endIndex);
|
|
250
|
+
const modifiedLines = [...beforeLines, ...replacementLines, ...afterLines];
|
|
251
|
+
modifiedContent = modifiedLines.join('\n');
|
|
252
|
+
|
|
253
|
+
modificationInfo = {
|
|
254
|
+
mode: 'line-based',
|
|
255
|
+
originalLines: totalLines,
|
|
256
|
+
modifiedLines: modifiedLines.length,
|
|
257
|
+
replacedLines: endLine - startLine + 1,
|
|
258
|
+
insertedLines: replacementLines.length,
|
|
259
|
+
startLine: startLine,
|
|
260
|
+
endLine: endLine
|
|
261
|
+
};
|
|
262
|
+
} else if (isSearchReplace) {
|
|
263
|
+
// Search and replace mode
|
|
264
|
+
if (!originalContent.includes(oldString)) {
|
|
265
|
+
const errorResult = {
|
|
266
|
+
success: false,
|
|
267
|
+
error: `oldString not found in file. The exact string must match (including whitespace and newlines).`
|
|
268
|
+
};
|
|
269
|
+
resolver.tool = JSON.stringify({ toolUsed: "EditFileBySearchAndReplace" });
|
|
270
|
+
return JSON.stringify(errorResult);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Count occurrences
|
|
274
|
+
const occurrences = (originalContent.match(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
|
275
|
+
|
|
276
|
+
if (replaceAll) {
|
|
277
|
+
modifiedContent = originalContent.split(oldString).join(newString);
|
|
278
|
+
modificationInfo = {
|
|
279
|
+
mode: 'string-based',
|
|
280
|
+
replaceAll: true,
|
|
281
|
+
occurrencesReplaced: occurrences
|
|
282
|
+
};
|
|
283
|
+
} else {
|
|
284
|
+
// Replace only first occurrence
|
|
285
|
+
modifiedContent = originalContent.replace(oldString, newString);
|
|
286
|
+
modificationInfo = {
|
|
287
|
+
mode: 'string-based',
|
|
288
|
+
replaceAll: false,
|
|
289
|
+
occurrencesReplaced: 1,
|
|
290
|
+
totalOccurrences: occurrences
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Determine MIME type from filename using utility function
|
|
296
|
+
const filename = foundFile.filename || 'modified.txt';
|
|
297
|
+
let mimeType = getMimeTypeFromFilename(filename, 'text/plain');
|
|
298
|
+
|
|
299
|
+
// Add charset=utf-8 for text-based MIME types
|
|
300
|
+
if (mimeType.startsWith('text/') || mimeType === 'application/json' ||
|
|
301
|
+
mimeType === 'application/javascript' || mimeType === 'application/typescript' ||
|
|
302
|
+
mimeType === 'application/xml') {
|
|
303
|
+
mimeType = `${mimeType}; charset=utf-8`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Delete the old file version before uploading the new one
|
|
307
|
+
const oldHash = foundFile.hash;
|
|
308
|
+
if (oldHash) {
|
|
309
|
+
logger.info(`Deleting old file version with hash ${oldHash} before uploading new version`);
|
|
310
|
+
const deleted = await deleteFileByHash(oldHash, resolver);
|
|
311
|
+
if (deleted) {
|
|
312
|
+
logger.info(`Successfully deleted old file version`);
|
|
313
|
+
} else {
|
|
314
|
+
logger.info(`Old file version not found or already deleted (hash: ${oldHash})`);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
logger.info(`No hash found for old file, skipping deletion`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Upload the modified file
|
|
321
|
+
const fileBuffer = Buffer.from(modifiedContent, 'utf8');
|
|
322
|
+
const uploadResult = await uploadFileToCloud(
|
|
323
|
+
fileBuffer,
|
|
324
|
+
mimeType,
|
|
325
|
+
filename,
|
|
326
|
+
resolver
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
if (!uploadResult || !uploadResult.url) {
|
|
330
|
+
throw new Error('Failed to upload modified file to cloud storage');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Update the file collection entry with new URL and hash using optimistic locking
|
|
334
|
+
const updatedCollection = await modifyFileCollectionWithLock(contextId, contextKey, (collection) => {
|
|
335
|
+
const fileToUpdate = collection.find(f => f.id === fileIdToUpdate);
|
|
336
|
+
if (!fileToUpdate) {
|
|
337
|
+
throw new Error(`File with ID "${fileIdToUpdate}" not found in collection during update`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fileToUpdate.url = uploadResult.url;
|
|
341
|
+
if (uploadResult.gcs) {
|
|
342
|
+
fileToUpdate.gcs = uploadResult.gcs;
|
|
343
|
+
}
|
|
344
|
+
if (uploadResult.hash) {
|
|
345
|
+
fileToUpdate.hash = uploadResult.hash;
|
|
346
|
+
}
|
|
347
|
+
fileToUpdate.lastAccessed = new Date().toISOString();
|
|
348
|
+
|
|
349
|
+
return collection;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Get the updated file info for the result
|
|
353
|
+
const updatedFile = updatedCollection.find(f => f.id === fileIdToUpdate);
|
|
354
|
+
|
|
355
|
+
// Build result message
|
|
356
|
+
let message;
|
|
357
|
+
if (isEditByLine) {
|
|
358
|
+
message = `File "${filename}" modified successfully. Replaced lines ${startLine}-${endLine} (${endLine - startLine + 1} lines) with ${modificationInfo.insertedLines} line(s).`;
|
|
359
|
+
} else if (isSearchReplace) {
|
|
360
|
+
if (replaceAll) {
|
|
361
|
+
message = `File "${filename}" modified successfully. Replaced all ${modificationInfo.occurrencesReplaced} occurrence(s) of the specified string.`;
|
|
362
|
+
} else {
|
|
363
|
+
message = `File "${filename}" modified successfully. Replaced first occurrence of the specified string${modificationInfo.totalOccurrences > 1 ? ` (${modificationInfo.totalOccurrences} total occurrences found)` : ''}.`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const result = {
|
|
368
|
+
success: true,
|
|
369
|
+
filename: filename,
|
|
370
|
+
fileId: updatedFile.id,
|
|
371
|
+
url: uploadResult.url,
|
|
372
|
+
gcs: uploadResult.gcs || null,
|
|
373
|
+
hash: uploadResult.hash || null,
|
|
374
|
+
...modificationInfo,
|
|
375
|
+
message: message
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
resolver.tool = JSON.stringify({ toolUsed: toolName });
|
|
379
|
+
return JSON.stringify(result);
|
|
380
|
+
|
|
381
|
+
} catch (error) {
|
|
382
|
+
let errorMsg;
|
|
383
|
+
if (error?.message) {
|
|
384
|
+
errorMsg = error.message;
|
|
385
|
+
} else if (error?.errors && Array.isArray(error.errors)) {
|
|
386
|
+
// Handle AggregateError
|
|
387
|
+
errorMsg = error.errors.map(e => e?.message || String(e)).join('; ');
|
|
388
|
+
} else {
|
|
389
|
+
errorMsg = String(error);
|
|
390
|
+
}
|
|
391
|
+
logger.error(`Error modifying file: ${errorMsg}`);
|
|
392
|
+
|
|
393
|
+
const errorResult = {
|
|
394
|
+
success: false,
|
|
395
|
+
error: errorMsg
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
resolver.tool = JSON.stringify({ toolUsed: toolName });
|
|
399
|
+
return JSON.stringify(errorResult);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|