From 656dbe6dc64013215eb312173df398fe4606d788 Mon Sep 17 00:00:00 2001 From: "A.J. Shulman" Date: Tue, 27 May 2025 14:08:11 -0400 Subject: feat: implement dynamic tool creation with deferred webpack rebuild and AI integration Added runtime tool registry to Agent.ts for dynamic tool lookup Implemented CreateNewTool agent tool for AI-driven code analysis and tool generation Enabled deferred saving to avoid interrupting AI workflows with immediate rebuilds Introduced user-controlled modal for confirming tool installation and page reload Added REST API and secure server-side persistence for dynamic tools Built TypeScript validation, transpilation, and sandboxed execution for safe tool handling UI enhancements: modal with blur, responsive design, clear messaging Ensured compatibility with Webpack using dynamic require() calls Full error handling, code validation, and secure storage on client and server sides --- .../views/nodes/chatbot/agentsystem/Agent.ts | 252 +++++++++++++++++++-- 1 file changed, 235 insertions(+), 17 deletions(-) (limited to 'src/client/views/nodes/chatbot/agentsystem') diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index 1a9df1a75..c3d37fd0e 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -29,6 +29,7 @@ import { CreateLinksTool } from '../tools/CreateLinksTool'; import { CodebaseSummarySearchTool } from '../tools/CodebaseSummarySearchTool'; import { FileContentTool } from '../tools/FileContentTool'; import { FileNamesTool } from '../tools/FileNamesTool'; +import { CreateNewTool } from '../tools/CreateNewTool'; //import { CreateTextDocTool } from '../tools/CreateTextDocumentTool'; dotenv.config(); @@ -52,6 +53,12 @@ export class Agent { private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser(); private tools: Record>>; private _docManager: AgentDocumentManager; + // Dynamic tool registry for tools created at runtime + private dynamicToolRegistry: Map>> = new Map(); + // Callback for notifying when tools are created and need reload + private onToolCreatedCallback?: (toolName: string) => void; + // Storage for deferred tool saving + private pendingToolSave?: { toolName: string; completeToolCode: string }; /** * The constructor initializes the agent with the vector store and toolset, and sets up the OpenAI client. @@ -79,6 +86,9 @@ export class Agent { this._csvData = csvData; this._docManager = docManager; + // Initialize dynamic tool registry + this.dynamicToolRegistry = new Map(); + // Define available tools for the assistant this.tools = { calculate: new CalculateTool(), @@ -94,6 +104,146 @@ export class Agent { fileContent: new FileContentTool(this.vectorstore), fileNames: new FileNamesTool(this.vectorstore), }; + + // Add the createNewTool after other tools are defined + this.tools.createNewTool = new CreateNewTool(this.dynamicToolRegistry, this.tools, this); + + // Load existing dynamic tools + this.loadExistingDynamicTools(); + } + + /** + * Loads existing dynamic tools by checking the current registry and ensuring all stored tools are available + */ + private async loadExistingDynamicTools(): Promise { + try { + console.log('Loading dynamic tools...'); + + // Since we're in a browser environment, we can't use filesystem operations + // Instead, we'll maintain tools in the registry and try to load known tools + + // Try to manually load the known dynamic tools that exist + const knownDynamicTools = [ + { name: 'CharacterCountTool', actionName: 'charactercount' }, + { name: 'WordCountTool', actionName: 'wordcount' }, + { name: 'TestTool', actionName: 'test' }, + ]; + + let loadedCount = 0; + for (const toolInfo of knownDynamicTools) { + try { + // Check if tool is already in registry + if (this.dynamicToolRegistry.has(toolInfo.actionName)) { + console.log(`✓ Tool ${toolInfo.actionName} already loaded`); + loadedCount++; + continue; + } + + // Try to load the tool using require (works better in webpack environment) + let toolInstance = null; + try { + // Use require with the relative path + const toolModule = require(`../tools/dynamic/${toolInfo.name}`); + const ToolClass = toolModule[toolInfo.name]; + + if (ToolClass && typeof ToolClass === 'function') { + toolInstance = new ToolClass(); + + if (toolInstance instanceof BaseTool) { + this.dynamicToolRegistry.set(toolInfo.actionName, toolInstance); + loadedCount++; + console.log(`✓ Loaded dynamic tool: ${toolInfo.actionName} (from ${toolInfo.name})`); + } + } + } catch (requireError) { + // Tool file doesn't exist or can't be loaded, which is fine + console.log(`Tool ${toolInfo.name} not available:`, (requireError as Error).message); + } + } catch (error) { + console.warn(`⚠ Failed to load ${toolInfo.name}:`, error); + } + } + + console.log(`Successfully loaded ${loadedCount} dynamic tools`); + + // Log all currently registered dynamic tools + if (this.dynamicToolRegistry.size > 0) { + console.log('Currently registered dynamic tools:', Array.from(this.dynamicToolRegistry.keys())); + } + } catch (error) { + console.error('Error loading dynamic tools:', error); + } + } + + /** + * Manually registers a dynamic tool instance (called by CreateNewTool) + */ + public registerDynamicTool(toolName: string, toolInstance: BaseTool>): void { + this.dynamicToolRegistry.set(toolName, toolInstance); + console.log(`Manually registered dynamic tool: ${toolName}`); + } + + /** + * Notifies that a tool has been created and saved to disk (called by CreateNewTool) + */ + public notifyToolCreated(toolName: string, completeToolCode: string): void { + // Store the tool data for deferred saving + this.pendingToolSave = { toolName, completeToolCode }; + + if (this.onToolCreatedCallback) { + this.onToolCreatedCallback(toolName); + } + } + + /** + * Performs the deferred tool save operation (called after user confirmation) + */ + public async performDeferredToolSave(): Promise { + if (!this.pendingToolSave) { + console.warn('No pending tool save operation'); + return false; + } + + const { toolName, completeToolCode } = this.pendingToolSave; + + try { + // Get the CreateNewTool instance to perform the save + const createNewTool = this.tools.createNewTool as any; + if (createNewTool && typeof createNewTool.saveToolToServerDeferred === 'function') { + const success = await createNewTool.saveToolToServerDeferred(toolName, completeToolCode); + + if (success) { + console.log(`Tool ${toolName} saved to server successfully via deferred save.`); + // Clear the pending save + this.pendingToolSave = undefined; + return true; + } else { + console.warn(`Tool ${toolName} could not be saved to server via deferred save.`); + return false; + } + } else { + console.error('CreateNewTool instance not available for deferred save'); + return false; + } + } catch (error) { + console.error(`Error performing deferred tool save for ${toolName}:`, error); + return false; + } + } + + /** + * Sets the callback for when tools are created + */ + public setToolCreatedCallback(callback: (toolName: string) => void): void { + this.onToolCreatedCallback = callback; + } + + /** + * Public method to reload dynamic tools (called when new tools are created) + */ + public reloadDynamicTools(): void { + console.log('Reloading dynamic tools...'); + this.loadExistingDynamicTools(); } /** @@ -126,11 +276,8 @@ export class Agent { // Push sanitized user's question to message history this.messages.push({ role: 'user', content: sanitizedQuestion }); - // Retrieve chat history and generate system prompt - const chatHistory = this._history(); - // Get document summaries directly from document manager - // Generate the system prompt with the summaries - const systemPrompt = getReactPrompt(Object.values(this.tools), () => JSON.stringify(this._docManager.listDocs), chatHistory); + // Get system prompt with all tools (static + dynamic) + const systemPrompt = this.getSystemPromptWithAllTools(); // Initialize intermediate messages this.interMessages = [{ role: 'system', content: systemPrompt }]; @@ -193,22 +340,25 @@ export class Agent { currentAction = stage[key] as string; console.log(`Action: ${currentAction}`); - if (this.tools[currentAction]) { + // Check both static tools and dynamic registry + const tool = this.tools[currentAction] || this.dynamicToolRegistry.get(currentAction); + + if (tool) { // Prepare the next action based on the current tool const nextPrompt = [ { type: 'text', - text: `` + builder.build({ action_rules: this.tools[currentAction].getActionRule() }) + ``, + text: `` + builder.build({ action_rules: tool.getActionRule() }) + ``, } as Observation, ]; this.interMessages.push({ role: 'user', content: nextPrompt }); break; } else { // Handle error in case of an invalid action - console.log('Error: No valid action'); + console.log(`Error: Action "${currentAction}" is not a valid tool`); this.interMessages.push({ role: 'user', - content: `No valid action, try again.`, + content: `Action "${currentAction}" is not a valid tool, try again.`, }); break; } @@ -376,8 +526,8 @@ export class Agent { throw new Error('Action must be a non-empty string'); } - // Optional: Check if the action is among allowed actions - const allowedActions = Object.keys(this.tools); + // Optional: Check if the action is among allowed actions (including dynamic tools) + const allowedActions = [...Object.keys(this.tools), ...Array.from(this.dynamicToolRegistry.keys())]; if (!allowedActions.includes(stage.action)) { throw new Error(`Action "${stage.action}" is not a valid tool`); } @@ -482,12 +632,15 @@ export class Agent { * @throws An error if the action is unknown, if required parameters are missing, or if input types don't match the expected parameter types. */ private async processAction(action: string, actionInput: ParametersType>): Promise { - // Check if the action exists in the tools list - if (!(action in this.tools)) { + // Check if the action exists in the tools list or dynamic registry + if (!(action in this.tools) && !this.dynamicToolRegistry.has(action)) { throw new Error(`Unknown action: ${action}`); } console.log(actionInput); + // Determine which tool to use - either from static tools or dynamic registry + const tool = this.tools[action] || this.dynamicToolRegistry.get(action); + // Special handling for documentMetadata tool with numeric or boolean fieldValue if (action === 'documentMetadata') { // Handle single field edit @@ -520,9 +673,49 @@ export class Agent { } } - for (const param of this.tools[action].parameterRules) { + // Special handling for createNewTool with parsed XML toolCode + if (action === 'createNewTool') { + if ('toolCode' in actionInput && typeof actionInput.toolCode === 'object' && actionInput.toolCode !== null) { + try { + // Convert the parsed XML object back to a string + const extractText = (obj: any): string => { + if (typeof obj === 'string') { + return obj; + } else if (obj && typeof obj === 'object') { + if (obj._text) { + return obj._text; + } + // Recursively extract text from all properties + let text = ''; + for (const key in obj) { + if (key !== '_text') { + const value = obj[key]; + if (typeof value === 'string') { + text += value + '\n'; + } else if (value && typeof value === 'object') { + text += extractText(value) + '\n'; + } + } + } + return text; + } + return ''; + }; + + const reconstructedCode = extractText(actionInput.toolCode); + actionInput.toolCode = reconstructedCode; + } catch (error) { + console.error('Error processing toolCode:', error); + // Convert to string as fallback + actionInput.toolCode = String(actionInput.toolCode); + } + } + } + + // Check parameter requirements and types for the tool + for (const param of tool.parameterRules) { // Check if the parameter is required and missing in the input - if (param.required && !(param.name in actionInput) && !this.tools[action].inputValidator(actionInput)) { + if (param.required && !(param.name in actionInput) && !tool.inputValidator(actionInput)) { throw new Error(`Missing required parameter: ${param.name}`); } @@ -540,11 +733,30 @@ export class Agent { } } - const tool = this.tools[action]; - + // Execute the tool with the validated inputs return await tool.execute(actionInput); } + /** + * Gets a combined list of all tools, both static and dynamic + * @returns An array of all available tool instances + */ + private getAllTools(): BaseTool>[] { + // Combine static and dynamic tools + return [...Object.values(this.tools), ...Array.from(this.dynamicToolRegistry.values())]; + } + + /** + * Overridden method to get the React prompt with all tools (static + dynamic) + */ + private getSystemPromptWithAllTools(): string { + const allTools = this.getAllTools(); + const docSummaries = () => JSON.stringify(this._docManager.listDocs); + const chatHistory = this._history(); + + return getReactPrompt(allTools, docSummaries, chatHistory); + } + /** * Reinitializes the DocumentMetadataTool with a direct reference to the ChatBox instance. * This ensures that the tool can properly access the ChatBox document and find related documents. @@ -560,3 +772,9 @@ export class Agent { } } } + +// Forward declaration to avoid circular import +interface AgentLike { + registerDynamicTool(toolName: string, toolInstance: BaseTool>): void; + notifyToolCreated(toolName: string, completeToolCode: string): void; +} -- cgit v1.2.3-70-g09d2