aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/chatbot/agentsystem/Agent.ts
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-01-21 10:16:51 -0500
committerbobzel <zzzman@gmail.com>2025-01-21 10:16:51 -0500
commitcaf384bb374c47118296aa397446e85d3121116b (patch)
treeda270043f88d6cfa0d6b8f1838a55f6faba35bfa /src/client/views/nodes/chatbot/agentsystem/Agent.ts
parent22daafa9c505eb9bffb8604be0924a7450d20113 (diff)
parenta6a6929501b7ce9b3ceed73c7e8141a8d8f0c7dc (diff)
Merge branch 'master' into alyssa-agent
Diffstat (limited to 'src/client/views/nodes/chatbot/agentsystem/Agent.ts')
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts152
1 files changed, 143 insertions, 9 deletions
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index 0b0e211eb..a2a575f19 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -2,6 +2,7 @@ import dotenv from 'dotenv';
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
import OpenAI from 'openai';
import { ChatCompletionMessageParam } from 'openai/resources';
+import { escape } from 'lodash'; // Imported escape from lodash
import { AnswerParser } from '../response_parsers/AnswerParser';
import { StreamedAnswerParser } from '../response_parsers/StreamedAnswerParser';
import { CalculateTool } from '../tools/CalculateTool';
@@ -11,13 +12,14 @@ 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 } from '../types/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 { BaseTool } from '../tools/BaseTool';
import { Parameter, ParametersType, TypeMap } from '../types/tool_types';
import { CreateDocTool } from '../tools/CreateDocumentTool';
import { DocumentOptions } from '../../../../documents/Documents';
+import { CreateAnyDocumentTool } from '../tools/CreateAnyDocTool';
dotenv.config();
@@ -56,7 +58,7 @@ export class Agent {
history: () => string,
csvData: () => { filename: string; id: string; text: string }[],
addLinkedUrlDoc: (url: string, id: string) => void,
- addLinkedDoc: (doc_type: string, data: string, options: DocumentOptions, id: string) => void,
+ addLinkedDoc: (doc_type: string, data: string | undefined, options: DocumentOptions, id: string) => void,
createCSVInDash: (url: string, title: string, id: string, data: string) => void
) {
// Initialize OpenAI client with API key from environment
@@ -73,9 +75,11 @@ export class Agent {
dataAnalysis: new DataAnalysisTool(csvData),
// websiteInfoScraper: new WebsiteInfoScraperTool(addLinkedUrlDoc),
searchTool: new SearchTool(addLinkedUrlDoc),
- createCSV: new CreateCSVTool(createCSVInDash),
+ //createCSV: new CreateCSVTool(createCSVInDash),
noTool: new NoTool(),
createDoc: new CreateDocTool(addLinkedDoc),
+ //createTextDoc: new CreateTextDocTool(addLinkedDoc),
+ createAnyDocument: new CreateAnyDocumentTool(addLinkedDoc),
};
}
@@ -90,9 +94,17 @@ export class Agent {
*/
async askAgent(question: string, onProcessingUpdate: (processingUpdate: ProcessingInfo[]) => void, onAnswerUpdate: (answerUpdate: string) => void, maxTurns: number = 30): Promise<AssistantMessage> {
console.log(`Starting query: ${question}`);
+ const MAX_QUERY_LENGTH = 1000; // adjust the limit as needed
- // Push user's question to message history
- this.messages.push({ role: 'user', content: question });
+ // Check if the question exceeds the maximum length
+ if (question.length > MAX_QUERY_LENGTH) {
+ return { role: ASSISTANT_ROLE.ASSISTANT, content: [{ text: 'User query too long. Please shorten your question and try again.', index: 0, type: TEXT_TYPE.NORMAL, 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 });
// Retrieve chat history and generate system prompt
const chatHistory = this._history();
@@ -100,14 +112,20 @@ export class Agent {
// Initialize intermediate messages
this.interMessages = [{ role: 'system', content: systemPrompt }];
- this.interMessages.push({ role: 'user', content: `<stage number="1" role="user"><query>${question}</query></stage>` });
+
+ this.interMessages.push({
+ role: 'user',
+ content: this.constructUserPrompt(1, 'user', `<query>${sanitizedQuestion}</query>`),
+ });
// Setup XML parser and builder
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
textNodeName: '_text',
- isArray: (name /* , jpath, isLeafNode, isAttribute */) => ['query', 'url'].indexOf(name) !== -1,
+ 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: '@_' });
@@ -128,8 +146,11 @@ export class Agent {
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 response: ${error}`);
+ throw new Error(`Error parsing or validating response: ${error}`);
}
// Extract the stage from the parsed result
@@ -162,7 +183,10 @@ export class Agent {
} else {
// Handle error in case of an invalid action
console.log('Error: No valid action');
- this.interMessages.push({ role: 'user', content: `<stage number="${i + 1}" role="system-error-reporter">No valid action, try again.</stage>` });
+ this.interMessages.push({
+ role: 'user',
+ content: `<stage number="${i + 1}" role="system-error-reporter">No valid action, try again.</stage>`,
+ });
break;
}
} else if (key === 'action_input') {
@@ -199,6 +223,10 @@ export class Agent {
throw new Error('Reached maximum turns. Ending query.');
}
+ private constructUserPrompt(stageNumber: number, role: string, content: string): string {
+ return `<stage number="${stageNumber}" role="${role}">${content}</stage>`;
+ }
+
/**
* Executes a step in the conversation, processing the assistant's response and parsing it in real-time.
* @param onProcessingUpdate Callback for processing updates.
@@ -212,6 +240,7 @@ export class Agent {
messages: this.interMessages as ChatCompletionMessageParam[],
temperature: 0,
stream: true,
+ stop: ['</stage>'],
});
let fullResponse: string = '';
@@ -269,6 +298,111 @@ export class Agent {
}
/**
+ * 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: any) {
+ if (!response.stage) {
+ throw new Error('Response does not contain a <stage> 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
+ const allowedActions = Object.keys(this.tools);
+ 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;
+
+ 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;
+
+ // 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').