import dotenv from 'dotenv'; import { XMLBuilder, XMLParser } from 'fast-xml-parser'; import { escape } from 'lodash'; // Imported escape from lodash import OpenAI from 'openai'; import { DocumentOptions } from '../../../../documents/Documents'; import { AnswerParser } from '../response_parsers/AnswerParser'; import { StreamedAnswerParser } from '../response_parsers/StreamedAnswerParser'; import { BaseTool } from '../tools/BaseTool'; import { CalculateTool } from '../tools/CalculateTool'; //import { CreateAnyDocumentTool } from '../tools/CreateAnyDocTool'; import { DataAnalysisTool } from '../tools/DataAnalysisTool'; import { DocumentMetadataTool } from '../tools/DocumentMetadataTool'; import { ImageCreationTool } from '../tools/ImageCreationTool'; import { NoTool } from '../tools/NoTool'; import { SearchTool } from '../tools/SearchTool'; import { Parameter, ParametersType, TypeMap } from '../types/tool_types'; import { AgentMessage, ASSISTANT_ROLE, AssistantMessage, Observation, PROCESSING_TYPE, ProcessingInfo, TEXT_TYPE } from '../types/types'; import { Vectorstore } from '../vectorstore/Vectorstore'; import { getReactPrompt } from './prompts'; //import { DictionaryTool } from '../tools/DictionaryTool'; import { ChatCompletionMessageParam } from 'openai/resources'; import { Upload } from '../../../../../server/SharedMediaTypes'; import { DocumentView } from '../../DocumentView'; import { CodebaseSummarySearchTool } from '../tools/CodebaseSummarySearchTool'; import { CreateLinksTool } from '../tools/CreateLinksTool'; import { CreateNewTool } from '../tools/CreateNewTool'; import { FileContentTool } from '../tools/FileContentTool'; import { FileNamesTool } from '../tools/FileNamesTool'; import { RAGTool } from '../tools/RAGTool'; import { SortDocsTool } from '../tools/SortDocsTool'; import { TagDocsTool } from '../tools/TagDocsTool'; import { TakeQuizTool } from '../tools/TakeQuizTool'; import { GPTTutorialTool } from '../tools/TutorialTool'; import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool'; import { AgentDocumentManager } from '../utils/AgentDocumentManager'; import { FilterDocsTool } from '../tools/FilterDocsTool'; import { CanvasDocsTool } from '../tools/CanvasDocsTool'; import { UIControlTool } from '../tools/UIControlTool'; dotenv.config(); /** * The Agent class handles the interaction between the assistant and the tools available, * processes user queries, and manages the communication flow between the tools and OpenAI. */ export class Agent { // Private properties private client: OpenAI; private messages: AgentMessage[] = []; private interMessages: AgentMessage[] = []; private vectorstore: Vectorstore; private _history: () => string; private _csvData: () => { filename: string; id: string; text: string }[]; private actionNumber: number = 0; private thoughtNumber: number = 0; private processingNumber: number = 0; private processingInfo: ProcessingInfo[] = []; private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser(); private tools: Record>>; private _docManager: AgentDocumentManager; private is_dash_doc_assistant: boolean; private parentView: DocumentView; // 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. * @param _vectorstore Vector store instance for document storage and retrieval. * @param summaries A function to retrieve document summaries (deprecated, now using docManager directly). * @param history A function to retrieve chat history. * @param csvData A function to retrieve CSV data linked to the assistant. * @param getLinkedUrlDocId A function to get document IDs from URLs. * @param createImage A function to create images in the dashboard. * @param createCSVInDash A function to create a CSV document in the dashboard. * @param docManager The document manager instance. */ constructor( _vectorstore: Vectorstore, history: () => string, csvData: () => { filename: string; id: string; text: string }[], createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void, createCSVInDash: (url: string, title: string, id: string, data: string) => void, docManager: AgentDocumentManager ) { // Initialize OpenAI client with API key from environment this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true }); this.vectorstore = _vectorstore; this.parentView = docManager.parentViewDocument; // Get the parent DocumentView this._history = history; this._csvData = csvData; this._docManager = docManager; this.is_dash_doc_assistant = true; // Initialize to default value // Initialize dynamic tool registry this.dynamicToolRegistry = new Map(); // Define available tools for the assistant this.tools = { calculate: new CalculateTool(), rag: new RAGTool(this.vectorstore), dataAnalysis: new DataAnalysisTool(csvData), websiteInfoScraper: new WebsiteInfoScraperTool(this._docManager), searchTool: new SearchTool(this._docManager), noTool: new NoTool(), imageCreationTool: new ImageCreationTool(createImage), documentMetadata: new DocumentMetadataTool(this._docManager), createLinks: new CreateLinksTool(this._docManager), codebaseSummarySearch: new CodebaseSummarySearchTool(this.vectorstore), fileContent: new FileContentTool(this.vectorstore), fileNames: new FileNamesTool(this.vectorstore), generateTutorialNode: new GPTTutorialTool(this._docManager), sortDocs: new SortDocsTool(this._docManager, this.parentView), tagDocs: new TagDocsTool(this._docManager, this.parentView), filterDocs: new FilterDocsTool(this._docManager, this.parentView), takeQuiz: new TakeQuizTool(this._docManager), canvasDocs: new CanvasDocsTool(), uiControl: new UIControlTool(), }; // 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 every dynamic tool that the server reports via /getDynamicTools. * • Uses dynamic `import()` so webpack/vite will code-split each tool automatically. * • Registers the tool in `dynamicToolRegistry` under the name it advertises via * `toolInfo.name`; also registers the legacy camel-case key if different. */ private async loadExistingDynamicTools(): Promise { try { console.log('Loading dynamic tools from server…'); const toolFiles = await this.fetchDynamicToolList(); let loaded = 0; for (const { name: className, path } of toolFiles) { // Legacy key (e.g., CharacterCountTool → characterCountTool) const legacyKey = className.replace(/^[A-Z]/, m => m.toLowerCase()); // Skip if we already have the legacy key if (this.dynamicToolRegistry.has(legacyKey)) continue; try { // @vite-ignore keeps Vite/Webpack from trying to statically analyse the variable part const ToolClass = require(`../tools/${path}`)[className]; if (!ToolClass || !(ToolClass.prototype instanceof BaseTool)) { console.warn(`File ${path} does not export a valid BaseTool subclass`); continue; } const instance: BaseTool> = new ToolClass(); // Prefer the tool’s self-declared name (matches tag) const key = (instance.name || '').trim() || legacyKey; // Check for duplicates if (this.dynamicToolRegistry.has(key)) { console.warn(`Dynamic tool key '${key}' already registered – keeping existing instance`); continue; } // ✅ register under the preferred key this.dynamicToolRegistry.set(key, instance); // optional: also register the legacy key for safety if (key !== legacyKey && !this.dynamicToolRegistry.has(legacyKey)) { this.dynamicToolRegistry.set(legacyKey, instance); } loaded++; console.info(`✓ Loaded dynamic tool '${key}' from '${path}'`); } catch (err) { console.error(`✗ Failed to load '${path}':`, err); } } console.log(`Dynamic-tool load complete – ${loaded}/${toolFiles.length} added`); } catch (err) { console.error('Dynamic-tool bootstrap failed:', err); } } /** * 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(); } private async fetchDynamicToolList(): Promise<{ name: string; path: string }[]> { const res = await fetch('/getDynamicTools'); if (!res.ok) throw new Error(`Failed to fetch dynamic tool list – ${res.statusText}`); const json = await res.json(); console.log('Dynamic tools fetched:', json.tools); return json.tools ?? []; } /** * This method handles the conversation flow with the assistant, processes user queries, * and manages the assistant's decision-making process, including tool actions. * @param question The user's question. * @param onProcessingUpdate Callback function for processing updates. * @param onAnswerUpdate Callback function for answer updates. * @param maxTurns The maximum number of turns to allow in the conversation. * @returns The final response from the assistant. */ async askAgent(question: string, onProcessingUpdate: (processingUpdate: ProcessingInfo[]) => void, onAnswerUpdate: (answerUpdate: string) => void, maxTurns: number = 50): Promise { console.log(`Starting query: ${question}`); const MAX_QUERY_LENGTH = 1000; // adjust the limit as needed // Check if the question exceeds the maximum length if (question.length > MAX_QUERY_LENGTH) { const errorText = `Your query is too long (${question.length} characters). Please shorten it to ${MAX_QUERY_LENGTH} characters or less and try again.`; console.warn(errorText); // Log the specific reason return { role: ASSISTANT_ROLE.ASSISTANT, // Use ERROR type for clarity in the UI if handled differently content: [{ text: errorText, index: 0, type: TEXT_TYPE.ERROR, citation_ids: null }], processing_info: [], }; } const sanitizedQuestion = escape(question); // Sanitized user input // Push sanitized user's question to message history this.messages.push({ role: 'user', content: sanitizedQuestion }); // Get system prompt with all tools (static + dynamic) const systemPrompt = this.getSystemPromptWithAllTools(); // Initialize intermediate messages this.interMessages = [{ role: 'system', content: systemPrompt }]; this.interMessages.push({ role: 'user', content: this.constructUserPrompt(1, 'user', `${sanitizedQuestion}`), }); // Setup XML parser and builder const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', textNodeName: '_text', isArray: name => ['query', 'url'].indexOf(name) !== -1, processEntities: false, // Disable processing of entities stopNodes: ['*.entity'], // Do not process any entities }); const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_' }); let currentAction: string | undefined; this.processingInfo = []; let i = 2; while (i < maxTurns) { console.log(this.interMessages); console.log(`Turn ${i}/${maxTurns}`); // eslint-disable-next-line no-await-in-loop const result = await this.execute(onProcessingUpdate, onAnswerUpdate); this.interMessages.push({ role: 'assistant', content: result }); i += 2; let parsedResult; try { // Parse XML result from the assistant parsedResult = parser.parse(result); // Validate the structure of the parsedResult this.validateAssistantResponse(parsedResult); } catch (error) { throw new Error(`Error parsing or validating response: ${error}`); } // Extract the stage from the parsed result const stage = parsedResult.stage; if (!stage) { throw new Error(`Error: No stage found in response`); } // Handle different stage elements (thoughts, actions, inputs, answers) for (const key in stage) { if (key === 'thought') { // Handle assistant's thoughts console.log(`Thought: ${stage[key]}`); this.processingNumber++; } else if (key === 'action') { // Handle action stage currentAction = stage[key] as string; console.log(`Action: ${currentAction}`); // Check both static tools and dynamic registry const tool = this.tools[currentAction] || this.dynamicToolRegistry.get(currentAction); if (currentAction === 'noTool') { // Immediately ask for clarification in plain text, not as a tool prompt this.interMessages.push({ role: 'user', content: ` I’m not sure what you’d like me to do. Could you clarify your request? `, }); break; } if (tool) { // Prepare the next action based on the current tool const nextPrompt = [ { type: 'text', 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: Action "${currentAction}" is not a valid tool`); this.interMessages.push({ role: 'user', content: `Action "${currentAction}" is not a valid tool, try again.`, }); break; } } else if (key === 'action_input') { // Handle action input stage const actionInput = stage[key]; console.log(`Action input full:`, actionInput); console.log(`Action input:`, actionInput.inputs); if (currentAction) { try { // Process the action with its input // eslint-disable-next-line no-await-in-loop const observation = (await this.processAction(currentAction, actionInput.inputs)) as Observation[]; const nextPrompt = [{ type: 'text', text: ` ` }, ...observation, { type: 'text', text: '' }] as Observation[]; console.log(observation); this.interMessages.push({ role: 'user', content: nextPrompt }); this.processingNumber++; console.log(`Tool ${currentAction} executed successfully. Observations:`, observation); break; } catch (error) { console.error(`Error during execution of tool '${currentAction}':`, error); const errorMessage = error instanceof Error ? error.message : String(error); // Return an error observation formatted for the LLM loop return { role: ASSISTANT_ROLE.USER, content: [ { type: TEXT_TYPE.ERROR, text: `Execution failed: ${escape(errorMessage)}`, index: 0, citation_ids: null, }, ], processing_info: [], }; } } else { throw new Error('Error: Action input without a valid action'); } } else if (key === 'answer') { // If an answer is found, end the query console.log('Answer found. Ending query.'); this.streamedAnswerParser.reset(); const parsedAnswer = AnswerParser.parse(result, this.processingInfo); return parsedAnswer; } } } throw new Error('Reached maximum turns. Ending query.'); } private constructUserPrompt(stageNumber: number, role: string, content: string): string { return `${content}`; } /** * Executes a step in the conversation, processing the assistant's response and parsing it in real-time. * @param onProcessingUpdate Callback for processing updates. * @param onAnswerUpdate Callback for answer updates. * @returns The full response from the assistant. */ private async execute(onProcessingUpdate: (processingUpdate: ProcessingInfo[]) => void, onAnswerUpdate: (answerUpdate: string) => void): Promise { // Stream OpenAI response for real-time updates const stream = await this.client.chat.completions.create({ model: 'gpt-4o', messages: this.interMessages as ChatCompletionMessageParam[], temperature: 0, stream: true, stop: [''], }); let fullResponse: string = ''; let currentTag: string = ''; let currentContent: string = ''; let isInsideTag: boolean = false; // Process each chunk of the streamed response for await (const chunk of stream) { const content = chunk.choices[0]?.delta?.content || ''; fullResponse += content; // Parse the streamed content character by character for (const char of content) { if (currentTag === 'answer') { // Handle answer parsing for real-time updates currentContent += char; const streamedAnswer = this.streamedAnswerParser.parse(char); onAnswerUpdate(streamedAnswer); continue; } else if (char === '<') { // Start of a new tag isInsideTag = true; currentTag = ''; currentContent = ''; } else if (char === '>') { // End of the tag isInsideTag = false; if (currentTag.startsWith('/')) { currentTag = ''; } } else if (isInsideTag) { // Append characters to the tag name currentTag += char; } else if (currentTag === 'thought' || currentTag === 'action_input_description') { // Handle processing information for thought or action input description currentContent += char; const current_info = this.processingInfo.find(info => info.index === this.processingNumber); if (current_info) { current_info.content = currentContent.trim(); onProcessingUpdate(this.processingInfo); } else { this.processingInfo.push({ index: this.processingNumber, type: currentTag === 'thought' ? PROCESSING_TYPE.THOUGHT : PROCESSING_TYPE.ACTION, content: currentContent.trim(), }); onProcessingUpdate(this.processingInfo); } } } } return fullResponse; } /** * Validates the assistant's response to ensure it conforms to the expected XML structure. * @param response The parsed XML response from the assistant. * @throws An error if the response does not meet the expected structure. */ private validateAssistantResponse(response: { stage: { [key: string]: object | string } }) { if (!response.stage) { throw new Error('Response does not contain a element'); } // Validate that the stage has the required attributes const stage = response.stage; if (!stage['@_number'] || !stage['@_role']) { throw new Error('Stage element must have "number" and "role" attributes'); } // Extract the role of the stage to determine expected content const role = stage['@_role']; // Depending on the role, validate the presence of required elements if (role === 'assistant') { // Assistant's response should contain either 'thought', 'action', 'action_input', or 'answer' if (!('thought' in stage || 'action' in stage || 'action_input' in stage || 'answer' in stage)) { throw new Error('Assistant stage must contain a thought, action, action_input, or answer element'); } // If 'thought' is present, validate it if ('thought' in stage) { if (typeof stage.thought !== 'string' || stage.thought.trim() === '') { throw new Error('Thought must be a non-empty string'); } } // If 'action' is present, validate it if ('action' in stage) { if (typeof stage.action !== 'string' || stage.action.trim() === '') { throw new Error('Action must be a non-empty string'); } // 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`); } } // If 'action_input' is present, validate its structure if ('action_input' in stage) { const actionInput = stage.action_input as object; if (!('action_input_description' in actionInput) || typeof actionInput.action_input_description !== 'string') { throw new Error('action_input must contain an action_input_description string'); } if (!('inputs' in actionInput)) { throw new Error('action_input must contain an inputs object'); } // Further validation of inputs can be done here based on the expected parameters of the action } // If 'answer' is present, validate its structure if ('answer' in stage) { const answer = stage.answer as object; // Ensure answer contains at least one of the required elements if (!('grounded_text' in answer || 'normal_text' in answer)) { throw new Error('Answer must contain grounded_text or normal_text'); } // Validate follow_up_questions if (!('follow_up_questions' in answer)) { throw new Error('Answer must contain follow_up_questions'); } // Validate loop_summary if (!('loop_summary' in answer)) { throw new Error('Answer must contain a loop_summary'); } // Additional validation for citations, grounded_text, etc., can be added here } } else if (role === 'user') { // User's stage should contain 'query' or 'observation' if (!('query' in stage || 'observation' in stage)) { throw new Error('User stage must contain a query or observation element'); } // Validate 'query' if present if ('query' in stage && typeof stage.query !== 'string') { throw new Error('Query must be a string'); } // Validate 'observation' if present if ('observation' in stage) { // Ensure observation has the correct structure // This can be expanded based on how observations are structured } } else { throw new Error(`Unknown role "${role}" in stage`); } // Add any additional validation rules as necessary } /** * Helper function to check if a string can be parsed as an array of the expected type. * @param input The input string to check. * @param expectedType The expected type of the array elements ('string', 'number', or 'boolean'). * @returns The parsed array if valid, otherwise throws an error. */ private parseArray(input: string, expectedType: 'string' | 'number' | 'boolean'): T[] { try { // Parse the input string into a JSON object const parsed = JSON.parse(input); // Check if the parsed object is an array and if all elements are of the expected type if (Array.isArray(parsed) && parsed.every(item => typeof item === expectedType)) { return parsed; } else { throw new Error(`Invalid ${expectedType} array format.`); } } catch (error) { throw new Error(`Failed to parse ${expectedType} array: ` + error); } } /** * Processes a specific action by invoking the appropriate tool with the provided inputs. * This method ensures that the action exists and validates the types of `actionInput` * based on the tool's parameter rules. It throws errors for missing required parameters * or mismatched types before safely executing the tool with the validated input. * * NOTE: In the future, it should typecheck for specific tool parameter types using the `TypeMap` or otherwise. * * Type validation includes checks for: * - `string`, `number`, `boolean` * - `string[]`, `number[]` (arrays of strings or numbers) * * @param action The action to perform. It corresponds to a registered tool. * @param actionInput The inputs for the action, passed as an object where each key is a parameter name. * @returns A promise that resolves to an array of `Observation` objects representing the result of the action. * @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 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 if ('fieldValue' in actionInput) { if (typeof actionInput.fieldValue === 'number' || typeof actionInput.fieldValue === 'boolean') { // Convert number or boolean to string to pass validation actionInput.fieldValue = String(actionInput.fieldValue); } } // Handle fieldEdits parameter (for multiple field edits) if ('fieldEdits' in actionInput && actionInput.fieldEdits) { try { // If it's already an array, stringify it to ensure it passes validation if (Array.isArray(actionInput.fieldEdits)) { actionInput.fieldEdits = JSON.stringify(actionInput.fieldEdits); } // If it's an object but not an array, it might be a single edit - convert to array and stringify else if (typeof actionInput.fieldEdits === 'object') { actionInput.fieldEdits = JSON.stringify([actionInput.fieldEdits]); } // Otherwise, ensure it's a string for the validator else if (typeof actionInput.fieldEdits !== 'string') { actionInput.fieldEdits = String(actionInput.fieldEdits); } } catch (error) { console.error('Error processing fieldEdits:', error); // Don't fail validation here, let the tool handle it } } } // 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) && !tool.inputValidator(actionInput)) { throw new Error(`Missing required parameter: ${param.name}`); } // Check if the parameter type matches the expected type const expectedType = param.type.replace('[]', '') as 'string' | 'number' | 'boolean'; const isArray = param.type.endsWith('[]'); const input = actionInput[param.name]; if (isArray) { // Check if the input is a valid array of the expected type const parsedArray = this.parseArray(input as string, expectedType); actionInput[param.name] = parsedArray as TypeMap[typeof param.type]; } else if (input !== undefined && typeof input !== expectedType) { throw new Error(`Invalid type for parameter ${param.name}: expected ${expectedType}`); } } // 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. * * @param chatBox The ChatBox instance to pass to the DocumentMetadataTool */ public reinitializeDocumentMetadataTool(): void { if (this.tools && this.tools.documentMetadata) { this.tools.documentMetadata = new DocumentMetadataTool(this._docManager); console.log('Agent: Reinitialized DocumentMetadataTool with ChatBox instance'); } else { console.warn('Agent: Could not reinitialize DocumentMetadataTool - tool not found'); } } } // Forward declaration to avoid circular import interface AgentLike { registerDynamicTool(toolName: string, toolInstance: BaseTool>): void; notifyToolCreated(toolName: string, completeToolCode: string): void; }