From 4997c3de20a381eac30224a7a550afa66174f07d Mon Sep 17 00:00:00 2001 From: Joanne Date: Mon, 12 May 2025 20:53:12 -0400 Subject: added tutorial tool, still need to integrate with metadatatool --- .../views/nodes/chatbot/agentsystem/Agent.ts | 32 +- .../nodes/chatbot/chatboxcomponents/ChatBox.tsx | 45 ++- .../views/nodes/chatbot/tools/TutorialTool.ts | 339 ++++++++++++--------- 3 files changed, 225 insertions(+), 191 deletions(-) (limited to 'src/client/views/nodes/chatbot') diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index e93fb87db..8075cab5f 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -7,24 +7,21 @@ import { AnswerParser } from '../response_parsers/AnswerParser'; import { StreamedAnswerParser } from '../response_parsers/StreamedAnswerParser'; import { BaseTool } from '../tools/BaseTool'; import { CalculateTool } from '../tools/CalculateTool'; -//import { CreateAnyDocumentTool } from '../tools/CreateAnyDocTool'; import { CreateDocTool } from '../tools/CreateDocumentTool'; import { DataAnalysisTool } from '../tools/DataAnalysisTool'; -import { ImageCreationTool } from '../tools/ImageCreationTool'; import { NoTool } from '../tools/NoTool'; import { SearchTool } from '../tools/SearchTool'; import { Parameter, ParametersType, TypeMap } from '../types/tool_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 { DictionaryTool } from '../tools/DictionaryTool'; import { ChatCompletionMessageParam } from 'openai/resources'; import { Doc } from '../../../../../fields/Doc'; import { parsedDoc } from '../chatboxcomponents/ChatBox'; import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool'; import { Upload } from '../../../../../server/SharedMediaTypes'; import { RAGTool } from '../tools/RAGTool'; -//import { CreateTextDocTool } from '../tools/CreateTextDocumentTool'; +import { GPTTutorialTool } from '../tools/TutorialTool'; dotenv.config(); @@ -47,6 +44,7 @@ export class Agent { private processingInfo: ProcessingInfo[] = []; private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser(); private tools: Record>>; + private Document: Doc; /** * The constructor initializes the agent with the vector store and toolset, and sets up the OpenAI client. @@ -65,8 +63,8 @@ export class Agent { addLinkedUrlDoc: (url: string, id: string) => void, createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void, addLinkedDoc: (doc: parsedDoc) => Doc | undefined, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - createCSVInDash: (url: string, title: string, id: string, data: string) => void + createCSVInDash: (url: string, title: string, id: string, data: string) => void, + document: Doc ) { // Initialize OpenAI client with API key from environment this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true }); @@ -74,6 +72,7 @@ export class Agent { this._history = history; this._summaries = summaries; this._csvData = csvData; + this.Document = document; // Define available tools for the assistant this.tools = { @@ -82,13 +81,9 @@ export class Agent { dataAnalysis: new DataAnalysisTool(csvData), websiteInfoScraper: new WebsiteInfoScraperTool(addLinkedUrlDoc), searchTool: new SearchTool(addLinkedUrlDoc), - // createCSV: new CreateCSVTool(createCSVInDash), noTool: new NoTool(), - imageCreationTool: new ImageCreationTool(createImage), - // createTextDoc: new CreateTextDocTool(addLinkedDoc), createDoc: new CreateDocTool(addLinkedDoc), - // createAnyDocument: new CreateAnyDocumentTool(addLinkedDoc), - // dictionary: new DictionaryTool(), + generateTutorialNode: new GPTTutorialTool(addLinkedDoc), }; } @@ -117,7 +112,18 @@ export class Agent { // Retrieve chat history and generate system prompt const chatHistory = this._history(); - const systemPrompt = getReactPrompt(Object.values(this.tools), this._summaries, chatHistory); + let systemPrompt = getReactPrompt(Object.values(this.tools), this._summaries, chatHistory); + + // If this is a Dash documentation assistant chat, modify the system prompt + if (this.Document?.is_dash_doc_assistant) { + systemPrompt = systemPrompt.replace( + '', + ` + IMPORTANT: You are specifically focused on helping users with questions about Dash documentation and usage. When users ask questions, interpret them in the context of Dash documentation and features, even if they don't explicitly mention Dash. For example, if a user asks "How do I create a document?", interpret this as "How do I create a document in Dash?" and provide relevant Dash-specific guidance. + + For any questions about Dash features, functionality, or usage, you should use the generateTutorialNode tool to create a tutorial document that explains the concept in detail. This tool will help create well-formatted, interactive tutorials that guide users through Dash features.` + ); + } // Initialize intermediate messages this.interMessages = [{ role: 'system', content: systemPrompt }]; @@ -132,7 +138,7 @@ export class Agent { ignoreAttributes: false, attributeNamePrefix: '@_', textNodeName: '_text', - isArray: name => ['query', 'url'].indexOf(name) !== -1, + isArray: name => name === 'url', processEntities: false, // Disable processing of entities stopNodes: ['*.entity'], // Do not process any entities }); diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index 6e9307d37..ad2f3e892 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -106,7 +106,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { 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.createImageInDash, this.createDocInDash, this.createCSVInDash); + this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createImageInDash, this.createDocInDash, this.createCSVInDash, this.Document); this.messagesRef = React.createRef(); // Reaction to update dataDoc when chat history changes @@ -309,7 +309,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { }; } }); - this.scrollToBottom(); }; const onAnswerUpdate = (answerUpdate: string) => { @@ -317,41 +316,29 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { if (this._current_message) { this._current_message = { ...this._current_message, - content: [{ text: answerUpdate, type: TEXT_TYPE.NORMAL, index: 0, citation_ids: [] }], + content: [{ index: 0, type: TEXT_TYPE.NORMAL, text: answerUpdate, citation_ids: null }], }; } }); }; - // Send the user's question to the assistant and get the final message - const finalMessage = await this.agent.askAgent(trimmedText, onProcessingUpdate, onAnswerUpdate); + // Get the response from the agent + const response = await this.agent.askAgent(trimmedText, onProcessingUpdate, onAnswerUpdate); - // Update the history with the final assistant message + // Push the final message to history runInAction(() => { - if (this._current_message) { - this._history.push({ ...finalMessage }); - this._current_message = undefined; - this.dataDoc.data = JSON.stringify(this._history); - } + this._history.push(response); + this._isLoading = false; + this._current_message = undefined; }); - } catch (err) { - console.error('Error:', err); - // Handle error in processing - runInAction(() => - this._history.push({ - role: ASSISTANT_ROLE.ASSISTANT, - content: [{ index: 0, type: TEXT_TYPE.ERROR, text: `Sorry, I encountered an error while processing your request: ${err} `, citation_ids: null }], - processing_info: [], - }) - ); - } finally { + } catch (error) { + console.error('Error in askGPT:', error); runInAction(() => { this._isLoading = false; + this._current_message = undefined; }); - this.scrollToBottom(); } } - this.scrollToBottom(); }; /** @@ -408,7 +395,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { if (doc) { LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc)); this._props.addDocument?.(doc); - DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {}).then(() => this.addCSVForAnalysis(doc, id)); + DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => this.addCSVForAnalysis(doc, id)); } }); @@ -446,7 +433,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { const ndoc = (() => { switch (doc.doc_type) { default: - case supportedDocTypes.text: return Docs.Create.TextDocument(data as string, options); + case supportedDocTypes.text: return Docs.Create.TextDocument(doc.text as string, options); case supportedDocTypes.comparison: return this.createComparison(JSON.parse(data as string) as parsedDoc[], options); case supportedDocTypes.flashcard: return this.createFlashcard(JSON.parse(data as string) as parsedDoc[], options); case supportedDocTypes.deck: return this.createDeck(JSON.parse(data as string) as parsedDoc[], options); @@ -825,7 +812,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { { index: 0, type: TEXT_TYPE.NORMAL, - text: `Hey, ${this.userName()}! Welcome to Your Friendly Assistant. Link a document or ask questions to get started.`, + text: this.dataDoc.is_dash_doc_assistant + ? 'Welcome to your help assistant for Dash. Ask any Dash-related questions to get started.' + : `Hey, ${this.userName()}! Welcome to Your Friendly Assistant. Link a document or ask questions to get started.`, citation_ids: null, }, ], @@ -987,7 +976,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent() { )}
-

{this.userName()}'s AI Assistant

+

{this.dataDoc.is_dash_doc_assistant ? 'Dash Help Assistant' : `${this.userName()}'s AI Assistant`}

{this._history.map((message, index) => ( diff --git a/src/client/views/nodes/chatbot/tools/TutorialTool.ts b/src/client/views/nodes/chatbot/tools/TutorialTool.ts index 69ae9c618..08e4e1409 100644 --- a/src/client/views/nodes/chatbot/tools/TutorialTool.ts +++ b/src/client/views/nodes/chatbot/tools/TutorialTool.ts @@ -1,166 +1,205 @@ import { BaseTool } from './BaseTool'; import { Observation } from '../types/types'; -import { Parameter, ParametersType, ToolInfo } from '../types/tool_types'; -// import { gptAPICall } from '../../../../apis/gpt/GPT'; +import { ParametersType, ToolInfo } from '../types/tool_types'; import { schema } from '../../../../views/nodes/formattedText/schema_rts'; -import { RichTextField } from '../../../../../fields/RichTextField'; -import { Docs } from '../../../../documents/Documents'; -import { DocumentViewInternal } from '../../../nodes/DocumentView'; import { v4 as uuidv4 } from 'uuid'; -import { OpenWhere } from '../../../../views/nodes/OpenWhere'; -import { gptAPICall } from '../../../../apis/gpt/GPT'; +import { gptTutorialAPICall } from '../../../../apis/gpt/TutorialGPT'; import { parsedDoc } from '../chatboxcomponents/ChatBox'; import { Id } from '../../../../../fields/FieldSymbols'; import { Doc } from '../../../../../fields/Doc'; - +import { RichTextField } from '../../../../../fields/RichTextField'; +import { DocumentViewInternal } from '../../DocumentView'; +import { Docs } from '../../../../documents/Documents'; +import { OpenWhere } from '../../OpenWhere'; +import { CollectionFreeFormView } from '../../../collections/collectionFreeForm'; const generateTutorialNodeToolParams = [ - { - name: 'query', - type: 'string', - description: 'The user\'s query about Dash functionality.', - required: true, - }, + { + name: 'query', + type: 'string', + description: 'The user query that asks how to use the environment', + required: true, + }, ] as const; const generateTutorialNodeToolInfo: ToolInfo = { - name: 'generateTutorialNode', - description: 'Generates a tutorial text node based on the user\'s query about Dash functionality. Use this when the user asks for help or tutorials on how to use Dash features.', - parameterRules: generateTutorialNodeToolParams, - citationRules: 'No citation needed for this tool\'s output.', + name: 'generateTutorialNode', + description: "Generates a tutorial text node based on the user's query about Dash functionality. Use this when the user asks for help or tutorials on how to use Dash features.", + parameterRules: generateTutorialNodeToolParams, + citationRules: "No citation needed for this tool's output.", +}; +const applyFormatting = (markdownText: string): { doc: any; plainText: string } => { + const lines = markdownText.split('\n'); + const nodes: any[] = []; + let plainText = ''; + let i = 0; + let currentListItems: any[] = []; + let currentParagraph: any[] = []; + let currentOrderedListItems: any[] = []; + let inOrderedList = false; + let inBulletList = false; + + const processBoldText = (text: string) => { + const boldRegex = /\*\*(.*?)\*\*/g; + const parts = []; + let lastIndex = 0; + let match; + + while ((match = boldRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(schema.text(text.substring(lastIndex, match.index))); + } + parts.push(schema.text(match[1], [schema.marks.strong.create()])); + lastIndex = match.index + match[0].length; + } + if (lastIndex < text.length) { + parts.push(schema.text(text.substring(lastIndex))); + } + return parts.length > 0 ? parts : [schema.text(text)]; + }; + + const flushListItems = () => { + if (currentListItems.length > 0) { + nodes.push(schema.nodes.ordered_list.create({ mapStyle: 'bullet' }, currentListItems)); + nodes.push(schema.nodes.paragraph.create()); + currentListItems = []; + inBulletList = false; + } + if (currentOrderedListItems.length > 0) { + nodes.push(schema.nodes.ordered_list.create({ mapStyle: 'number' }, currentOrderedListItems)); + nodes.push(schema.nodes.paragraph.create()); + currentOrderedListItems = []; + inOrderedList = false; + } + }; + + const flushParagraph = () => { + if (currentParagraph.length > 0) { + nodes.push(schema.nodes.paragraph.create({}, currentParagraph)); + currentParagraph = []; + } + }; + + const processHeader = (line: string) => { + const headerMatch = line.match(/^(#{1,6})\s+(.+)$/); + if (headerMatch) { + const level = Math.min(headerMatch[1].length, 6); // Cap at h6 + const textContent = headerMatch[2]; + flushParagraph(); + nodes.push(schema.nodes.heading.create({ level }, processBoldText(textContent))); + plainText += textContent + '\n'; + return true; + } + return false; + }; + + while (i < lines.length) { + const line = lines[i].trim(); + if (line) { + if (processHeader(line)) { + flushListItems(); + flushParagraph(); + } else if (line.startsWith('- ')) { + flushParagraph(); + if (!inBulletList) { + flushListItems(); + inBulletList = true; + } + const textContent = line.replace('- ', ''); + currentListItems.push(schema.nodes.list_item.create({}, schema.nodes.paragraph.create({}, processBoldText(textContent)))); + plainText += textContent + '\n'; + } else if (/^\d+\.\s+/.test(line)) { + flushParagraph(); + if (!inOrderedList) { + flushListItems(); + inOrderedList = true; + } + const textContent = line.replace(/^\d+\.\s+/, ''); + currentOrderedListItems.push(schema.nodes.list_item.create({}, schema.nodes.paragraph.create({}, processBoldText(textContent)))); + plainText += textContent + '\n'; + } else { + flushListItems(); + currentParagraph = currentParagraph.concat(processBoldText(line)); + plainText += line + '\n'; + } + } else { + flushListItems(); + flushParagraph(); + nodes.push(schema.nodes.paragraph.create()); + plainText += '\n'; + } + i++; + } + flushListItems(); + flushParagraph(); + + const doc = schema.nodes.doc.create({}, nodes); + return { doc, plainText: plainText.trim() }; }; export class GPTTutorialTool extends BaseTool { - private _createDocInDash: (doc: parsedDoc) => Doc | undefined; - - constructor(createDocInDash: (doc: parsedDoc) => Doc | undefined) { - super(generateTutorialNodeToolInfo); - - this._createDocInDash = createDocInDash; - } - -// private applyFormatting(markdownText: string): { doc: any; plainText: string } { -// const lines = markdownText.split('\n'); -// const nodes: any[] = []; -// let plainText = ''; -// let i = 0; -// let currentListItems: any[] = []; - -// const processBoldText = (text: string) => { -// const boldRegex = /\*\*(.*?)\*\*/g; -// const parts = []; -// let lastIndex = 0; -// let match; - -// while ((match = boldRegex.exec(text)) !== null) { -// if (match.index > lastIndex) { -// parts.push(schema.text(text.substring(lastIndex, match.index))); -// } -// parts.push(schema.text(match[1], [schema.marks.strong.create()])); -// lastIndex = match.index + match[0].length; -// } -// if (lastIndex < text.length) { -// parts.push(schema.text(text.substring(lastIndex))); -// } -// return parts.length > 0 ? parts : [schema.text(text)]; -// }; - -// const flushListItems = () => { -// if (currentListItems.length > 0) { -// nodes.push(schema.nodes.ordered_list.create({ mapStyle: 'bullet' }, currentListItems)); -// currentListItems = []; -// } -// }; - -// while (i < lines.length) { -// const line = lines[i].trim(); -// if (line) { -// if (line.startsWith('## ')) { -// flushListItems(); -// const textContent = line.replace('## ', ''); -// nodes.push(schema.nodes.heading.create({ level: 1 }, processBoldText(textContent))); -// plainText += textContent + '\n'; -// } else if (line.startsWith('- ')) { -// const textContent = line.replace('- ', ''); -// currentListItems.push( -// schema.nodes.list_item.create( -// {}, -// schema.nodes.paragraph.create({}, processBoldText(textContent)) -// ) -// ); -// plainText += textContent + '\n'; -// } else { -// flushListItems(); -// nodes.push(schema.nodes.paragraph.create({}, processBoldText(line))); -// plainText += line + '\n'; -// } -// } else { -// flushListItems(); -// nodes.push(schema.nodes.paragraph.create()); -// plainText += '\n'; -// } -// i++; -// } -// flushListItems(); - -// const doc = schema.nodes.doc.create({}, nodes); -// return { doc, plainText: plainText.trim() }; -// } - - async execute(args: ParametersType): Promise { - const chunkId = uuidv4(); - try { - console.log('Executing with args:', args); - const query = args.query; - if (typeof query !== 'string' || !query.trim()) { - return [{ - type: 'text', - text: `Invalid input: Query must be a non-empty string.` - }]; - } - - const markdownResponse = await gptAPICall(query); - if (!markdownResponse || typeof markdownResponse !== 'string') { - throw new Error('Invalid GPT API response'); - } - console.log('Markdown response:', markdownResponse); - - // const { doc } = this.applyFormatting(markdownResponse); - // const rtfData = { - // doc: doc.toJSON(), - // selection: { type: 'text', anchor: 1, head: 1 }, - // storedMarks: [], - // }; - // const serializedData = JSON.stringify(rtfData); - // console.log('Serialized data:', serializedData); - - const tutorialDoc: parsedDoc = { - doc_type: 'text', - data: markdownResponse, - title: 'Tutorial Node', - _width: 600, - _layout_fitWidth: true, - _layout_autoHeight: true, - text_fontSize: '16px', - }; - - const createdDoc = this._createDocInDash(tutorialDoc); - console.log('Created doc:', createdDoc); - if (!createdDoc || !createdDoc[Id]) { - throw new Error('Failed to create tutorial node'); - } - - return [{ - type: 'text', - text: `Created tutorial node with ID ${createdDoc[Id]}.` - }]; - } catch (error) { - console.error('Error in GPTTutorialTool:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return [{ - type: 'text', - text: `Error generating tutorial node: ${errorMessage}` - }]; + private _createDocInDash: (doc: parsedDoc) => Doc | undefined; + + constructor(createDocInDash: (doc: parsedDoc) => Doc | undefined) { + super(generateTutorialNodeToolInfo); + + this._createDocInDash = createDocInDash; + } + + async execute(args: ParametersType): Promise { + const chunkId = uuidv4(); + try { + const query = (args.query || '').trim(); + if (!query) { + return [{ type: 'text', text: `Please provide a query.` }]; + } + const markdown = await gptTutorialAPICall(query); + const { doc, plainText } = applyFormatting(markdown); + + // Build the ProseMirror‐in‐JSON + plain-text for RichTextField + const rtfData = { + doc: (doc as any).toJSON ? (doc as any).toJSON() : doc, + selection: { type: 'text', anchor: 0, head: 0 }, + storedMarks: [], + }; + const rtf = new RichTextField(JSON.stringify(rtfData), plainText); + + // Create and show the TextDocument directly: + const formattedDoc = Docs.Create.TextDocument(rtf, { + title: 'Tutorial Node', + _width: 600, + _layout_fitWidth: true, + _layout_autoHeight: true, + text_fontSize: '16px', + }); + DocumentViewInternal.addDocTabFunc(formattedDoc, OpenWhere.addRight); + + // If user asked about linking/pinning/presentation, also fire the in-app tutorial: + const q = query.toLowerCase(); + if (q.includes('link')) { + Doc.IsInfoUIDisabled = false; + CollectionFreeFormView.showTutorial('links'); + } else if (q.includes('presentation')) { + Doc.IsInfoUIDisabled = false; + CollectionFreeFormView.showTutorial('presentation'); + } else if (q.includes('pin')) { + Doc.IsInfoUIDisabled = false; + CollectionFreeFormView.showTutorial('pins'); + } + + return [ + { + type: 'text', + text: `Created tutorial node with ID ${formattedDoc[Id]}.`, + }, + ]; + } catch (error) { + return [ + { + type: 'text', + text: `Error generating tutorial node: ${error}`, + }, + ]; + } } - } -} \ No newline at end of file +} -- cgit v1.2.3-70-g09d2