diff options
Diffstat (limited to 'src/client/views/nodes')
3 files changed, 225 insertions, 191 deletions
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<string, BaseTool<ReadonlyArray<Parameter>>>; + 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( + '<task>', + `<task> + 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 6c3da8977..fc51ca5f9 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<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.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<HTMLDivElement>(); // Reaction to update dataDoc when chat history changes @@ -309,7 +309,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; } }); - this.scrollToBottom(); }; const onAnswerUpdate = (answerUpdate: string) => { @@ -317,41 +316,29 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { 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<FieldViewProps>() { 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<FieldViewProps>() { 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); @@ -826,7 +813,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { { 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, }, ], @@ -989,7 +978,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { </div> )} <div className="chat-header"> - <h2>{this.userName()}'s AI Assistant</h2> + <h2>{this.dataDoc.is_dash_doc_assistant ? 'Dash Help Assistant' : `${this.userName()}'s AI Assistant`}</h2> </div> <div className="chat-messages" ref={this.messagesRef}> {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<typeof generateTutorialNodeToolParams> = { - 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<typeof generateTutorialNodeToolParams> { - 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<typeof generateTutorialNodeToolParams>): Promise<Observation[]> { - const chunkId = uuidv4(); - try { - console.log('Executing with args:', args); - const query = args.query; - if (typeof query !== 'string' || !query.trim()) { - return [{ - type: 'text', - text: `<chunk chunk_id="${chunkId}" chunk_type="error">Invalid input: Query must be a non-empty string.</chunk>` - }]; - } - - 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: `<chunk chunk_id="${chunkId}" chunk_type="tutorial_node_creation">Created tutorial node with ID ${createdDoc[Id]}.</chunk>` - }]; - } catch (error) { - console.error('Error in GPTTutorialTool:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return [{ - type: 'text', - text: `<chunk chunk_id="${chunkId}" chunk_type="error">Error generating tutorial node: ${errorMessage}</chunk>` - }]; + private _createDocInDash: (doc: parsedDoc) => Doc | undefined; + + constructor(createDocInDash: (doc: parsedDoc) => Doc | undefined) { + super(generateTutorialNodeToolInfo); + + this._createDocInDash = createDocInDash; + } + + async execute(args: ParametersType<typeof generateTutorialNodeToolParams>): Promise<Observation[]> { + const chunkId = uuidv4(); + try { + const query = (args.query || '').trim(); + if (!query) { + return [{ type: 'text', text: `<chunk chunk_id="${chunkId}" chunk_type="error">Please provide a query.</chunk>` }]; + } + 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: `<chunk chunk_id="${chunkId}" chunk_type="tutorial_node_creation">Created tutorial node with ID ${formattedDoc[Id]}.</chunk>`, + }, + ]; + } catch (error) { + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error">Error generating tutorial node: ${error}</chunk>`, + }, + ]; + } } - } -}
\ No newline at end of file +} |
