aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/chatbot/agentsystem
diff options
context:
space:
mode:
authorA.J. Shulman <Shulman.aj@gmail.com>2025-05-27 14:08:11 -0400
committerA.J. Shulman <Shulman.aj@gmail.com>2025-05-27 14:08:11 -0400
commit656dbe6dc64013215eb312173df398fe4606d788 (patch)
tree05c2d35e5f636091c637779d1c8352c25e9ce7f6 /src/client/views/nodes/chatbot/agentsystem
parentc3dba47bcda10bbcd72010c177afa8fd301e87e1 (diff)
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
Diffstat (limited to 'src/client/views/nodes/chatbot/agentsystem')
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts252
1 files changed, 235 insertions, 17 deletions
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<string, BaseTool<ReadonlyArray<Parameter>>>;
private _docManager: AgentDocumentManager;
+ // Dynamic tool registry for tools created at runtime
+ private dynamicToolRegistry: Map<string, BaseTool<ReadonlyArray<Parameter>>> = 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<void> {
+ 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<ReadonlyArray<Parameter>>): 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<boolean> {
+ 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: `<stage number="${i + 1}" role="user">` + builder.build({ action_rules: this.tools[currentAction].getActionRule() }) + `</stage>`,
+ text: `<stage number="${i + 1}" role="user">` + builder.build({ action_rules: tool.getActionRule() }) + `</stage>`,
} 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: `<stage number="${i + 1}" role="system-error-reporter">No valid action, try again.</stage>`,
+ content: `<stage number="${i + 1}" role="system-error-reporter">Action "${currentAction}" is not a valid tool, try again.</stage>`,
});
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<ReadonlyArray<Parameter>>): Promise<Observation[]> {
- // 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,12 +733,31 @@ 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<ReadonlyArray<Parameter>>[] {
+ // 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<ReadonlyArray<Parameter>>): void;
+ notifyToolCreated(toolName: string, completeToolCode: string): void;
+}