aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/chatbot/agentsystem
diff options
context:
space:
mode:
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;
+}