diff options
-rw-r--r-- | src/client/views/nodes/ChatBox/Agent.ts | 63 | ||||
-rw-r--r-- | src/client/views/nodes/ChatBox/AnswerParser.ts | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ChatBox/ChatBox.tsx | 22 | ||||
-rw-r--r-- | src/client/views/nodes/ChatBox/prompts.ts | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ChatBox/tools/CreateCSVTool.ts | 51 | ||||
-rw-r--r-- | src/server/ApiManagers/AssistantManager.ts | 37 |
6 files changed, 147 insertions, 32 deletions
diff --git a/src/client/views/nodes/ChatBox/Agent.ts b/src/client/views/nodes/ChatBox/Agent.ts index 6f42c08b9..eaa17d283 100644 --- a/src/client/views/nodes/ChatBox/Agent.ts +++ b/src/client/views/nodes/ChatBox/Agent.ts @@ -15,6 +15,7 @@ import { on } from 'events'; import { v4 as uuidv4 } from 'uuid'; import { AnswerParser } from './AnswerParser'; import { StreamedAnswerParser } from './StreamedAnswerParser'; +import { CreateCSVTool } from './tools/CreateCSVTool'; dotenv.config(); @@ -33,7 +34,14 @@ export class Agent { private processingInfo: ProcessingInfo[] = []; private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser(); - constructor(_vectorstore: Vectorstore, summaries: () => string, history: () => string, csvData: () => { filename: string; id: string; text: string }[], addLinkedUrlDoc: (url: string, id: string) => void) { + constructor( + _vectorstore: Vectorstore, + summaries: () => string, + history: () => string, + csvData: () => { filename: string; id: string; text: string }[], + addLinkedUrlDoc: (url: string, id: string) => void, + createCSVInDash: (url: string, title: string, id: string, data: string) => void + ) { this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true }); this.vectorstore = _vectorstore; this._history = history; @@ -45,6 +53,7 @@ export class Agent { dataAnalysis: new DataAnalysisTool(csvData), websiteInfoScraper: new WebsiteInfoScraperTool(addLinkedUrlDoc), searchTool: new SearchTool(addLinkedUrlDoc), + createCSV: new CreateCSVTool(createCSVInDash), no_tool: new NoTool(), }; } @@ -114,7 +123,7 @@ export class Agent { } } else if (key === 'action_input') { const actionInput = stage[key]; - console.log(`Action input:`, actionInput); + console.log(`Action input:`, actionInput.inputs); if (currentAction) { try { // Parse the inputs @@ -204,31 +213,31 @@ export class Agent { const tool = this.tools[action]; const args: Record<string, any> = {}; - for (const paramName in tool.parameters) { - if (actionInput[paramName] !== undefined) { - if (Array.isArray(actionInput[paramName])) { - // If the input is already an array, use it as is - args[paramName] = actionInput[paramName]; - } else if (typeof actionInput[paramName] === 'object' && actionInput[paramName] !== null) { - // If the input is an object, check if it has multiple of the same tag - const values = Object.values(actionInput[paramName]); - if (values.length > 1) { - // If there are multiple values, convert to an array - args[paramName] = values; - } else { - // If there's only one value, use it directly - args[paramName] = values[0]; - } - } else { - // For single values, use them as is - args[paramName] = actionInput[paramName]; - } - } else if (tool.parameters[paramName].required === 'true') { - throw new Error(`Missing required parameter '${paramName}' for action '${action}'`); - } - } - - return await tool.execute(args); + // for (const paramName in tool.parameters) { + // if (actionInput[paramName] !== undefined) { + // if (Array.isArray(actionInput[paramName])) { + // // If the input is already an array, use it as is + // args[paramName] = actionInput[paramName]; + // } else if (typeof actionInput[paramName] === 'object' && actionInput[paramName] !== null) { + // // If the input is an object, check if it has multiple of the same tag + // const values = Object.values(actionInput[paramName]); + // if (values.length > 1) { + // // If there are multiple values, convert to an array + // args[paramName] = values; + // } else { + // // If there's only one value, use it directly + // args[paramName] = values[0]; + // } + // } else { + // // For single values, use them as is + // args[paramName] = actionInput[paramName]; + // } + // } else if (tool.parameters[paramName].required === 'true') { + // throw new Error(`Missing required parameter '${paramName}' for action '${action}'`); + // } + // } + + return await tool.execute(actionInput); } private parseActionInputs(inputs: any): Record<string, string | string[]> { diff --git a/src/client/views/nodes/ChatBox/AnswerParser.ts b/src/client/views/nodes/ChatBox/AnswerParser.ts index b18083a27..05d26b8de 100644 --- a/src/client/views/nodes/ChatBox/AnswerParser.ts +++ b/src/client/views/nodes/ChatBox/AnswerParser.ts @@ -9,6 +9,7 @@ export class AnswerParser { const followUpQuestionsRegex = /<follow_up_questions>([\s\S]*?)<\/follow_up_questions>/; const questionRegex = /<question>(.*?)<\/question>/g; const groundedTextRegex = /<grounded_text citation_index="([^"]+)">([\s\S]*?)<\/grounded_text>/g; + const normalTextRegex = /<normal_text>([\s\S]*?)<\/normal_text>/g; const answerMatch = answerRegex.exec(xml); const citationsMatch = citationsRegex.exec(xml); @@ -49,6 +50,8 @@ export class AnswerParser { } } + rawTextContent = rawTextContent.replace(normalTextRegex, '$1'); + // Parse text content (normal and grounded) let lastIndex = 0; let match; diff --git a/src/client/views/nodes/ChatBox/ChatBox.tsx b/src/client/views/nodes/ChatBox/ChatBox.tsx index d79233fae..ffede6901 100644 --- a/src/client/views/nodes/ChatBox/ChatBox.tsx +++ b/src/client/views/nodes/ChatBox/ChatBox.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import OpenAI, { ClientOptions } from 'openai'; import * as React from 'react'; import { Doc, DocListCast } from '../../../../fields/Doc'; -import { CsvCast, DocCast, PDFCast, StrCast } from '../../../../fields/Types'; +import { CsvCast, DocCast, PDFCast, RTFCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { LinkManager } from '../../../util/LinkManager'; @@ -60,7 +60,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.vectorstore_id = StrCast(this.dataDoc.vectorstore_id); } this.vectorstore = new Vectorstore(this.vectorstore_id, this.retrieveDocIds); - this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc); + this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createCSVInDash); this.messagesRef = React.createRef<HTMLDivElement>(); reaction( @@ -77,7 +77,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; @action - addCSVForAnalysis = async (newLinkedDoc: Doc) => { + addCSVForAnalysis = async (newLinkedDoc: Doc, id?: string) => { console.log('adding csv file for analysis'); if (!newLinkedDoc.chunk_simpl) { const csvData: string = StrCast(newLinkedDoc.text); @@ -104,7 +104,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { model: 'gpt-3.5-turbo', }); console.log('CSV Data:', csvData); - const csvId = uuidv4(); + const csvId = id ?? uuidv4(); this.linked_csv_files.push({ filename: CsvCast(newLinkedDoc.data).url.pathname, @@ -240,6 +240,20 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } @action + createCSVInDash = async (url: string, title: string, id: string, data: string) => { + console.log('Creating CSV in Dash:', url, title); + const doc = DocCast(await DocUtils.DocumentFromType('csv', url, { title: title, text: RTFCast(data) })); + + const linkDoc = Docs.Create.LinkDocument(this.Document, doc); + LinkManager.Instance.addLink(linkDoc); + + doc && this._props.addDocument?.(doc); + await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {}); + + this.addCSVForAnalysis(doc, id); + }; + + @action handleCitationClick = (citation: Citation) => { console.log('Citation clicked:', citation); const currentLinkedDocs: Doc[] = this.linkedDocs; diff --git a/src/client/views/nodes/ChatBox/prompts.ts b/src/client/views/nodes/ChatBox/prompts.ts index 76d958daf..71a4f33b4 100644 --- a/src/client/views/nodes/ChatBox/prompts.ts +++ b/src/client/views/nodes/ChatBox/prompts.ts @@ -27,8 +27,9 @@ export function getReactPrompt(tools: Tool[], summaries: () => string, chatHisto <point>If a tool is needed, ALWAYS select the most appropriate tool based on the user's query.</point> <point>If the query could relate to user documents or require external information (e.g., RAG, search + website scraping, data analysis), USE the appropriate tool to gather that information.</point> <point>If there are no user docs or the user docs have not yielded helpful information, use the search tool to find websites followed by the website scraper tool to get useful infromation from one of those websites. You can use the website scraper (or even the search tool), multiple times to find information from multiple websites either from the same search or different searches.</point> + <point>Ensure at the end of every final answer, you provide exactly three follow-up questions from the user's perspective—from the perspective that they are asking the question.</point> <point>Always follow the response structure provided in the instructions.</point> - <point>If you are stuck in a loop because a tool isn't helping you even though you think it should, use the 'no_tool' action to proceed with the response and ask the user for more information or clarification or let them know you cannot answer their question and why.</point> + <point>If a tool doesn't work—or yield helpful results—after two tries, EITHER use another tool or proceed with the response and ask the user for more information or clarification or let them know you cannot answer their question and why. DO NOT CONTINUE WITH THE SAME TOOL 3 TIMES.</point> </critical_points> <response_structure> diff --git a/src/client/views/nodes/ChatBox/tools/CreateCSVTool.ts b/src/client/views/nodes/ChatBox/tools/CreateCSVTool.ts new file mode 100644 index 000000000..55015846b --- /dev/null +++ b/src/client/views/nodes/ChatBox/tools/CreateCSVTool.ts @@ -0,0 +1,51 @@ +import { BaseTool } from './BaseTool'; +import { Networking } from '../../../../Network'; + +export class CreateCSVTool extends BaseTool<{ csvData: string; filename: string }> { + private _handleCSVResult: (url: string, filename: string, id: string, data: string) => void; + + constructor(handleCSVResult: (url: string, title: string, id: string, data: string) => void) { + super( + 'createCSV', + 'Creates a CSV file from raw CSV data and saves it to the server', + { + type: 'object', + properties: { + csvData: { + type: 'string', + description: 'A string of comma-separated values representing the CSV data.', + }, + filename: { + type: 'string', + description: 'The base name of the CSV file to be created. Should end in ".csv".', + }, + }, + required: ['csvData', 'filename'], + }, + 'Provide a CSV string and a filename to create a CSV file.', + 'Creates a CSV file from the provided CSV string and saves it to the server with a unique identifier, returning the file URL and UUID.' + ); + this._handleCSVResult = handleCSVResult; + } + + async execute(args: { csvData: string; filename: string }): Promise<any> { + try { + console.log('Creating CSV file:', args.filename, ' with data:', args.csvData); + // Post the raw CSV data to the createCSV endpoint on the server + const { fileUrl, id } = await Networking.PostToServer('/createCSV', { filename: args.filename, data: args.csvData }); + + // Handle the result by invoking the callback + this._handleCSVResult(fileUrl, args.filename, id, args.csvData); + + return [ + { + type: 'text', + text: `File successfully created: ${fileUrl}. \nNow a CSV file with this data and the name ${args.filename} is available as a user doc.`, + }, + ]; + } catch (error) { + console.error('Error creating CSV file:', error); + throw new Error('Failed to create CSV file.'); + } + } +} diff --git a/src/server/ApiManagers/AssistantManager.ts b/src/server/ApiManagers/AssistantManager.ts index 9bc5bf128..0456c730b 100644 --- a/src/server/ApiManagers/AssistantManager.ts +++ b/src/server/ApiManagers/AssistantManager.ts @@ -298,6 +298,43 @@ export default class AssistantManager extends ApiManager { register({ method: Method.POST, + subscription: '/createCSV', + secureHandler: async ({ req, res }) => { + const { filename, data } = req.body; + + // Validate input + if (!filename || !data) { + res.status(400).send({ error: 'Filename and data fields are required.' }); + return; + } + + try { + // Generate a UUID for the file + const uuidv4 = uuid.v4(); + + // Construct the full filename with the UUID prefix + const fullFilename = `${uuidv4}-${filename}`; + + // Get the full server path where the file will be saved + const serverFilePath = serverPathToFile(Directory.csv, fullFilename); + + // Write the CSV data (which is a raw string) to the file + await writeFileAsync(serverFilePath, data, 'utf8'); + + // Construct the full client URL for accessing the file + const fileUrl = clientPathToFile(Directory.csv, fullFilename); + + // Return the file URL and UUID to the client + res.send({ fileUrl, id: uuidv4 }); + } catch (error: any) { + console.error('Error creating CSV file:', error); + res.status(500).send({ error: 'Failed to create CSV file.', details: error.message }); + } + }, + }); + + register({ + method: Method.POST, subscription: '/chunkDocument', secureHandler: async ({ req, res }) => { const { file_path } = req.body; |