aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts77
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx3
-rw-r--r--src/client/views/nodes/chatbot/tools/BaseTool.ts11
-rw-r--r--src/client/views/nodes/chatbot/tools/SearchTool.ts22
-rw-r--r--src/client/views/nodes/chatbot/tools/ToolTypes.ts16
-rw-r--r--src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts17
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;
}