aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-07-24 10:53:45 -0400
committerbobzel <zzzman@gmail.com>2025-07-24 10:53:45 -0400
commitee129da1ed03a9897cc282e52e0d31dee006983d (patch)
tree928182fcaee777609576c4eba91ab57ca3002035 /src
parent9c7ddca0782a1d8dd1a8ff35759321b867a2b175 (diff)
making filter sort and tag doc tools work the same way with docs within the collection and with improved formatting for openai.
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/gpt/GPT.ts30
-rw-r--r--src/client/views/nodes/chatbot/agentsystem/Agent.ts3
-rw-r--r--src/client/views/nodes/chatbot/tools/FilterDocsTool.ts11
-rw-r--r--src/client/views/nodes/chatbot/tools/SortDocsTool.ts175
-rw-r--r--src/client/views/nodes/chatbot/tools/TagDocsTool.ts231
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx10
6 files changed, 268 insertions, 192 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index c2b518178..83ead150b 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -10,6 +10,8 @@ export enum GPTDocCommand {
}
export const DescriptionSeperator = '======';
+export const DescStart = '<description>';
+export const DescEnd = '</description>';
export const DocSeperator = '------';
export const DataSeperator = '>>>>>>';
export enum TextClassifications {
@@ -62,14 +64,14 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
sort_docs: {
model: 'gpt-4o',
maxTokens: 2048,
- temp: 0.25,
+ temp: 0,
prompt: `The user is going to give you a list of descriptions.
- Each one is separated by '${DescriptionSeperator}' on either side.
- Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'.
- Sort them by the user's specifications.
- Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'.
+ Every description is enclosed within a ${DescStart} and ${DescEnd} tag.
+ Based on the question the user asks, sort the given descriptions making sure each description is treated as a single entity no matter what its content.
+ Do NOT omit any descriptions from the final sorted list.
+ Return the sorted list of descriptions using the same tag format you received them.
Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description).
- It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
+ It is VERY important that you format it exactly as described, ensuring the proper number (${DocSeperator.length} of each) of '${DocSeperator[0]}' and NO commas`,
},
edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
@@ -210,10 +212,10 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
maxTokens: 1024,
temp: 0,
prompt: `I'm going to give you a list of descriptions.
- Each one is separated by '${DescriptionSeperator}' on either side.
- Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'.
+ Every description is enclosed within a ${DescStart} and ${DescEnd} tag.
Based on the question/command the user asks, provide a tag label of the given descriptions that best matches the user's specifications.
- Format your response precisely as a single string that prints each description without including any '${DescriptionSeperator}', followed by '${DataSeperator}', followed by the tag label, followed by '${DescriptionSeperator}'.
+ Format you response by returnign each description with its tag label in the following format:
+ ${DescStart}description${DataSeperator}tag label${DescEnd}
Do not use any additional formatting marks or punctuation'.
Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description).
`,
@@ -223,12 +225,12 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
maxTokens: 1024,
temp: 0,
prompt: `I'm going to give you a list of descriptions.
- Each one is separated by '${DescriptionSeperator}' on either side.
- Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'.
+ Every description is enclosed within a ${DescStart} and ${DescEnd} tag.
Based on the question the user asks, provide a subset of the given descriptions that best matches the user's specifications.
- Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'.
+ Make sure each description is only in the list once.
+ Return the filtered list of descriptions using the same tag format you received them.
Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning in the 2nd person (and mention nothing about the formatting details given in this description).
- It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
+ It is VERY important that you format it exactly as described, ensuring the proper number (${DocSeperator.length}) of '${DocSeperator[0]}' and NO commas`,
}, //A description of a Chat Assistant, if present, should always be included in the subset.
doc_info: {
@@ -263,7 +265,7 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: s
}
if (lastCall === inputText && dontCache !== true && lastResp) return lastResp;
try {
- const usePrompt = prompt ? prompt + '.' + opts.prompt : opts.prompt;
+ const usePrompt = prompt ? opts.prompt + '.\n' + prompt : opts.prompt;
const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: usePrompt },
{ role: 'user', content: inputText },
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index 0f7738703..cf9b47fca 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -114,12 +114,11 @@ export class Agent {
fileNames: new FileNamesTool(this.vectorstore),
generateTutorialNode: new GPTTutorialTool(this._docManager),
sortDocs: new SortDocsTool(this._docManager, this.parentView),
- tagDocs: new TagDocsTool(this._docManager),
+ tagDocs: new TagDocsTool(this._docManager, this.parentView),
filterDocs: new FilterDocsTool(this._docManager, this.parentView),
takeQuiz: new TakeQuizTool(this._docManager),
canvasDocs: new CanvasDocsTool(),
uiControl: new UIControlTool(),
-
};
// Add the createNewTool after other tools are defined
diff --git a/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts b/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts
index 18e7481f5..730f91005 100644
--- a/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts
+++ b/src/client/views/nodes/chatbot/tools/FilterDocsTool.ts
@@ -3,12 +3,11 @@ import { BaseTool } from './BaseTool';
import { Observation } from '../types/types';
import { ParametersType, ToolInfo } from '../types/tool_types';
import { AgentDocumentManager } from '../utils/AgentDocumentManager';
-import { gptAPICall, GPTCallType, GPTDocCommand, DescriptionSeperator, DataSeperator, DocSeperator } from '../../../../apis/gpt/GPT';
+import { gptAPICall, GPTCallType, GPTDocCommand, DataSeperator, DocSeperator, DescStart, DescEnd } from '../../../../apis/gpt/GPT';
import { v4 as uuidv4 } from 'uuid';
import { TagItem } from '../../../TagsView';
import { DocumentView } from '../../DocumentView';
import { Doc } from '../../../../../fields/Doc';
-import { computed } from 'mobx';
const parameterRules = [
{
@@ -51,7 +50,7 @@ export class FilterDocsTool extends BaseTool<typeof parameterRules> {
Doc.getDescription(doc).then(text => {
const cleanText = text.replace(/\n/g, ' ').trim();
textToDocMap.set(cleanText, doc);
- return `${DescriptionSeperator}${cleanText}${DescriptionSeperator}`;
+ return `${DescStart}${cleanText}${DescEnd}`;
})
)
).then(docDescriptions => docDescriptions.join(''));
@@ -165,7 +164,7 @@ FilterDocsTool: No parent collection document found. Please ensure you're workin
const doc = this._docManager.getDocument(id);
if (doc) {
textToDocMap.set(desc, doc);
- blocks.push(`${DescriptionSeperator}${desc}${DescriptionSeperator}`);
+ blocks.push(`${DescStart}${desc}${DescEnd}`);
}
}
@@ -226,9 +225,9 @@ Please try rephrasing your filter criteria or check if the collection contains v
// Parse document descriptions from GPT response
const docBlocks = gptResponse
- .split(DescriptionSeperator)
+ .split(DescEnd)
.filter(block => block.trim() !== '')
- .map(block => block.replace(/\n/g, ' ').trim());
+ .map(block => block.replace(DescStart, '').replace(/\n/g, ' ').trim());
console.log(`[FilterDocsTool] Found ${docBlocks.length} document blocks in GPT response`);
diff --git a/src/client/views/nodes/chatbot/tools/SortDocsTool.ts b/src/client/views/nodes/chatbot/tools/SortDocsTool.ts
index 9eb0cf4d1..a1f43a8f2 100644
--- a/src/client/views/nodes/chatbot/tools/SortDocsTool.ts
+++ b/src/client/views/nodes/chatbot/tools/SortDocsTool.ts
@@ -2,97 +2,138 @@ 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 } from '../../../../apis/gpt/GPT';
+import { gptAPICall, GPTCallType, DescStart, DescEnd } 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 { Doc } from '../../../../../fields/Doc';
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.',
- 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 _collectionView: DocumentView;
+ private _docManager: AgentDocumentManager;
+ private _collectionView: DocumentView;
+ private _documentDescriptions: Promise<string> | undefined;
+
+ 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.initializeDocuments();
+ }
+
+ get TextToDocMap() {
+ // Use any type to avoid complex type checking while maintaining runtime safety
+ const childDocs = this._collectionView?.ComponentView?.hasChildDocs?.();
+ if (childDocs) {
+ const textToDocMap = new Map<string, Doc>();
+ try {
+ this._documentDescriptions = Promise.all(
+ childDocs.map((doc: Doc) =>
+ Doc.getDescription(doc).then(text => {
+ const cleanText = text.replace(/\n/g, ' ').trim();
+ textToDocMap.set(cleanText, doc);
+ return `${DescStart}${cleanText}${DescEnd}`;
+ })
+ )
+ ).then(docDescriptions => docDescriptions.join(''));
+ return textToDocMap;
+ } catch (error) {
+ console.warn('[SortDocsTool] Error initializing document context:', error);
+ }
+ }
+ return undefined;
+ }
+
+ async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> {
+ const chunkId = uuidv4();
+ let textToDocMap = await this.TextToDocMap;
+ await this._documentDescriptions;
+ let chunks: string;
- 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.initializeDocuments();
- }
+ if (textToDocMap && textToDocMap.size > 0 && this._documentDescriptions) {
+ console.log('[SortDocsTool] Using pre-computed document descriptions');
+ chunks = await this._documentDescriptions;
+ } else {
+ // Method 2: Build descriptions from scratch using docManager
+ console.log('[SortDocsTool] Building document descriptions from docManager');
+ textToDocMap = new Map<string, Doc>();
+ const blocks: string[] = [];
- async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> {
- const chunkId = uuidv4();
+ for (const id of this._docManager.docIds) {
+ const descRaw = await this._docManager.getDocDescription(id);
+ const desc = descRaw.replace(/\n/g, ' ').trim();
- // 1) gather metadata & build map from text→id
- const textToId = new Map<string, string>();
+ if (!desc) {
+ console.warn(`[SortDocsTool] Skipping document ${id} with empty description`);
+ continue;
+ }
- 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);
+ const doc = this._docManager.getDocument(id);
+ if (doc) {
+ textToDocMap.set(desc, doc);
+ blocks.push(`${DescStart}${desc}${DescEnd}`);
+ }
+ }
- // 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);
+ chunks = blocks.join('');
+ }
+ try {
+ // 2) call GPT to sort those chunks
+ const gptResponse = await gptAPICall(args.sortCriteria, GPTCallType.SORTDOCS, chunks);
+ console.log('GPT RESP:', gptResponse);
- // 4) write back the ordering
- sortedIds.forEach((docId, idx) => {
- this._docManager.editDocumentField(docId, ChatSortField, idx);
- });
+ // 3) parse & map back to IDs
+ const sortedIds = gptResponse
+ .split(DescEnd)
+ .filter(s => s.trim() !== '')
+ .map(s => s.replace(DescStart, '').replace(/\n/g, ' ').trim())
+ .map(s => textToDocMap.get(s)) // lookup in our map
+ .filter(doc => doc)
+ .map(doc => doc!);
- const fieldKey = this._collectionView.ComponentView!.fieldKey;
- this._collectionView.Document[ `${fieldKey}_sort` ] = docSortings.Chat;
+ // 4) write back the ordering
+ sortedIds.forEach((doc, idx) => {
+ doc[ChatSortField] = idx;
+ });
+ const fieldKey = this._collectionView.ComponentView!.fieldKey;
+ this._collectionView.Document[`${fieldKey}_sort`] = docSortings.Chat;
- return [
- {
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="sort_status">
+ 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">
+ },
+ ];
+ } 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 de824ec0d..69b303724 100644
--- a/src/client/views/nodes/chatbot/tools/TagDocsTool.ts
+++ b/src/client/views/nodes/chatbot/tools/TagDocsTool.ts
@@ -2,120 +2,155 @@ import { v4 as uuidv4 } from 'uuid';
import { Doc } from '../../../../../fields/Doc';
import { Id } from '../../../../../fields/FieldSymbols';
import { TagItem } from '../../../TagsView';
-import { gptAPICall, GPTCallType, DescriptionSeperator, DataSeperator } from '../../../../apis/gpt/GPT';
+import { gptAPICall, GPTCallType, DataSeperator, DescStart, DescEnd } from '../../../../apis/gpt/GPT';
import { ParametersType, ToolInfo } from '../types/tool_types';
import { Observation } from '../types/types';
import { BaseTool } from './BaseTool';
import { AgentDocumentManager } from '../utils/AgentDocumentManager';
+import { DocumentView } from '../../DocumentView';
const parameterRules = [
- {
- name: 'taggingCriteria',
- type: 'string',
- description: 'Natural language description of how to tag the documents (e.g., "tag by subject", "categorize by theme")',
- required: true,
- },
+ {
+ name: 'taggingCriteria',
+ type: 'string',
+ description: 'Natural language description of how to tag the documents (e.g., "tag by subject", "categorize by theme")',
+ required: true,
+ },
] as const;
const toolInfo: ToolInfo<typeof parameterRules> = {
- name: 'tagDocs',
- description: 'Tag documents in the collection based on natural language criteria. Uses GPT to analyze document content and apply appropriate tags.',
- parameterRules,
- citationRules: 'Citation not required for tagging operations.',
+ name: 'tagDocs',
+ description: 'Tag documents in the collection based on natural language criteria. Uses GPT to analyze document content and apply appropriate tags.',
+ parameterRules,
+ citationRules: 'Citation not required for tagging operations.',
};
export class TagDocsTool extends BaseTool<typeof parameterRules> {
- private _docManager: AgentDocumentManager;
-
- constructor(docManager: AgentDocumentManager) {
- super(toolInfo);
- this._docManager = docManager;
- this._docManager.initializeDocuments();
- }
-
- async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> {
- const chunkId = uuidv4();
-
- try {
- // Build the textToDocMap exactly like GPTPopup does
- this._docManager.initializeDocuments();
- const textToDocMap = new Map<string, Doc>();
- const descriptionsArray: string[] = [];
-
- // Build descriptions exactly like GPTPopup
- for (const id of this._docManager.docIds) {
- const doc = this._docManager.getDocument(id);
- if (!doc) continue;
-
- const desc = (await this._docManager.getDocDescription(id)).replace(/\n/g, ' ').trim();
- if (!desc) continue;
-
- textToDocMap.set(desc, doc);
- descriptionsArray.push(`${DescriptionSeperator}${desc}${DescriptionSeperator}`);
- }
-
- const promptDescriptions = descriptionsArray.join('');
- console.log('[TagDocsTool] Sending descriptions to GPT:', promptDescriptions.substring(0, 200) + '...');
-
- // Call GPT with the same method as GPTPopup
- const gptOutput = await gptAPICall(
- args.taggingCriteria,
- GPTCallType.TAGDOCS,
- promptDescriptions
- );
- console.log('[TagDocsTool] GPT raw:', gptOutput);
-
- // Use the same parsing logic as GPTPopup's processGptResponse for AssignTags
- const appliedTags: Record<string, string[]> = {};
-
- gptOutput
- .split(DescriptionSeperator)
- .filter(item => item.trim() !== '')
- .map(docContentRaw => docContentRaw.replace(/\n/g, ' ').trim())
- .map(docContentRaw => ({
- doc: textToDocMap.get(docContentRaw.split(DataSeperator)[0]),
- data: docContentRaw.split(DataSeperator)[1]
- }))
- .filter(({doc}) => doc)
- .map(({doc, data}) => ({doc: doc!, data}))
- .forEach(({doc, data}) => {
- if (data) {
- // Apply tag exactly like GPTPopup does
- const tag = data.startsWith('#') ? data : '#' + data[0].toLowerCase() + data.slice(1);
- TagItem.addTagToDoc(doc, tag);
-
- const docId = doc[Id] || 'unknown';
- appliedTags[docId] = appliedTags[docId] || [];
- appliedTags[docId].push(tag);
-
- console.log(`[TagDocsTool] Applied tag "${tag}" to document ${docId}`);
- }
- });
-
- // Build summary
- const summary = Object.entries(appliedTags)
- .map(([id, tags]) => `${id}: ${tags.join(', ')}`)
- .join('; ');
-
- return [
- {
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="tagging_status">
+ private _docManager: AgentDocumentManager;
+ private _collectionView: DocumentView;
+ private _documentDescriptions: Promise<string> | undefined;
+
+ constructor(docManager: AgentDocumentManager, collectionView: DocumentView) {
+ super(toolInfo);
+ this._docManager = docManager;
+ this._collectionView = collectionView;
+ this._docManager.initializeDocuments();
+ }
+
+ get TextToDocMap() {
+ // Use any type to avoid complex type checking while maintaining runtime safety
+ const childDocs = this._collectionView?.ComponentView?.hasChildDocs?.();
+ if (childDocs) {
+ const textToDocMap = new Map<string, Doc>();
+ try {
+ this._documentDescriptions = Promise.all(
+ childDocs.map((doc: Doc) =>
+ Doc.getDescription(doc).then(text => {
+ const cleanText = text.replace(/\n/g, ' ').trim();
+ textToDocMap.set(cleanText, doc);
+ return `${DescStart}${cleanText}${DescEnd}`;
+ })
+ )
+ ).then(docDescriptions => docDescriptions.join(''));
+ return textToDocMap;
+ } catch (error) {
+ console.warn('[TagDocsTool] Error initializing document context:', error);
+ }
+ }
+ return undefined;
+ }
+
+ async execute(args: ParametersType<typeof parameterRules>): Promise<Observation[]> {
+ const chunkId = uuidv4();
+
+ try {
+ // Build the textToDocMap exactly like GPTPopup does
+ let textToDocMap = await this.TextToDocMap;
+ await this._documentDescriptions;
+ let promptDescriptions: string;
+
+ if (textToDocMap && textToDocMap.size > 0 && this._documentDescriptions) {
+ console.log('[TagDocsTool] Using pre-computed document descriptions');
+ promptDescriptions = await this._documentDescriptions;
+ } else {
+ // Method 2: Build descriptions from scratch using docManager
+ console.log('[TagDocsTool] Building document descriptions from docManager');
+ textToDocMap = new Map<string, Doc>();
+ const blocks: string[] = [];
+
+ for (const id of this._docManager.docIds) {
+ const descRaw = await this._docManager.getDocDescription(id);
+ const desc = descRaw.replace(/\n/g, ' ').trim();
+
+ if (!desc) {
+ console.warn(`[TagDocsTool] Skipping document ${id} with empty description`);
+ continue;
+ }
+
+ const doc = this._docManager.getDocument(id);
+ if (doc) {
+ textToDocMap.set(desc, doc);
+ blocks.push(`${DescStart}${desc}${DescEnd}`);
+ }
+ }
+
+ promptDescriptions = blocks.join('');
+ }
+ // Call GPT with the same method as GPTPopup
+ const gptOutput = await gptAPICall(args.taggingCriteria, GPTCallType.TAGDOCS, promptDescriptions);
+ console.log('[TagDocsTool] GPT raw:', gptOutput);
+
+ // Use the same parsing logic as GPTPopup's processGptResponse for AssignTags
+ const appliedTags: Record<string, string[]> = {};
+
+ gptOutput
+ .split(DescEnd)
+ .filter(item => item.trim() !== '')
+ .map(docContentRaw => docContentRaw.replace(DescStart, '').replace(/\n/g, ' ').trim())
+ .map(docContentRaw => ({
+ doc: textToDocMap.get(docContentRaw.split(DataSeperator)[0]),
+ data: docContentRaw.split(DataSeperator)[1],
+ }))
+ .filter(({ doc }) => doc)
+ .map(({ doc, data }) => ({ doc: doc!, data }))
+ .forEach(({ doc, data }) => {
+ if (data) {
+ // Apply tag exactly like GPTPopup does
+ const tag = data.startsWith('#') ? data : '#' + data[0].toLowerCase() + data.slice(1);
+ TagItem.addTagToDoc(doc, tag);
+
+ const docId = doc[Id] || 'unknown';
+ appliedTags[docId] = appliedTags[docId] || [];
+ appliedTags[docId].push(tag);
+
+ console.log(`[TagDocsTool] Applied tag "${tag}" to document ${docId}`);
+ }
+ });
+
+ // Build summary
+ const summary = Object.entries(appliedTags)
+ .map(([id, tags]) => `${id}: ${tags.join(', ')}`)
+ .join('; ');
+
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="tagging_status">
Successfully tagged ${Object.keys(appliedTags).length} documents based on "${args.taggingCriteria}".
Tags applied: ${summary || '(none)'}
</chunk>`,
- },
- ];
- } catch (err) {
- console.error('[TagDocsTool] error:', err);
- return [
- {
- type: 'text',
- text: `<chunk chunk_id="${chunkId}" chunk_type="error">
+ },
+ ];
+ } catch (err) {
+ console.error('[TagDocsTool] error:', err);
+ return [
+ {
+ type: 'text',
+ text: `<chunk chunk_id="${chunkId}" chunk_type="error">
Tagging failed: ${err instanceof Error ? err.message : String(err)}
</chunk>`,
- },
- ];
+ },
+ ];
+ }
}
- }
}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 65be98c83..23bbe3577 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -14,7 +14,7 @@ import { NumCast, StrCast } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
import { Upload } from '../../../../server/SharedMediaTypes';
import { Networking } from '../../../Network';
-import { DataSeperator, DescriptionSeperator, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { DataSeperator, DescEnd, DescStart, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
import { DocUtils } from '../../../documents/DocUtils';
import { Docs } from '../../../documents/Documents';
import { SettingsManager } from '../../../util/SettingsManager';
@@ -93,8 +93,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
Doc.getDescription(doc).then(text => text.replace(/\n/g, ' ').trim())
- .then(text => this._textToDocMap.set(text, doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
- )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
+ .then(text => this._textToDocMap.set(text, doc) && `${DescStart}${text}${DescEnd}`)
+ )).then(docDescriptions => docDescriptions.join('')); // prettier-ignore
this._documentDescriptions.then(descs => {
console.log(descs);
});
@@ -148,8 +148,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
break;
} // prettier-ignore
- gptOutput.split(DescriptionSeperator).filter(item => item.trim() !== '') // Split output into individual document contents
- .map(docContentRaw => docContentRaw.replace(/\n/g, ' ').trim())
+ gptOutput.split(DescEnd).filter(item => item.trim() !== '') // Split output into individual document contents
+ .map(docContentRaw => docContentRaw.replace(DescStart,"").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) => {