diff options
author | A.J. Shulman <Shulman.aj@gmail.com> | 2024-10-17 11:14:51 -0400 |
---|---|---|
committer | A.J. Shulman <Shulman.aj@gmail.com> | 2024-10-17 11:14:51 -0400 |
commit | 14f412611299fc350f13b6f96be913d59533cfb3 (patch) | |
tree | 79e05a3467a44b5a3a5db5baaec2febce72a2526 /src | |
parent | 80d86bd5ae3e1d3dc70e7636f72a872a5fb2f01d (diff) |
Removed awaits inside loops and made Parameters readonly for better type safety
Diffstat (limited to 'src')
6 files changed, 104 insertions, 42 deletions
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<string, BaseTool>; + private tools: Record<string, BaseTool<ReadonlyArray<Parameter>>>; /** * 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: `<stage number="${i + 1}" role="user">` + builder.build({ action_rules: this.tools[currentAction].getActionRule() }) + `</stage>`, - }, + } 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: `<stage number="${i + 1}" role="user"> <observation>` }, ...observation, { type: 'text', text: '</observation></stage>' }]; + const nextPrompt = [{ type: 'text', text: `<stage number="${i + 1}" role="user"> <observation>` }, ...observation, { type: 'text', text: '</observation></stage>' }] 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<string, unknown>): Promise<Observation[]> { + // 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<typeof tool.parameterRules>); } } 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<MessageComponentProps> = ({ message, index, onFollowUpClick, onCitationClick, updateMessageCitations }) => { +const MessageComponentBox: React.FC<MessageComponentProps> = ({ 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<Parameter>`. + * This means `P` is a readonly array of `Parameter` objects that cannot be modified (immutable). */ -export abstract class BaseTool<P extends readonly Parameter[]> implements Tool<P> { +export abstract class BaseTool<P extends ReadonlyArray<Parameter>> implements Tool<P> { // 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<P extends readonly Parameter[]> implements Tool<P * Constructs a new `BaseTool` instance. * @param name - The name of the tool. * @param description - A detailed description of what the tool does. - * @param parameterRules - An array of parameter definitions (`Parameter[]`). + * @param parameterRules - A readonly array of parameter definitions (`ReadonlyArray<Parameter>`). * @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<SearchToolParamsType> { async execute(args: ParametersType<SearchToolParamsType>): Promise<Observation[]> { 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<SearchToolParamsType> { text: `<chunk chunk_id="${id}" chunk_type="text"><url>${result.url}</url><overview>${result.snippet}</overview></chunk>`, }; }); - 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<Parameter>`. * @template P - An array of `Parameter` objects defining the tool's parameters. */ -export interface Tool<P extends readonly Parameter[]> { +export interface Tool<P extends ReadonlyArray<Parameter>> { // 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<P extends readonly Parameter[]> { */ 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 extends Parameter> = 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<P extends readonly Parameter[]> = { +export type ParametersType<P extends ReadonlyArray<Parameter>> = { [K in P[number] as K['name']]: ParamType<K>; }; 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<WebsiteInfoScraperToolParam async execute(args: ParametersType<WebsiteInfoScraperToolParamsType>): Promise<Observation[]> { 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: `<chunk chunk_id="${id}" chunk_type="url">\n${website_plain_text}\n</chunk>`, - }); + } 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; } |