From 14f412611299fc350f13b6f96be913d59533cfb3 Mon Sep 17 00:00:00 2001 From: "A.J. Shulman" Date: Thu, 17 Oct 2024 11:14:51 -0400 Subject: Removed awaits inside loops and made Parameters readonly for better type safety --- .../views/nodes/chatbot/agentsystem/Agent.ts | 77 ++++++++++++++++++---- .../chatbot/chatboxcomponents/MessageComponent.tsx | 3 +- src/client/views/nodes/chatbot/tools/BaseTool.ts | 11 ++-- src/client/views/nodes/chatbot/tools/SearchTool.ts | 22 ++++--- src/client/views/nodes/chatbot/tools/ToolTypes.ts | 16 ++--- .../nodes/chatbot/tools/WebsiteInfoScraperTool.ts | 17 +++-- 6 files changed, 104 insertions(+), 42 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index ba5868207..34e7cf5ea 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -11,10 +11,11 @@ import { NoTool } from '../tools/NoTool'; import { RAGTool } from '../tools/RAGTool'; import { SearchTool } from '../tools/SearchTool'; import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool'; -import { AgentMessage, AssistantMessage, Observation, PROCESSING_TYPE, ProcessingInfo, Tool } from '../types/types'; +import { AgentMessage, AssistantMessage, Observation, PROCESSING_TYPE, ProcessingInfo } from '../types/types'; import { Vectorstore } from '../vectorstore/Vectorstore'; import { getReactPrompt } from './prompts'; import { BaseTool } from '../tools/BaseTool'; +import { Parameter, ParametersType, Tool } from '../tools/ToolTypes'; dotenv.config(); @@ -36,7 +37,7 @@ export class Agent { private processingNumber: number = 0; private processingInfo: ProcessingInfo[] = []; private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser(); - private tools: Record; + private tools: Record>>; /** * The constructor initializes the agent with the vector store and toolset, and sets up the OpenAI client. @@ -109,15 +110,16 @@ export class Agent { let currentAction: string | undefined; this.processingInfo = []; - // Conversation loop (up to maxTurns) - for (let i = 2; i < maxTurns; i += 2) { + let i = 2; + while (i < maxTurns) { console.log(this.interMessages); console.log(`Turn ${i}/${maxTurns}`); - // Execute a step in the conversation and get the result 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 @@ -149,7 +151,7 @@ export class Agent { { type: 'text', text: `` + builder.build({ action_rules: this.tools[currentAction].getActionRule() }) + ``, - }, + } as Observation, ]; this.interMessages.push({ role: 'user', content: nextPrompt }); break; @@ -168,7 +170,7 @@ export class Agent { try { // Process the action with its input const observation = (await this.processAction(currentAction, actionInput.inputs)) as Observation[]; - const nextPrompt = [{ type: 'text', text: ` ` }, ...observation, { type: 'text', text: '' }]; + const nextPrompt = [{ type: 'text', text: ` ` }, ...observation, { type: 'text', text: '' }] as Observation[]; console.log(observation); this.interMessages.push({ role: 'user', content: nextPrompt }); this.processingNumber++; @@ -263,16 +265,69 @@ export class Agent { /** * Processes a specific action by invoking the appropriate tool with the provided inputs. - * @param action The action to perform. - * @param actionInput The inputs for the action. - * @returns The result of the action. + * 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. + * + * 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: Record): Promise { + // Check if the action exists in the tools list if (!(action in this.tools)) { throw new Error(`Unknown action: ${action}`); } const tool = this.tools[action]; - return await tool.execute(actionInput); + + // Validate actionInput based on tool's parameter rules + for (const paramRule of tool.parameterRules) { + const inputValue = actionInput[paramRule.name]; + + if (paramRule.required && inputValue === undefined) { + throw new Error(`Missing required parameter: ${paramRule.name}`); + } + + // If the parameter is defined, check its type + if (inputValue !== undefined) { + switch (paramRule.type) { + case 'string': + if (typeof inputValue !== 'string') { + throw new Error(`Expected parameter '${paramRule.name}' to be a string.`); + } + break; + case 'number': + if (typeof inputValue !== 'number') { + throw new Error(`Expected parameter '${paramRule.name}' to be a number.`); + } + break; + case 'boolean': + if (typeof inputValue !== 'boolean') { + throw new Error(`Expected parameter '${paramRule.name}' to be a boolean.`); + } + break; + case 'string[]': + if (!Array.isArray(inputValue) || !inputValue.every(item => typeof item === 'string')) { + throw new Error(`Expected parameter '${paramRule.name}' to be an array of strings.`); + } + break; + case 'number[]': + if (!Array.isArray(inputValue) || !inputValue.every(item => typeof item === 'number')) { + throw new Error(`Expected parameter '${paramRule.name}' to be an array of numbers.`); + } + break; + default: + throw new Error(`Unsupported parameter type: ${paramRule.type}`); + } + } + } + + return await tool.execute(actionInput as ParametersType); } } diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx index d48f46963..e463d15bf 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx @@ -23,7 +23,6 @@ import ReactMarkdown from 'react-markdown'; */ interface MessageComponentProps { message: AssistantMessage; - index: number; onFollowUpClick: (question: string) => void; onCitationClick: (citation: Citation) => void; updateMessageCitations: (index: number, citations: Citation[]) => void; @@ -34,7 +33,7 @@ interface MessageComponentProps { * processing information, and follow-up questions. * @param {MessageComponentProps} props - The props for the component. */ -const MessageComponentBox: React.FC = ({ message, index, onFollowUpClick, onCitationClick, updateMessageCitations }) => { +const MessageComponentBox: React.FC = ({ message, onFollowUpClick, onCitationClick }) => { // State for managing whether the dropdown is open or closed for processing info const [dropdownOpen, setDropdownOpen] = useState(false); diff --git a/src/client/views/nodes/chatbot/tools/BaseTool.ts b/src/client/views/nodes/chatbot/tools/BaseTool.ts index e01296ac4..58cd514d9 100644 --- a/src/client/views/nodes/chatbot/tools/BaseTool.ts +++ b/src/client/views/nodes/chatbot/tools/BaseTool.ts @@ -1,4 +1,5 @@ -import { Tool, Parameter, ParametersType, Observation } from '../types/types'; +import { Observation } from '../types/types'; +import { Parameter, Tool, ParametersType } from './ToolTypes'; /** * @file BaseTool.ts @@ -10,10 +11,10 @@ import { Tool, Parameter, ParametersType, Observation } from '../types/types'; /** * The `BaseTool` class is an abstract class that implements the `Tool` interface. - * It is generic over a type parameter `P`, which extends `readonly Parameter[]`. - * This means `P` is an array of `Parameter` objects that cannot be modified (immutable). + * It is generic over a type parameter `P`, which extends `ReadonlyArray`. + * This means `P` is a readonly array of `Parameter` objects that cannot be modified (immutable). */ -export abstract class BaseTool

implements Tool

{ +export abstract class BaseTool

> implements Tool

{ // The name of the tool (e.g., "calculate", "searchTool") name: string; // A description of the tool's functionality @@ -29,7 +30,7 @@ export abstract class BaseTool

implements Tool

`). * @param citationRules - Rules or guidelines for citations. * @param briefSummary - A short summary of the tool. */ diff --git a/src/client/views/nodes/chatbot/tools/SearchTool.ts b/src/client/views/nodes/chatbot/tools/SearchTool.ts index c5cf951e7..fd5144dd6 100644 --- a/src/client/views/nodes/chatbot/tools/SearchTool.ts +++ b/src/client/views/nodes/chatbot/tools/SearchTool.ts @@ -34,9 +34,9 @@ export class SearchTool extends BaseTool { async execute(args: ParametersType): Promise { const queries = args.query; - const allResults: Observation[] = []; - for (const query of queries) { + // Create an array of promises, each one handling a search for a query + const searchPromises = queries.map(async query => { try { const { results } = await Networking.PostToServer('/getWebSearchResults', { query, @@ -49,16 +49,20 @@ export class SearchTool extends BaseTool { text: `${result.url}${result.snippet}`, }; }); - allResults.push(...data); + return data; } catch (error) { console.log(error); - allResults.push({ - type: 'text', - text: `An error occurred while performing the web search for query: ${query}`, - }); + return [ + { + type: 'text', + text: `An error occurred while performing the web search for query: ${query}`, + }, + ]; } - } + }); + + const allResultsArrays = await Promise.all(searchPromises); - return allResults; + return allResultsArrays.flat(); } } diff --git a/src/client/views/nodes/chatbot/tools/ToolTypes.ts b/src/client/views/nodes/chatbot/tools/ToolTypes.ts index 74a92bcf2..d47a38952 100644 --- a/src/client/views/nodes/chatbot/tools/ToolTypes.ts +++ b/src/client/views/nodes/chatbot/tools/ToolTypes.ts @@ -2,10 +2,10 @@ import { Observation } from '../types/types'; /** * The `Tool` interface represents a generic tool in the system. - * It is generic over a type parameter `P`, which extends `readonly Parameter[]`. + * It is generic over a type parameter `P`, which extends `ReadonlyArray`. * @template P - An array of `Parameter` objects defining the tool's parameters. */ -export interface Tool

{ +export interface Tool

> { // The name of the tool (e.g., "calculate", "searchTool") name: string; // A description of the tool's functionality @@ -34,15 +34,15 @@ export interface Tool

{ */ export type Parameter = { // The type of the parameter; constrained to the types 'string', 'number', 'boolean', 'string[]', 'number[]' - type: 'string' | 'number' | 'boolean' | 'string[]' | 'number[]'; + readonly type: 'string' | 'number' | 'boolean' | 'string[]' | 'number[]'; // The name of the parameter - name: string; + readonly name: string; // A description of the parameter - description: string; + readonly description: string; // Indicates whether the parameter is required - required: boolean; + readonly required: boolean; // (Optional) The maximum number of inputs (useful for array types) - max_inputs?: number; + readonly max_inputs?: number; }; /** @@ -71,6 +71,6 @@ export type ParamType

= P['type'] extends keyof TypeMap ? T * This is used to define the types of the arguments passed to the `execute` method of a tool. * @template P - An array of `Parameter` objects. */ -export type ParametersType

= { +export type ParametersType

> = { [K in P[number] as K['name']]: ParamType; }; diff --git a/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts b/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts index e91ebdad1..f2e3863a6 100644 --- a/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts +++ b/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts @@ -72,25 +72,28 @@ export class WebsiteInfoScraperTool extends BaseTool): Promise { const urls = args.urls; - const results: Observation[] = []; - for (const url of urls) { + // Create an array of promises, each one handling a website scrape for a URL + const scrapingPromises = urls.map(async url => { try { const { website_plain_text } = await Networking.PostToServer('/scrapeWebsite', { url }); const id = uuidv4(); this._addLinkedUrlDoc(url, id); - results.push({ + return { type: 'text', text: `\n${website_plain_text}\n`, - }); + } as Observation; } catch (error) { console.log(error); - results.push({ + return { type: 'text', text: `An error occurred while scraping the website: ${url}`, - }); + } as Observation; } - } + }); + + // Wait for all scraping promises to resolve + const results = await Promise.all(scrapingPromises); return results; } -- cgit v1.2.3-70-g09d2