diff options
| author | bobzel <zzzman@gmail.com> | 2025-01-21 10:16:51 -0500 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2025-01-21 10:16:51 -0500 |
| commit | caf384bb374c47118296aa397446e85d3121116b (patch) | |
| tree | da270043f88d6cfa0d6b8f1838a55f6faba35bfa /src/client/views/nodes/chatbot/agentsystem/Agent.ts | |
| parent | 22daafa9c505eb9bffb8604be0924a7450d20113 (diff) | |
| parent | a6a6929501b7ce9b3ceed73c7e8141a8d8f0c7dc (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.ts | 152 |
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'). |
