diff options
Diffstat (limited to 'src/client/views')
6 files changed, 87 insertions, 58 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx index 595bbf2e9..5a54553b1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx @@ -9,6 +9,9 @@ import { DocButtonState, DocumentLinksButton } from '../../nodes/DocumentLinksBu import { TopBar } from '../../topbar/TopBar'; import { CollectionFreeFormInfoState, InfoState, StateEntryFunc, infoState } from './CollectionFreeFormInfoState'; import { CollectionFreeFormView } from './CollectionFreeFormView'; +import { Button } from '@dash/components'; +import { ButtonType } from '../../nodes/FontIconBox/FontIconBox'; + import './CollectionFreeFormView.scss'; export interface CollectionFreeFormInfoUIProps { diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index 361c5eb2b..a16794e10 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -27,7 +27,7 @@ import { CodebaseSummarySearchTool } from '../tools/CodebaseSummarySearchTool'; import { FileContentTool } from '../tools/FileContentTool'; import { FileNamesTool } from '../tools/FileNamesTool'; import { CreateNewTool } from '../tools/CreateNewTool'; -//import { CreateTextDocTool } from '../tools/CreateTextDocumentTool'; +import { GPTTutorialTool } from '../tools/TutorialTool'; dotenv.config(); @@ -50,6 +50,7 @@ export class Agent { private streamedAnswerParser: StreamedAnswerParser = new StreamedAnswerParser(); private tools: Record<string, BaseTool<ReadonlyArray<Parameter>>>; private _docManager: AgentDocumentManager; + private is_dash_doc_assistant: boolean; // Dynamic tool registry for tools created at runtime private dynamicToolRegistry: Map<string, BaseTool<ReadonlyArray<Parameter>>> = new Map(); // Callback for notifying when tools are created and need reload @@ -74,7 +75,8 @@ export class Agent { csvData: () => { filename: string; id: string; text: string }[], createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void, createCSVInDash: (url: string, title: string, id: string, data: string) => void, - docManager: AgentDocumentManager + docManager: AgentDocumentManager, + isDashDocAssistant: boolean ) { // Initialize OpenAI client with API key from environment this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true }); @@ -82,6 +84,7 @@ export class Agent { this._history = history; this._csvData = csvData; this._docManager = docManager; + this.is_dash_doc_assistant = isDashDocAssistant; // Initialize dynamic tool registry this.dynamicToolRegistry = new Map(); @@ -100,6 +103,7 @@ export class Agent { codebaseSummarySearch: new CodebaseSummarySearchTool(this.vectorstore), fileContent: new FileContentTool(this.vectorstore), fileNames: new FileNamesTool(this.vectorstore), + generateTutorialNode: new GPTTutorialTool(this._docManager), }; // Add the createNewTool after other tools are defined @@ -139,7 +143,7 @@ export class Agent { const instance: BaseTool<ReadonlyArray<Parameter>> = new ToolClass(); - // Prefer the tool’s self-declared name (matches <action> tag) + // Prefer the tool's self-declared name (matches <action> tag) const key = (instance.name || '').trim() || legacyKey; // Check for duplicates @@ -756,7 +760,7 @@ export class Agent { const docSummaries = () => JSON.stringify(this._docManager.listDocs); const chatHistory = this._history(); - return getReactPrompt(allTools, docSummaries, chatHistory); + return getReactPrompt(allTools, docSummaries, chatHistory, this.is_dash_doc_assistant); } /** diff --git a/src/client/views/nodes/chatbot/agentsystem/prompts.ts b/src/client/views/nodes/chatbot/agentsystem/prompts.ts index fcb4ab450..b7678bd08 100644 --- a/src/client/views/nodes/chatbot/agentsystem/prompts.ts +++ b/src/client/views/nodes/chatbot/agentsystem/prompts.ts @@ -10,7 +10,7 @@ import { BaseTool } from '../tools/BaseTool'; import { Parameter } from '../types/tool_types'; -export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summaries: () => string, chatHistory: string): string { +export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summaries: () => string, chatHistory: string, isDashDocAssistant?: boolean): string { const toolDescriptions = tools .map( tool => ` @@ -21,11 +21,21 @@ export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summ ) .join('\n'); + const dashDocContext = isDashDocAssistant + ? ` + <dash_doc_assistant_context> + <point>You are acting as a help assistant for a software application called Dash.</point> + <point>All user queries, unless otherwise specified, should be interpreted as questions about how to use Dash or about Dash's functionality.</point> + <point>You should prioritize using the 'generateTutorialNode' tool to answer user questions about Dash.</point> + </dash_doc_assistant_context> + ` + : ''; + return `<system_message> <task> You are an advanced AI assistant equipped with tools to answer user queries efficiently. You operate in a loop that is RIGIDLY structured and requires the use of specific tags and formats for your responses. Your goal is to provide accurate and well-structured answers to user queries. Below are the guidelines and information you can use to structure your approach to accomplishing this task. </task> - + ${dashDocContext} <critical_points> <point>**STRUCTURE**: Always use the correct stage tags (e.g., <stage number="2" role="assistant">) for every response. Use only even-numbered assisntant stages for your responses.</point> <point>**STOP after every stage and wait for input. Do not combine multiple stages in one response.**</point> @@ -189,7 +199,7 @@ export function getReactPrompt(tools: BaseTool<ReadonlyArray<Parameter>>[], summ <action_input> <action_input_description>Getting information from the relevant websites about Qatar's tourism impact during the World Cup.</action_input_description> <inputs> - <urls>[***URLS to search elided, but they will be comma seperated double quoted strings"]</urls> + <chunk_ids>[***CHUNK IDS to search elided, but they will be comma separated double quoted strings"]</chunk_ids> </inputs> </action_input> </stage> diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index db01b7c88..18d0266af 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -7,23 +7,21 @@ * with support for follow-up questions and citation management. */ -import dotenv from 'dotenv'; import { ObservableSet, action, computed, makeObservable, observable, observe, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import OpenAI, { ClientOptions } from 'openai'; import * as React from 'react'; import { v4 as uuidv4 } from 'uuid'; import { ClientUtils, OmitKeys } from '../../../../../ClientUtils'; -import { Doc, DocListCast, Opt } from '../../../../../fields/Doc'; -import { DocData, DocLayout, DocViews } from '../../../../../fields/DocSymbols'; +import { Doc, Opt } from '../../../../../fields/Doc'; +import { DocViews } from '../../../../../fields/DocSymbols'; import { Id } from '../../../../../fields/FieldSymbols'; import { RichTextField } from '../../../../../fields/RichTextField'; import { ScriptField } from '../../../../../fields/ScriptField'; -import { CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast, VideoCast, AudioCast } from '../../../../../fields/Types'; +import { CsvCast, DocCast, PDFCast, RTFCast, StrCast, VideoCast, AudioCast } from '../../../../../fields/Types'; import { DocUtils } from '../../../../documents/DocUtils'; import { CollectionViewType, DocumentType } from '../../../../documents/DocumentTypes'; import { Docs, DocumentOptions } from '../../../../documents/Documents'; -import { DocServer } from '../../../../DocServer'; import { DocumentManager } from '../../../../util/DocumentManager'; import { ImageUtils } from '../../../../util/Import & Export/ImageUtils'; import { LinkManager } from '../../../../util/LinkManager'; @@ -44,11 +42,8 @@ import './ChatBox.scss'; import MessageComponentBox from './MessageComponent'; import { OpenWhere } from '../../OpenWhere'; import { Upload } from '../../../../../server/SharedMediaTypes'; -import { DocumentMetadataTool } from '../tools/DocumentMetadataTool'; import { AgentDocumentManager } from '../utils/AgentDocumentManager'; -dotenv.config(); - export type parsedDocData = { doc_type: string; data: unknown; @@ -58,6 +53,7 @@ export type parsedDocData = { data_useCors?: boolean; }; export type parsedDoc = DocumentOptions & parsedDocData; + /** * ChatBox is the main class responsible for managing the interaction between the user and the assistant, * handling documents, and integrating with OpenAI for tasks such as document analysis, chat functionality, @@ -124,7 +120,15 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.vectorstore = new Vectorstore(this.vectorstore_id, this.docManager); // Create an agent with the vectorstore - this.agent = new Agent(this.vectorstore, this.retrieveFormattedHistory.bind(this), this.retrieveCSVData.bind(this), this.createImageInDash.bind(this), this.createCSVInDash.bind(this), this.docManager); + this.agent = new Agent( + this.vectorstore, + this.retrieveFormattedHistory.bind(this), + this.retrieveCSVData.bind(this), + this.createImageInDash.bind(this), + this.createCSVInDash.bind(this), + this.docManager, + this.dataDoc.is_dash_doc_assistant === 'true' + ); // Set up the tool created callback this.agent.setToolCreatedCallback(this.handleToolCreated); @@ -388,7 +392,11 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; // Get the response from the agent - const response = await this.agent.askAgent(trimmedText, onProcessingUpdate, onAnswerUpdate); + let userQuery = trimmedText; + if (this.dataDoc.is_dash_doc_assistant) { + userQuery = `The user is asking a question about Dash functionality. Their question is: "${trimmedText}". You should use the generateTutorialNode tool to answer this question.`; + } + const response = await this.agent.askAgent(userQuery, onProcessingUpdate, onAnswerUpdate); // Push the final message to history runInAction(() => { @@ -476,7 +484,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const data = (doc as parsedDocData).data; const ndoc = (() => { switch (doc.doc_type) { - default: + default: case supportedDocTypes.note: return Docs.Create.TextDocument(data 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); @@ -487,22 +495,22 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { case supportedDocTypes.web: // Create web document with enhanced safety options const webOptions = { - ...options, + ...options, data_useCors: true }; - + // If iframe_sandbox was passed from AgentDocumentManager, add it to the options if ('_iframe_sandbox' in options) { (webOptions as any)._iframe_sandbox = options._iframe_sandbox; } - + return Docs.Create.WebDocument(data as string, webOptions); case supportedDocTypes.dataviz: return Docs.Create.DataVizDocument('/users/rz/Downloads/addresses.csv', options); case supportedDocTypes.pdf: return Docs.Create.PdfDocument(data as string, options); case supportedDocTypes.video: return Docs.Create.VideoDocument(data as string, options); case supportedDocTypes.diagram: return Docs.Create.DiagramDocument(undefined, { text: data as unknown as RichTextField, ...options}); // text: can take a string or RichTextField but it's typed for RichTextField. - - // case supportedDocumentTypes.dataviz: + + // case supportedDocumentTypes.dataviz: // { // const { fileUrl, id } = await Networking.PostToServer('/createCSV', { // filename: (options.title as string).replace(/\s+/g, '') + '.csv', @@ -527,7 +535,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const arr = this.createCollectionWithChildren(JSON.parse(data as string) as parsedDoc[], true).filter(d=>d).map(d => d!); const collOpts = { _width:300, _height: 300, _layout_fitWidth: true, _freeform_backgroundGrid: true, ...options, }; return (() => { - switch (options.type_collection) { + switch (options.type_collection) { case CollectionViewType.Tree: return Docs.Create.TreeDocument(arr, collOpts); case CollectionViewType.Stacking: return Docs.Create.StackingDocument(arr, collOpts); case CollectionViewType.Masonry: return Docs.Create.MasonryDocument(arr, collOpts); @@ -536,9 +544,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { case CollectionViewType.Carousel3D: return Docs.Create.Carousel3DDocument(arr, collOpts); case CollectionViewType.Multicolumn: return Docs.Create.CarouselDocument(arr, collOpts); default: return Docs.Create.FreeformDocument(arr, collOpts); - } - })(); - } + } + })(); + } // case supportedDocumentTypes.map: return Docs.Create.MapDocument([], options); // case supportedDocumentTypes.button: return Docs.Create.ButtonDocument(options); // case supportedDocumentTypes.trail: return Docs.Create.PresDocument(options); @@ -646,8 +654,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } else { console.warn(`Chunk not found for chunk ID: ${chunkId}`); } - return; - } + return; + } console.log(`Found chunk in document:`, foundChunk); @@ -656,7 +664,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const directMatchSegmentStart = this.getDirectMatchingSegmentStart(doc, citation.direct_text || '', foundChunk.indexes || []); if (directMatchSegmentStart) { await this.goToMediaTimestamp(doc, directMatchSegmentStart, foundChunk.chunkType); - } else { + } else { console.error('No direct matching segment found for the citation.'); } } else if (foundChunk.chunkType === CHUNK_TYPE.TABLE || foundChunk.chunkType === CHUNK_TYPE.IMAGE) { @@ -911,7 +919,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { console.error(`Maximum verification attempts (${attempt}) reached for document ${doc.id}`); // Last resort: force re-creation of the document view - if (isPDF) { + if (isPDF) { console.log('Forcing document recreation as last resort'); DocumentManager.Instance.showDocument(doc, { willZoomCentered: true, @@ -939,7 +947,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return; } - this.processPDFDocumentView(doc, isPDF, citation, foundChunk); + this.processPDFDocumentView(doc, isPDF, citation, foundChunk); } catch (error) { console.error(`Error on verification attempt ${attempt}:`, error); if (attempt < 5) { @@ -1395,7 +1403,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { </div> )} <div className="chat-header"> - <h2>{this.userName()}'s AI Assistant</h2> + <h2>{StrCast(this.dataDoc.title) || `${this.userName()}'s AI Assistant`}</h2> <div className="font-size-control" onClick={this.toggleFontSizeModal}> {this.renderFontSizeIcon()} </div> diff --git a/src/client/views/nodes/chatbot/tools/TutorialTool.ts b/src/client/views/nodes/chatbot/tools/TutorialTool.ts index 08e4e1409..1624f0439 100644 --- a/src/client/views/nodes/chatbot/tools/TutorialTool.ts +++ b/src/client/views/nodes/chatbot/tools/TutorialTool.ts @@ -11,7 +11,9 @@ import { RichTextField } from '../../../../../fields/RichTextField'; import { DocumentViewInternal } from '../../DocumentView'; import { Docs } from '../../../../documents/Documents'; import { OpenWhere } from '../../OpenWhere'; -import { CollectionFreeFormView } from '../../../collections/collectionFreeForm'; +import { CollectionFreeFormView } from '../../../collections/collectionFreeForm/CollectionFreeFormView'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { Node as ProseMirrorNode } from 'prosemirror-model'; const generateTutorialNodeToolParams = [ { @@ -28,20 +30,26 @@ const generateTutorialNodeToolInfo: ToolInfo<typeof generateTutorialNodeToolPara parameterRules: generateTutorialNodeToolParams, citationRules: "No citation needed for this tool's output.", }; -const applyFormatting = (markdownText: string): { doc: any; plainText: string } => { + +interface FormattedDocument { + doc: ProseMirrorNode; + plainText: string; +} + +const applyFormatting = (markdownText: string): FormattedDocument => { const lines = markdownText.split('\n'); - const nodes: any[] = []; + const nodes: ProseMirrorNode[] = []; let plainText = ''; let i = 0; - let currentListItems: any[] = []; - let currentParagraph: any[] = []; - let currentOrderedListItems: any[] = []; + let currentListItems: ProseMirrorNode[] = []; + let currentParagraph: ProseMirrorNode[] = []; + let currentOrderedListItems: ProseMirrorNode[] = []; let inOrderedList = false; let inBulletList = false; - const processBoldText = (text: string) => { + const processBoldText = (text: string): ProseMirrorNode[] => { const boldRegex = /\*\*(.*?)\*\*/g; - const parts = []; + const parts: ProseMirrorNode[] = []; let lastIndex = 0; let match; @@ -58,7 +66,7 @@ const applyFormatting = (markdownText: string): { doc: any; plainText: string } return parts.length > 0 ? parts : [schema.text(text)]; }; - const flushListItems = () => { + const flushListItems = (): void => { if (currentListItems.length > 0) { nodes.push(schema.nodes.ordered_list.create({ mapStyle: 'bullet' }, currentListItems)); nodes.push(schema.nodes.paragraph.create()); @@ -73,14 +81,14 @@ const applyFormatting = (markdownText: string): { doc: any; plainText: string } } }; - const flushParagraph = () => { + const flushParagraph = (): void => { if (currentParagraph.length > 0) { nodes.push(schema.nodes.paragraph.create({}, currentParagraph)); currentParagraph = []; } }; - const processHeader = (line: string) => { + const processHeader = (line: string): boolean => { const headerMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headerMatch) { const level = Math.min(headerMatch[1].length, 6); // Cap at h6 @@ -138,12 +146,11 @@ const applyFormatting = (markdownText: string): { doc: any; plainText: string } }; export class GPTTutorialTool extends BaseTool<typeof generateTutorialNodeToolParams> { - private _createDocInDash: (doc: parsedDoc) => Doc | undefined; + private _docManager: AgentDocumentManager; - constructor(createDocInDash: (doc: parsedDoc) => Doc | undefined) { + constructor(docManager: AgentDocumentManager) { super(generateTutorialNodeToolInfo); - - this._createDocInDash = createDocInDash; + this._docManager = docManager; } async execute(args: ParametersType<typeof generateTutorialNodeToolParams>): Promise<Observation[]> { @@ -158,7 +165,7 @@ export class GPTTutorialTool extends BaseTool<typeof generateTutorialNodeToolPar // Build the ProseMirror‐in‐JSON + plain-text for RichTextField const rtfData = { - doc: (doc as any).toJSON ? (doc as any).toJSON() : doc, + doc: doc.toJSON ? doc.toJSON() : doc, selection: { type: 'text', anchor: 0, head: 0 }, storedMarks: [], }; diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 5d8583873..9b24219cf 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -3,10 +3,9 @@ import { Button, Dropdown, DropdownType, IconButton, isDark, Size, Type } from ' import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Flip } from 'react-awesome-reveal'; import { FaBug } from 'react-icons/fa'; -import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; -import { Doc, DocListCast, Opt, returnEmptyDoclist } from '../../../fields/Doc'; +import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; +import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; import { AclAdmin, DashVersion } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; @@ -28,10 +27,6 @@ import { ObservableReactComponent } from '../ObservableReactComponent'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import './TopBar.scss'; import { OpenWhere } from '../nodes/OpenWhere'; -import { ChatBox } from '../nodes/chatbot/chatboxcomponents/ChatBox'; -import { FieldViewProps } from '../nodes/FieldView'; -import { FocusViewOptions } from '../nodes/FocusViewOptions'; -import { PinProps } from '../PinFuncs'; import { Docs } from '../../documents/Documents'; /** @@ -90,7 +85,7 @@ export class TopBar extends ObservableReactComponent<object> { {Doc.ActiveDashboard ? ( <IconButton onClick={this.navigateToHome} - icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs.data_dashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'portrait' : 'home'} />} + icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs?.data_dashboards)?.some(dash => !DocListCast(Doc.MySharedDocs?.viewed)?.includes(dash)) ? 'portrait' : 'home'} />} color={this.color} background={this.backgroundColor} /> @@ -231,9 +226,11 @@ export class TopBar extends ObservableReactComponent<object> { val: 'tutorialagent', text: 'Ask AI!', onClick: () => { + const userEmail = ClientUtils.CurrentUserEmail(); + const userName = userEmail.split('@')[0]; const doc = Docs.Create.ChatDocument({ chat: 'Welcome to your help assistant for Dash. Ask any Dash-related questions to get started.', - title: 'Dash Documentation Assistant', + title: `${userName}'s Dash Help Assistant`, is_dash_doc_assistant: 'true', }); DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight); |
