diff options
author | sharkiecodes <lanyi_stroud@brown.edu> | 2025-07-10 10:22:43 -0400 |
---|---|---|
committer | sharkiecodes <lanyi_stroud@brown.edu> | 2025-07-10 10:22:43 -0400 |
commit | 5ad889e555a02ae63e59ee6c9766d57f829ff687 (patch) | |
tree | 13352c4a4d1dba30e85fbeed16debf333baee464 /src | |
parent | b5d53fc2dda0c2adcf0ccd388872faaca7606fa0 (diff) |
expanding
Diffstat (limited to 'src')
4 files changed, 217 insertions, 2 deletions
diff --git a/src/client/views/nodes/chatbot/tools/FilterDocTool.ts b/src/client/views/nodes/chatbot/tools/FilterDocTool.ts new file mode 100644 index 000000000..6be42d83b --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/FilterDocTool.ts @@ -0,0 +1,182 @@ +// FilterDocsTool.ts +import { BaseTool } from './BaseTool'; +import { Observation } from '../types/types'; +import { ParametersType, ToolInfo } from '../types/tool_types'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { + gptAPICall, + GPTCallType, + DescriptionSeperator, + DataSeperator +} from '../../../../apis/gpt/GPT'; +import { v4 as uuidv4 } from 'uuid'; +import { TagItem } from '../../../TagsView'; +import { DocumentView } from '../../DocumentView'; +import { Doc } from '../../../../../fields/Doc'; +import { Filter } from '../../../../util/reportManager/ReportManagerComponents'; + +const parameterRules = [ + { + name: 'filterCriteria', + type: 'string', + description: 'Natural-language criteria for choosing a subset of documents.', + required: true, + }, +] as const; + +const toolInfo: ToolInfo<typeof parameterRules> = { + name: 'filterDocs', + description: 'Filters documents based on user-specified natural-language criteria.', + parameterRules, + citationRules: + 'No citation needed for filtering operations.', +}; + +export class FilterDocsTool extends BaseTool<typeof parameterRules> { + private _docManager: AgentDocumentManager; + static ChatTag = '#chat'; // tag used by GPT popup to filter docs + // the parent Document (collection) that will be filtered + private _collectionView: DocumentView; + + constructor( + docManager: AgentDocumentManager, + collectionView: DocumentView + ) { + super(toolInfo); + this._docManager = docManager; + this._docManager.initializeFindDocsFreeform(); + this._collectionView = collectionView; + } + + async execute( + args: ParametersType<typeof parameterRules> + ): Promise<Observation[]> { + const chunkId = uuidv4(); + + try { + // 1) Build description→ID map & prompt blocks + const textToId = new Map<string, string>(); + const blocks: string[] = []; + + for (const id of this._docManager.docIds) { + // get a reliable human-readable description + const desc = ( + await this._docManager.getDocDescription(id) + ) + .replace(/\n/g, ' ') + .trim(); + + if (!desc) continue; + textToId.set(desc, id); + blocks.push(`${DescriptionSeperator}${desc}${DescriptionSeperator}`); + } + + const prompt = blocks.join(''); + + // 2) Ask GPT for subset + const raw = await gptAPICall( + args.filterCriteria, + GPTCallType.SUBSETDOCS, + prompt + ); + console.log('[FilterDocsTool] GPT response:', raw); + + // 3) Clear existing chat-filter tags/filters + const allDocs = this._docManager.docIds + .map((id) => this._docManager.getDocument(id)) + .filter((d): d is Doc => !!d); + allDocs.forEach((d) => { + // remove any prior ChatTag + TagItem.removeTagFromDoc(d, FilterDocsTool.ChatTag); + }); + // also remove the docFilter setting on the parent + Doc.setDocFilter( + this._collectionView.Document, + 'tags', + FilterDocsTool.ChatTag, + 'remove' + ); + + // 4) Parse GPT’s output, re-apply tag + docFilter + raw + .split(DescriptionSeperator) + .filter((blk) => blk.trim() !== '') + .map((blk) => blk.replace(/\n/g, ' ').trim()) + .forEach((blk) => { + // split on '>>>>>>' aka DataSeperator + const [descText, _extra] = blk.split(DataSeperator).map((s) => s.trim()); + const docId = textToId.get(descText); + if (!docId) { + console.warn('[FilterDocsTool] no match for', descText); + return; + } + const doc = this._docManager.getDocument(docId); + if (!doc) return; + + // add the special '#chat' tag: + TagItem.addTagToDoc(doc, FilterDocsTool.ChatTag); + }); + + // Finally, set the parent’s filter to **check** on that tag + Doc.setDocFilter( + this._collectionView.Document, + 'tags', + FilterDocsTool.ChatTag, + 'check' + ); + + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="filter_status"> +Filtered documents based on "${args.filterCriteria}". Only docs tagged "${FilterDocsTool.ChatTag}" will be shown. +</chunk>`, + }, + ]; + } catch (err) { + console.error('[FilterDocsTool] error', err); + return [ + { + type: 'text', + text: `<chunk chunk_id="${chunkId}" chunk_type="error"> +Filtering failed: ${err instanceof Error ? err.message : String(err)} +</chunk>`, + }, + ]; + } + } +} + +/* processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTDocCommand) => + undoable(() => { + switch (questionType) { // reset collection based on question typefc + case GPTDocCommand.Sort: + docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat; + break; + case GPTDocCommand.Filter: + docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, GPTPopup.ChatTag)); + break; + } // prettier-ignore + + gptOutput.split(DescriptionSeperator).filter(item => item.trim() !== '') // Split output into individual document contents + .map(docContentRaw => docContentRaw.replace(/\n/g, ' ').trim()) + .map(docContentRaw => ({doc: textToDocMap.get(docContentRaw.split(DataSeperator)[0]), data: docContentRaw.split(DataSeperator)[1] })) // the find the corresponding Doc using textToDoc map + .filter(({doc}) => doc).map(({doc, data}) => ({doc:doc!, data})) // filter out undefined values + .forEach(({doc, data}, index) => { + switch (questionType) { + case GPTDocCommand.Sort: + doc[ChatSortField] = index; + break; + case GPTDocCommand.AssignTags: + data && TagItem.addTagToDoc(doc, data.startsWith('#') ? data : '#'+data[0].toLowerCase()+data.slice(1) ); + break; + case GPTDocCommand.Filter: + TagItem.addTagToDoc(doc, GPTPopup.ChatTag); + Doc.setDocFilter(docView.Document, 'tags', GPTPopup.ChatTag, 'check'); + break; + } + }); // prettier-ignore + }, '')(); + + /** + * When in quiz mode, rando*/
\ No newline at end of file diff --git a/src/client/views/nodes/chatbot/tools/SortDocsTool.ts b/src/client/views/nodes/chatbot/tools/SortDocsTool.ts index 45d7b4f15..1944f0bc1 100644 --- a/src/client/views/nodes/chatbot/tools/SortDocsTool.ts +++ b/src/client/views/nodes/chatbot/tools/SortDocsTool.ts @@ -7,7 +7,7 @@ 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 = [ { diff --git a/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts b/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts index f025e95cd..78d9859b8 100644 --- a/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts +++ b/src/client/views/nodes/chatbot/tools/TakeQuizTool.ts @@ -17,7 +17,7 @@ const parameterRules = [ 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.', + 'Tests the user\'s knowledge and evaluates a user\'s answer for a randomly selected document.', parameterRules, citationRules: 'No citation needed for quiz operations.', }; diff --git a/src/client/views/nodes/chatbot/tools/ViewManipulator.ts b/src/client/views/nodes/chatbot/tools/ViewManipulator.ts new file mode 100644 index 000000000..67f183412 --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/ViewManipulator.ts @@ -0,0 +1,33 @@ + + + + processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTDocCommand) => + undoable(() => { + switch (questionType) { // reset collection based on question typefc + case GPTDocCommand.Sort: + docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat; + break; + case GPTDocCommand.Filter: + docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, GPTPopup.ChatTag)); + break; + } // prettier-ignore + + gptOutput.split(DescriptionSeperator).filter(item => item.trim() !== '') // Split output into individual document contents + .map(docContentRaw => docContentRaw.replace(/\n/g, ' ').trim()) + .map(docContentRaw => ({doc: textToDocMap.get(docContentRaw.split(DataSeperator)[0]), data: docContentRaw.split(DataSeperator)[1] })) // the find the corresponding Doc using textToDoc map + .filter(({doc}) => doc).map(({doc, data}) => ({doc:doc!, data})) // filter out undefined values + .forEach(({doc, data}, index) => { + switch (questionType) { + case GPTDocCommand.Sort: + doc[ChatSortField] = index; + break; + case GPTDocCommand.AssignTags: + data && TagItem.addTagToDoc(doc, data.startsWith('#') ? data : '#'+data[0].toLowerCase()+data.slice(1) ); + break; + case GPTDocCommand.Filter: + TagItem.addTagToDoc(doc, GPTPopup.ChatTag); + Doc.setDocFilter(docView.Document, 'tags', GPTPopup.ChatTag, 'check'); + break; + } + }); // prettier-ignore + }, '')(); |