aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/nodes/chatbot/tools/FilterDocTool.ts182
-rw-r--r--src/client/views/nodes/chatbot/tools/SortDocsTool.ts2
-rw-r--r--src/client/views/nodes/chatbot/tools/TakeQuizTool.ts2
-rw-r--r--src/client/views/nodes/chatbot/tools/ViewManipulator.ts33
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
+ }, '')();