diff options
Diffstat (limited to 'src')
6 files changed, 274 insertions, 59 deletions
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts index 3acdc6aa8..d7aee51e6 100644 --- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts +++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts @@ -30,6 +30,7 @@ import { CreateNewTool } from '../tools/CreateNewTool'; import { SortDocsTool} from '../tools/SortDocsTool'; import { TagDocsTool } from '../tools/TagDocsTool'; import { GPTTutorialTool } from '../tools/TutorialTool'; +import { DocumentView } from '../../DocumentView'; dotenv.config(); @@ -53,6 +54,7 @@ export class Agent { private tools: Record<string, BaseTool<ReadonlyArray<Parameter>>>; private _docManager: AgentDocumentManager; private is_dash_doc_assistant: boolean; + private parentView: DocumentView; // 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 @@ -83,6 +85,7 @@ export class Agent { // Initialize OpenAI client with API key from environment this.client = new OpenAI({ apiKey: process.env.OPENAI_KEY, dangerouslyAllowBrowser: true }); this.vectorstore = _vectorstore; + this.parentView = docManager.parentViewDocument; // Get the parent DocumentView this._history = history; this._csvData = csvData; this._docManager = docManager; @@ -106,7 +109,7 @@ export class Agent { fileContent: new FileContentTool(this.vectorstore), fileNames: new FileNamesTool(this.vectorstore), generateTutorialNode: new GPTTutorialTool(this._docManager), - sortDocs: new SortDocsTool(this._docManager), + sortDocs: new SortDocsTool(this._docManager, this.parentView), tagDocs: new TagDocsTool(this._docManager), }; @@ -346,13 +349,25 @@ export class Agent { console.log(`Thought: ${stage[key]}`); this.processingNumber++; } else if (key === 'action') { + // Handle action stage currentAction = stage[key] as string; console.log(`Action: ${currentAction}`); // Check both static tools and dynamic registry const tool = this.tools[currentAction] || this.dynamicToolRegistry.get(currentAction); - + if (currentAction === 'noTool') { + // Immediately ask for clarification in plain text, not as a tool prompt + this.interMessages.push({ + role: 'user', + content: `<stage number="${i+1}" role="assistant"> + <answer> + I’m not sure what you’d like me to do. Could you clarify your request? + </answer> + </stage>` + }); + break; + } if (tool) { // Prepare the next action based on the current tool const nextPrompt = [ diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index 636b77b38..f84a4cd2a 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -111,8 +111,16 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { super(props); makeObservable(this); + // At mount time, find the DocumentView whose .Document is the collection container. + const parentView = DocumentView.Selected().lastElement(); + if (!parentView) { + console.warn("GPT ChatBox not inside a DocumentView – cannot sort."); + } + + + this.messagesRef = React.createRef(); - this.docManager = new AgentDocumentManager(this); + this.docManager = new AgentDocumentManager(this, parentView); // Initialize OpenAI client this.initializeOpenAI(); @@ -154,6 +162,25 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } ); + /* + reaction( + () => ({ selDoc: DocumentView.Selected().lastElement(), visible: SnappingManager.ChatVisible }), + ({ selDoc, visible }) => { + const hasChildDocs = visible && selDoc?.ComponentView?.hasChildDocs; + if (hasChildDocs) { + this._textToDocMap.clear(); + this.setCollectionContext(selDoc.Document); + this.onGptResponse = (sortResult: string, questionType: GPTDocCommand) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType); + this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs()); + this._documentDescriptions = Promise.all(hasChildDocs().map(doc => + Doc.getDescription(doc).then(text => this._textToDocMap.set(text.replace(/\n/g, ' ').trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`) + )).then(docDescriptions => docDescriptions.join()); // prettier-ignore + } + }, + { fireImmediately: true } + ); + }*/ + // Initialize font size from saved preference this.initFontSize(); } diff --git a/src/client/views/nodes/chatbot/tools/SortDocsTool.ts b/src/client/views/nodes/chatbot/tools/SortDocsTool.ts index 741a8f3ce..45d7b4f15 100644 --- a/src/client/views/nodes/chatbot/tools/SortDocsTool.ts +++ b/src/client/views/nodes/chatbot/tools/SortDocsTool.ts @@ -2,66 +2,97 @@ import { BaseTool } from './BaseTool'; import { Observation } from '../types/types'; import { ParametersType, ToolInfo } from '../types/tool_types'; import { AgentDocumentManager } from '../utils/AgentDocumentManager'; -import { gptAPICall, GPTCallType } from '../../../../apis/gpt/GPT'; +import { gptAPICall, GPTCallType, DescriptionSeperator } from '../../../../apis/gpt/GPT'; import { ChatSortField } from '../../../collections/CollectionSubView'; import { v4 as uuidv4 } from 'uuid'; +import { DocumentView } from '../../DocumentView'; +import { docSortings } from '../../../collections/CollectionSubView'; +import { collect } from '@turf/turf'; const parameterRules = [ - { - name: 'sortCriteria', - type: 'string', - description: 'Criteria provided by the user to sort the documents.', - required: true, - }, + { + name: 'sortCriteria', + type: 'string', + description: 'Criteria provided by the user to sort the documents.', + required: true, + }, ] as const; const toolInfo: ToolInfo<typeof parameterRules> = { - name: 'sortDocs', - description: - 'Sorts documents within the current Dash environment based on user-specified criteria. Provide clear sorting criteria, such as by date, title, relevance, or custom metadata fields.', - parameterRules, - citationRules: 'No citation needed for sorting operations.', + name: 'sortDocs', + description: + 'Sorts documents within the current Dash environment based on user-specified criteria.', + parameterRules, + citationRules: 'No citation needed for sorting operations.', }; export class SortDocsTool extends BaseTool<typeof parameterRules> { - private _docManager: AgentDocumentManager; + private _docManager: AgentDocumentManager; + private _collectionView: DocumentView; - constructor(docManager: AgentDocumentManager) { - super(toolInfo); - this._docManager = docManager; - this._docManager.initializeFindDocsFreeform(); - } + constructor(docManager: AgentDocumentManager, collectionView: DocumentView) + { + super(toolInfo); + // Grab the parent collection’s DocumentView (the ChatBox container) + // We assume the ChatBox itself is currently selected in its parent view. + this._collectionView = collectionView; + this._docManager = docManager; + this._docManager.initializeFindDocsFreeform(); + } + + async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> { + const chunkId = uuidv4(); + + // 1) gather metadata & build map from text→id + const textToId = new Map<string, string>(); + + const chunks = (await Promise.all( + this._docManager.docIds.map(async id => { + const text = await this._docManager.getDocDescription(id); + textToId.set(text,id); + return DescriptionSeperator + text + DescriptionSeperator; + }) + )) + .join(''); + try { + // 2) call GPT to sort those chunks + const gptResponse = await gptAPICall(args.sortCriteria, GPTCallType.SORTDOCS, chunks); + console.log('GPT RESP:', gptResponse); - async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> { - const chunkId = uuidv4(); - try { - const docs = this._docManager.docIds.map((id) => this._docManager.extractDocumentMetadata(id)); + // 3) parse & map back to IDs + const sortedIds = gptResponse + .split(DescriptionSeperator) + .filter(s => s.trim() !== '') + .map(s => s.replace(/\n/g, ' ').trim()) + .map(s => textToId.get(s)) // lookup in our map + .filter((id): id is string => !!id); - const descriptions = docs - .filter((doc): doc is NonNullable<typeof doc> => doc !== null) - .map( - (doc) => `${doc.id}: ${doc.title} - ${doc.fields.layout.summary || ''}` - ) - .join('\n'); + // 4) write back the ordering + sortedIds.forEach((docId, idx) => { + this._docManager.editDocumentField(docId, ChatSortField, idx); + }); - const sortedIdsResponse = await gptAPICall(args.sortCriteria, GPTCallType.SORTDOCS, descriptions); - const sortedIds = sortedIdsResponse.trim().split('\n'); - console.log(sortedIdsResponse); - console.log(sortedIds); + const fieldKey = this._collectionView.ComponentView!.fieldKey; + this._collectionView.Document[ `${fieldKey}_sort` ] = docSortings.Chat; - sortedIds.forEach((id, index) => { - this._docManager.editDocumentField(id, ChatSortField, index); - }); - return [{ - type: 'text', - text: `<chunk chunk_id="${chunkId}" chunk_type="sort_status">Successfully sorted ${sortedIds.length} documents based on "${args.sortCriteria}".</chunk>`, - }]; - } catch (error) { - return [{ - type: 'text', - text: `<chunk chunk_id="${chunkId}" chunk_type="error">Sorting failed: ${error instanceof Error ? error.message : String(error)}</chunk>`, - }]; - } + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="sort_status"> +Successfully sorted ${sortedIds.length} documents by "${args.sortCriteria}". +</chunk>`, + }, + ]; + } catch (err) { + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Sorting failed: ${err instanceof Error ? err.message : err} +</chunk>`, + }, + ]; } + } } diff --git a/src/client/views/nodes/chatbot/tools/TagDocsTool.ts b/src/client/views/nodes/chatbot/tools/TagDocsTool.ts index 6b4693279..75f476348 100644 --- a/src/client/views/nodes/chatbot/tools/TagDocsTool.ts +++ b/src/client/views/nodes/chatbot/tools/TagDocsTool.ts @@ -34,25 +34,30 @@ async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[ const chunkId = uuidv4(); try { // Build a single string of all docs in the EXACT same format as GPTPopup does: - const descriptions = this._docManager.docIds - .map(id => this._docManager.extractDocumentMetadata(id)) - .filter(m => m !== null) - .map(m => `${m!.id}${DataSeperator}${m!.title}`) - .map(str => `${DescriptionSeperator}${str}${DescriptionSeperator}`) - .join(''); - + + // 1) gather metadata & build map from text→id + const textToId = new Map<string, string>(); + //make this a general UTIL + const descriptions = (await Promise.all( + this._docManager.docIds.map(async id => { + const text = await this._docManager.getDocDescription(id); + textToId.set(text,id); + return DescriptionSeperator + text + DescriptionSeperator; + }) + )) + .join(''); // Call GPT const raw = await gptAPICall( args.taggingCriteria, GPTCallType.TAGDOCS, descriptions ); - + console.log('TAG RESP:', raw); // Prepare to collect what we actually applied const appliedTags: Record<string, string[]> = {}; // Parse and apply tags exactly like GPTPopup - raw + /*raw .split(DescriptionSeperator) // 1) break into blocks at "======" .filter(block => block.trim() !== '') // 2) drop empty .map(block => block.replace(/\n/g, ' ').trim()) // 3) flatten & trim @@ -76,7 +81,36 @@ async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[ // Record for our summary appliedTags[id] = normalized; - }); + });*/ + + raw + .split(DescriptionSeperator) // 1) Split into “blocks” + .filter(item => item.trim() !== '') // 2) Drop empty blocks + .map(block => block.replace(/\n/g, ' ').trim()) // 3) Flatten & trim + .map(block => { + // 4) block looks like: "docId>>>>>>tag1, tag2" + const [idPart, tagsPart] = block.split(DataSeperator); + return { id: idPart.trim(), data: (tagsPart || '').trim() }; + }) + .filter(({ id, data }) => !!id && !!data) // 5) Keep only valid pairs + .forEach(({ id, data }) => { + // 6) Lookup doc by ID + const doc = this._docManager.getDocument(id); + if (!doc) return; + + // 7) Only in the AssignTags branch do we add tags + // And GPTPopup normalizes by lowercasing first letter if no '#' + const tagToApply = data.startsWith('#') + ? data + : '#' + data[0].toLowerCase() + data.slice(1); + + TagItem.addTagToDoc(doc, tagToApply); + + // 8) Record for summary + if (!appliedTags[id]) appliedTags[id] = []; + appliedTags[id].push(tagToApply); + }); + // Build single observation with summary const summary = Object.entries(appliedTags) diff --git a/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts b/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts new file mode 100644 index 000000000..f025e95cd --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts @@ -0,0 +1,88 @@ +import { BaseTool } from './BaseTool'; +import { Observation } from '../types/types'; +import { ParametersType, ToolInfo } from '../types/tool_types'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { GPTCallType, gptAPICall } from '../../../../apis/gpt/GPT'; +import { v4 as uuidv4 } from 'uuid'; + +const parameterRules = [ + { + name: 'userAnswer', + type: 'string', + description: 'User-provided answer to the quiz question.', + required: true, + }, +] as const; + +const toolInfo: ToolInfo<typeof parameterRules> = { + name: 'takeQuiz', + description: + 'Evaluates a user\'s answer for a randomly selected document using GPT, mirroring GPTPopup\'s quiz functionality.', + parameterRules, + citationRules: 'No citation needed for quiz operations.', +}; + +export class TakeQuizTool extends BaseTool<typeof parameterRules> { + private _docManager: AgentDocumentManager; + + constructor(docManager: AgentDocumentManager) { + super(toolInfo); + this._docManager = docManager; + this._docManager.initializeFindDocsFreeform(); + } + + private async generateRubric(docId: string, description: string): Promise<string> { + const docMeta = this._docManager.extractDocumentMetadata(docId); + if (docMeta && docMeta.fields.layout.gptRubric) { + return docMeta.fields.layout.gptRubric; + } else { + const rubric = await gptAPICall(description, GPTCallType.MAKERUBRIC); + if (rubric) { + await this._docManager.editDocumentField(docId, 'layout.gptRubric', rubric); + } + return rubric || ''; + } + } + + async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> { + const chunkId = uuidv4(); + + try { + const allDocIds = this._docManager.docIds; + const randomDocId = allDocIds[Math.floor(Math.random() * allDocIds.length)]; + const docMeta = this._docManager.extractDocumentMetadata(randomDocId); + + if (!docMeta) throw new Error('Randomly selected document metadata is undefined'); + + const description = docMeta.fields.layout.description.replace(/\n/g, ' ').trim(); + const rubric = await this.generateRubric(randomDocId, description); + + const prompt = ` + Question: ${description}; + UserAnswer: ${args.userAnswer}; + Rubric: ${rubric} + `; + + const evaluation = await gptAPICall(prompt, GPTCallType.QUIZDOC); + + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="quiz_result"> +Evaluation result: ${evaluation || 'GPT provided no answer'}. +Document evaluated: "${docMeta.title}" +</chunk>`, + }, + ]; + } catch (err) { + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Quiz evaluation failed: ${err instanceof Error ? err.message : err} +</chunk>`, + }, + ]; + } + } +} diff --git a/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts b/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts index dcb708450..857cc859d 100644 --- a/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts +++ b/src/client/views/nodes/chatbot/utils/AgentDocumentManager.ts @@ -27,6 +27,7 @@ interface AgentDocument { export class AgentDocumentManager { @observable private documentsById: ObservableMap<string, AgentDocument>; private chatBox: ChatBox; + private parentView : DocumentView private chatBoxDocument: Doc | null = null; private fieldMetadata: Record<string, any> = {}; // bcz: CHANGE any to a proper type! @observable private simplifiedChunks: ObservableMap<string, SimplifiedChunk>; @@ -35,8 +36,9 @@ export class AgentDocumentManager { * Creates a new DocumentManager * @param templateDocument The document that serves as a template for new documents */ - constructor(chatBox: ChatBox) { + constructor(chatBox: ChatBox, parentView : DocumentView) { makeObservable(this); + this.parentView = parentView; const agentDoc = DocCast(chatBox.Document.agentDocument) ?? new Doc(); const chunk_simpl = DocCast(agentDoc.chunk_simpl) ?? new Doc(); @@ -164,6 +166,10 @@ export class AgentDocumentManager { } } + public get parentViewDocument(): DocumentView { + return this.parentView; + } + /** * Process a document by ensuring it has an ID and adding it to the appropriate collections * @param doc The document to process @@ -1011,6 +1017,20 @@ export class AgentDocumentManager { const docInfo = this.documentsById.get(docId); return docInfo?.dataDoc; } + + // In AgentDocumentManager + private descriptionCache = new Map<string,string>(); + + public async getDocDescription(id: string): Promise<string> { + if (!this.descriptionCache.has(id)) { + const doc = this.getDocument(id)!; + const desc = await Doc.getDescription(doc); + this.descriptionCache.set(id, desc.replace(/\n/g,' ').trim()); + } + return this.descriptionCache.get(id)!; + } + + /** * Adds simplified chunks to a document for citation handling * @param doc The document to add simplified chunks to |