diff options
Diffstat (limited to 'src/client/views/nodes/chatbot/tools')
7 files changed, 751 insertions, 734 deletions
diff --git a/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts b/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts deleted file mode 100644 index 754d230c8..000000000 --- a/src/client/views/nodes/chatbot/tools/CreateAnyDocTool.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { toLower } from 'lodash'; -import { Doc } from '../../../../../fields/Doc'; -import { Id } from '../../../../../fields/FieldSymbols'; -import { DocumentOptions } from '../../../../documents/Documents'; -import { parsedDoc } from '../chatboxcomponents/ChatBox'; -import { ParametersType, ToolInfo } from '../types/tool_types'; -import { Observation } from '../types/types'; -import { BaseTool } from './BaseTool'; -import { supportedDocTypes } from './CreateDocumentTool'; - -const standardOptions = ['title', 'backgroundColor']; -/** - * Description of document options and data field for each type. - */ -const documentTypesInfo: { [key in supportedDocTypes]: { options: string[]; dataDescription: string } } = { - [supportedDocTypes.flashcard]: { - options: [...standardOptions, 'fontColor', 'text_align'], - dataDescription: 'an array of two strings. the first string contains a question, and the second string contains an answer', - }, - [supportedDocTypes.text]: { - options: [...standardOptions, 'fontColor', 'text_align'], - dataDescription: 'The text content of the document.', - }, - [supportedDocTypes.html]: { - options: [], - dataDescription: 'The HTML-formatted text content of the document.', - }, - [supportedDocTypes.equation]: { - options: [...standardOptions, 'fontColor'], - dataDescription: 'The equation content as a string.', - }, - [supportedDocTypes.functionplot]: { - options: [...standardOptions, 'function_definition'], - dataDescription: 'The function definition(s) for plotting. Provide as a string or array of function definitions.', - }, - [supportedDocTypes.dataviz]: { - options: [...standardOptions, 'chartType'], - dataDescription: 'A string of comma-separated values representing the CSV data.', - }, - [supportedDocTypes.notetaking]: { - options: standardOptions, - dataDescription: 'The initial content or structure for note-taking.', - }, - [supportedDocTypes.rtf]: { - options: standardOptions, - dataDescription: 'The rich text content in RTF format.', - }, - [supportedDocTypes.image]: { - options: standardOptions, - dataDescription: 'The image content as an image file URL.', - }, - [supportedDocTypes.pdf]: { - options: standardOptions, - dataDescription: 'the pdf content as a PDF file url.', - }, - [supportedDocTypes.audio]: { - options: standardOptions, - dataDescription: 'The audio content as a file url.', - }, - [supportedDocTypes.video]: { - options: standardOptions, - dataDescription: 'The video content as a file url.', - }, - [supportedDocTypes.message]: { - options: standardOptions, - dataDescription: 'The message content of the document.', - }, - [supportedDocTypes.diagram]: { - options: ['title', 'backgroundColor'], - dataDescription: 'diagram content as a text string in Mermaid format.', - }, - [supportedDocTypes.script]: { - options: ['title', 'backgroundColor'], - dataDescription: 'The compilable JavaScript code. Use this for creating scripts.', - }, -}; - -const createAnyDocumentToolParams = [ - { - name: 'document_type', - type: 'string', - description: `The type of the document to create. Supported types are: ${Object.values(supportedDocTypes).join(', ')}`, - required: true, - }, - { - name: 'data', - type: 'string', - description: 'The content or data of the document. The exact format depends on the document type.', - required: true, - }, - { - name: 'options', - type: 'string', - required: false, - description: `A JSON string representing the document options. Available options depend on the document type. For example: - ${Object.entries(documentTypesInfo).map( ([doc_type, info]) => ` -- For '${doc_type}' documents, options include: ${info.options.join(', ')}`) - .join('\n')}`, // prettier-ignore - }, -] as const; - -type CreateAnyDocumentToolParamsType = typeof createAnyDocumentToolParams; - -const createAnyDocToolInfo: ToolInfo<CreateAnyDocumentToolParamsType> = { - name: 'createAnyDocument', - description: - `Creates any type of document with the provided options and data. - Supported document types are: ${Object.values(supportedDocTypes).join(', ')}. - dataviz is a csv table tool, so for CSVs, use dataviz. Here are the options for each type: - <supported_document_types>` + - Object.entries(documentTypesInfo) - .map( - ([doc_type, info]) => - `<document_type name="${doc_type}"> - <data_description>${info.dataDescription}</data_description> - <options>` + - info.options.map(option => `<option>${option}</option>`).join('\n') + - `</options> - </document_type>` - ) - .join('\n') + - `</supported_document_types>`, - parameterRules: createAnyDocumentToolParams, - citationRules: 'No citation needed.', -}; - -export class CreateAnyDocumentTool extends BaseTool<CreateAnyDocumentToolParamsType> { - private _addLinkedDoc: (doc: parsedDoc) => Doc | undefined; - - constructor(addLinkedDoc: (doc: parsedDoc) => Doc | undefined) { - super(createAnyDocToolInfo); - this._addLinkedDoc = addLinkedDoc; - } - - async execute(args: ParametersType<CreateAnyDocumentToolParamsType>): Promise<Observation[]> { - try { - const documentType = toLower(args.document_type) as unknown as supportedDocTypes; - const info = documentTypesInfo[documentType]; - - if (info === undefined) { - throw new Error(`Unsupported document type: ${documentType}. Supported types are: ${Object.values(supportedDocTypes).join(', ')}.`); - } - - if (!args.data) { - throw new Error(`Data is required for ${documentType} documents. ${info.dataDescription}`); - } - - const options: DocumentOptions = !args.options ? {} : JSON.parse(args.options); - - // Call the function to add the linked document (add default title that can be overriden if set in options) - const doc = this._addLinkedDoc({ doc_type: documentType, data: args.data, title: `New ${documentType.charAt(0).toUpperCase() + documentType.slice(1)} Document`, ...options }); - - return [{ type: 'text', text: `Created ${documentType} document with ID ${doc?.[Id]}.` }]; - } catch (error) { - return [{ type: 'text', text: 'Error creating document: ' + (error as Error).message }]; - } - } -} diff --git a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts deleted file mode 100644 index 284879a4a..000000000 --- a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts +++ /dev/null @@ -1,497 +0,0 @@ -import { BaseTool } from './BaseTool'; -import { Observation } from '../types/types'; -import { Parameter, ParametersType, ToolInfo } from '../types/tool_types'; -import { parsedDoc } from '../chatboxcomponents/ChatBox'; -import { CollectionViewType } from '../../../../documents/DocumentTypes'; - -/** - * List of supported document types that can be created via text LLM. - */ -export enum supportedDocTypes { - flashcard = 'flashcard', - text = 'text', - html = 'html', - equation = 'equation', - functionplot = 'functionplot', - dataviz = 'dataviz', - notetaking = 'notetaking', - audio = 'audio', - video = 'video', - pdf = 'pdf', - rtf = 'rtf', - message = 'message', - collection = 'collection', - image = 'image', - deck = 'deck', - web = 'web', - comparison = 'comparison', - diagram = 'diagram', - script = 'script', -} -/** - * Tthe CreateDocTool class is responsible for creating - * documents of various types (e.g., text, flashcards, collections) and organizing them in a - * structured manner. The tool supports creating dashboards with diverse document types and - * ensures proper placement of documents without overlap. - */ - -// Example document structure for various document types -const example = [ - { - doc_type: supportedDocTypes.equation, - title: 'quadratic', - data: 'x^2 + y^2 = 3', - _width: 300, - _height: 300, - x: 0, - y: 0, - }, - { - doc_type: supportedDocTypes.collection, - title: 'Advanced Biology', - data: [ - { - doc_type: supportedDocTypes.text, - title: 'Cell Structure', - data: 'Cells are the basic building blocks of all living organisms.', - _width: 300, - _height: 300, - x: 500, - y: 0, - }, - ], - backgroundColor: '#00ff00', - _width: 600, - _height: 600, - x: 600, - y: 0, - type_collection: 'tree', - }, - { - doc_type: supportedDocTypes.image, - title: 'experiment', - data: 'https://plus.unsplash.com/premium_photo-1694819488591-a43907d1c5cc?q=80&w=2628&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', - _width: 300, - _height: 300, - x: 600, - y: 300, - }, - { - doc_type: supportedDocTypes.deck, - title: 'Chemistry', - data: [ - { - doc_type: supportedDocTypes.flashcard, - title: 'Photosynthesis', - data: [ - { - doc_type: supportedDocTypes.text, - title: 'front_Photosynthesis', - data: 'What is photosynthesis?', - _width: 300, - _height: 300, - x: 100, - y: 600, - }, - { - doc_type: supportedDocTypes.text, - title: 'back_photosynthesis', - data: 'The process by which plants make food.', - _width: 300, - _height: 300, - x: 100, - y: 700, - }, - ], - backgroundColor: '#00ff00', - _width: 300, - _height: 300, - x: 300, - y: 1000, - }, - { - doc_type: supportedDocTypes.flashcard, - title: 'Photosynthesis', - data: [ - { - doc_type: supportedDocTypes.text, - title: 'front_Photosynthesis', - data: 'What is photosynthesis?', - _width: 300, - _height: 300, - x: 200, - y: 800, - }, - { - doc_type: supportedDocTypes.text, - title: 'back_photosynthesis', - data: 'The process by which plants make food.', - _width: 300, - _height: 300, - x: 100, - y: -100, - }, - ], - backgroundColor: '#00ff00', - _width: 300, - _height: 300, - x: 10, - y: 70, - }, - ], - backgroundColor: '#00ff00', - _width: 600, - _height: 600, - x: 200, - y: 800, - }, - { - doc_type: supportedDocTypes.web, - title: 'Brown University Wikipedia', - data: 'https://en.wikipedia.org/wiki/Brown_University', - _width: 300, - _height: 300, - x: 1000, - y: 2000, - }, - { - doc_type: supportedDocTypes.comparison, - title: 'WWI vs. WWII', - data: [ - { - doc_type: supportedDocTypes.text, - title: 'WWI', - data: 'From 1914 to 1918, fighting took place across several continents, at sea and, for the first time, in the air.', - _width: 300, - _height: 300, - x: 100, - y: 100, - }, - { - doc_type: supportedDocTypes.text, - title: 'WWII', - data: 'A devastating global conflict spanning from 1939 to 1945, saw the Allied powers fight against the Axis powers.', - _width: 300, - _height: 300, - x: 100, - y: 100, - }, - ], - _width: 300, - _height: 300, - x: 100, - y: 100, - }, - { - doc_type: supportedDocTypes.collection, - title: 'Science Collection', - data: [ - { - doc_type: supportedDocTypes.flashcard, - title: 'Photosynthesis', - data: [ - { - doc_type: supportedDocTypes.text, - title: 'front_Photosynthesis', - data: 'What is photosynthesis?', - _width: 300, - _height: 300, - }, - { - doc_type: supportedDocTypes.text, - title: 'back_photosynthesis', - data: 'The process by which plants make food.', - _width: 300, - _height: 300, - }, - ], - backgroundColor: '#00ff00', - _width: 300, - _height: 300, - }, - { - doc_type: supportedDocTypes.web, - title: 'Brown University Wikipedia', - data: 'https://en.wikipedia.org/wiki/Brown_University', - _width: 300, - _height: 300, - x: 1100, - y: 1100, - }, - { - doc_type: supportedDocTypes.text, - title: 'Water Cycle', - data: 'The continuous movement of water on, above, and below the Earth’s surface.', - _width: 300, - _height: 300, - x: 1500, - y: 500, - }, - { - doc_type: supportedDocTypes.collection, - title: 'Advanced Biology', - data: [ - { - doc_type: 'text', - title: 'Cell Structure', - data: 'Cells are the basic building blocks of all living organisms.', - _width: 300, - _height: 300, - }, - ], - backgroundColor: '#00ff00', - _width: 600, - _height: 600, - x: 1100, - y: 500, - type_collection: 'stacking', - }, - ], - _width: 600, - _height: 600, - x: 500, - y: 500, - type_collection: 'carousel', - }, -]; - -// Stringify the entire structure for transmission if needed -const finalJsonString = JSON.stringify(example); - -const standardOptions = ['title', 'backgroundColor']; -/** - * Description of document options and data field for each type. - */ -const documentTypesInfo: { [key in supportedDocTypes]: { options: string[]; dataDescription: string } } = { - comparison: { - options: [...standardOptions, 'fontColor', 'text_align'], - dataDescription: 'an array of two documents of any kind that can be compared.', - }, - deck: { - options: [...standardOptions, 'fontColor', 'text_align'], - dataDescription: 'an array of flashcard docs', - }, - flashcard: { - options: [...standardOptions, 'fontColor', 'text_align'], - dataDescription: 'an array of two strings. the first string contains a question, and the second string contains an answer', - }, - text: { - options: [...standardOptions, 'fontColor', 'text_align'], - dataDescription: 'The text content of the document.', - }, - web: { - options: [], - dataDescription: 'A URL to a webpage. Example: https://en.wikipedia.org/wiki/Brown_University', - }, - html: { - options: [], - dataDescription: 'The HTML-formatted text content of the document.', - }, - equation: { - options: [...standardOptions, 'fontColor'], - dataDescription: 'The equation content represented as a MathML string.', - }, - functionplot: { - options: [...standardOptions, 'function_definition'], - dataDescription: 'The function definition(s) for plotting. Provide as a string or array of function definitions.', - }, - dataviz: { - options: [...standardOptions, 'chartType'], - dataDescription: 'A string of comma-separated values representing the CSV data.', - }, - notetaking: { - options: standardOptions, - dataDescription: 'An array of related text documents with small amounts of text.', - }, - rtf: { - options: standardOptions, - dataDescription: 'The rich text content in RTF format.', - }, - image: { - options: standardOptions, - dataDescription: `A url string that must end with '.png', '.jpeg', '.gif', or '.jpg'`, - }, - pdf: { - options: standardOptions, - dataDescription: 'the pdf content as a PDF file url.', - }, - audio: { - options: standardOptions, - dataDescription: 'The audio content as a file url.', - }, - video: { - options: standardOptions, - dataDescription: 'The video content as a file url.', - }, - message: { - options: standardOptions, - dataDescription: 'The message content of the document.', - }, - diagram: { - options: standardOptions, - dataDescription: 'diagram content as a text string in Mermaid format.', - }, - script: { - options: standardOptions, - dataDescription: 'The compilable JavaScript code. Use this for creating scripts.', - }, - collection: { - options: [...standardOptions, 'type_collection'], - dataDescription: 'A collection of Docs represented as an array.', - }, -}; - -// Parameters for creating individual documents -const createDocToolParams: { name: string; type: 'string' | 'number' | 'boolean' | 'string[]' | 'number[]'; description: string; required: boolean }[] = [ - { - name: 'data', - type: 'string', // Accepts either string or array, supporting individual and nested data - description: - 'the data that describes the Document contents. For collections this is an' + - `Array of documents in stringified JSON format. Each item in the array should be an individual stringified JSON object. ` + - `Creates any type of document with the provided options and data. Supported document types are: ${Object.keys(documentTypesInfo).join(', ')}. - dataviz is a csv table tool, so for CSVs, use dataviz. Here are the options for each type: - <supported_document_types>` + - Object.entries(documentTypesInfo) - .map( - ([doc_type, info]) => - `<document_type name="${doc_type}"> - <data_description>${info.dataDescription}</data_description> - <options>` + - info.options.map(option => `<option>${option}</option>`).join('\n') + - ` - </options> - </document_type>` - ) - .join('\n') + - `</supported_document_types> An example of the structure of a collection is:` + - finalJsonString, // prettier-ignore, - required: true, - }, - { - name: 'doc_type', - type: 'string', - description: `The type of the document. Options: ${Object.keys(documentTypesInfo).join(',')}.`, - required: true, - }, - { - name: 'title', - type: 'string', - description: 'The title of the document.', - required: true, - }, - { - name: 'x', - type: 'number', - description: 'The x location of the document; 0 <= x.', - required: true, - }, - { - name: 'y', - type: 'number', - description: 'The y location of the document; 0 <= y.', - required: true, - }, - { - name: 'backgroundColor', - type: 'string', - description: 'The background color of the document as a hex string.', - required: false, - }, - { - name: 'fontColor', - type: 'string', - description: 'The font color of the document as a hex string.', - required: false, - }, - { - name: '_width', - type: 'number', - description: 'The width of the document in pixels.', - required: true, - }, - { - name: '_height', - type: 'number', - description: 'The height of the document in pixels.', - required: true, - }, - { - name: 'type_collection', - type: 'string', - description: `the visual style for a collection doc. Options include: ${Object.values(CollectionViewType).join(',')}.`, - required: false, - }, -] as const; - -type CreateDocToolParamsType = typeof createDocToolParams; - -const createDocToolInfo: ToolInfo<CreateDocToolParamsType> = { - name: 'createDoc', - description: `Creates one or more documents that best fit the user’s request. - If the user requests a "dashboard," first call the search tool and then generate a variety of document types individually, with absolutely a minimum of 20 documents - with two stacks of flashcards that are small and it should have a couple nested freeform collections of things, each with different content and color schemes. - For example, create multiple individual documents, including ${Object.keys(documentTypesInfo) - .map(t => '"' + t + '"') - .join(',')} - If the "doc_type" parameter is missing, set it to an empty string (""). - Use Decks instead of Flashcards for dashboards. Decks should have at least three flashcards. - Really think about what documents are useful to the user. If they ask for a dashboard about the skeletal system, include flashcards, as they would be helpful. - Arrange the documents in a grid layout, ensuring that the x and y coordinates are calculated so no documents overlap but they should be directly next to each other with 20 padding in between. - Take into account the width and height of each document, spacing them appropriately to prevent collisions. - Use a systematic approach, such as placing each document in a grid cell based on its order, where cell dimensions match the document dimensions plus a fixed margin for spacing. - Do not nest all documents within a single collection unless explicitly requested by the user. - Instead, create a set of independent documents with diverse document types. Each type should appear separately unless specified otherwise. - Use the "data" parameter for document content and include title, color, and document dimensions. - Ensure web documents use URLs from the search tool if relevant. Each document in a dashboard should be unique and well-differentiated in type and content, - without repetition of similar types in any single collection. - When creating a dashboard, ensure that it consists of a broad range of document types. - Include a variety of documents, such as text, web, deck, comparison, image, and equation documents, - each with distinct titles and colors, following the user’s preferences. - Do not overuse collections or nest all document types within a single collection; instead, represent document types individually. Use this example for reference: - ${finalJsonString} . - Which documents are created should be random with different numbers of each document type and different for each dashboard. - Must use search tool before creating a dashboard.`, - parameterRules: createDocToolParams, - citationRules: 'No citation needed.', -}; - -// Tool class for creating documents -export class CreateDocTool extends BaseTool< - { - name: string; - type: 'string' | 'number' | 'boolean' | 'string[]' | 'number[]'; - description: string; - required: boolean; - }[] -> { - private _addLinkedDoc: (doc: parsedDoc) => void; - - constructor(addLinkedDoc: (doc: parsedDoc) => void) { - super(createDocToolInfo); - this._addLinkedDoc = addLinkedDoc; - } - - override inputValidator(inputParam: ParametersType<readonly Parameter[]>) { - return !!inputParam.data; - } - // Executes the tool logic for creating documents - async execute( - args: ParametersType< - { - name: 'string'; - type: 'string' | 'number' | 'boolean' | 'string[]' | 'number[]'; - description: 'string'; - required: boolean; - }[] - > - ): Promise<Observation[]> { - try { - const parsedDocs = args instanceof Array ? args : Object.keys(args).length === 1 && 'data' in args ? JSON.parse(args.data as string) : [args]; - parsedDocs.forEach((pdoc: parsedDoc) => this._addLinkedDoc({ ...pdoc, _layout_fitWidth: false, _layout_autoHeight: true })); - return [{ type: 'text', text: 'Created document.' }]; - } catch (error) { - return [{ type: 'text', text: 'Error creating text document, ' + error }]; - } - } -} diff --git a/src/client/views/nodes/chatbot/tools/CreateLinksTool.ts b/src/client/views/nodes/chatbot/tools/CreateLinksTool.ts new file mode 100644 index 000000000..c2850a8ce --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/CreateLinksTool.ts @@ -0,0 +1,68 @@ +import { Observation } from '../types/types'; +import { ParametersType, ToolInfo } from '../types/tool_types'; +import { BaseTool } from './BaseTool'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; + +const createLinksToolParams = [ + { + name: 'document_ids', + type: 'string[]', + description: 'List of document IDs to create links between. All documents will be linked to each other.', + required: true, + }, +] as const; + +type CreateLinksToolParamsType = typeof createLinksToolParams; + +const createLinksToolInfo: ToolInfo<CreateLinksToolParamsType> = { + name: 'createLinks', + description: 'Creates visual links between multiple documents in the dashboard. This allows related documents to be connected visually with lines that users can see.', + citationRules: 'No citation needed.', + parameterRules: createLinksToolParams, +}; + +export class CreateLinksTool extends BaseTool<CreateLinksToolParamsType> { + private _documentManager: AgentDocumentManager; + + constructor(documentManager: AgentDocumentManager) { + super(createLinksToolInfo); + this._documentManager = documentManager; + } + + async execute(args: ParametersType<CreateLinksToolParamsType>): Promise<Observation[]> { + try { + // Validate that we have at least 2 documents to link + if (args.document_ids.length < 2) { + return [{ type: 'text', text: 'Error: At least 2 document IDs are required to create links.' }]; + } + + // Validate that all documents exist + const missingDocIds = args.document_ids.filter(id => !this._documentManager.has(id)); + if (missingDocIds.length > 0) { + return [ + { + type: 'text', + text: `Error: The following document IDs were not found: ${missingDocIds.join(', ')}`, + }, + ]; + } + + // Create links between all documents with the specified relationship + const createdLinks = this._documentManager.addLinks(args.document_ids); + + return [ + { + type: 'text', + text: `Successfully created ${createdLinks.length} visual links between ${args.document_ids.length}.`, + }, + ]; + } catch (error) { + return [ + { + type: 'text', + text: `Error creating links: ${error instanceof Error ? error.message : String(error)}`, + }, + ]; + } + } +} diff --git a/src/client/views/nodes/chatbot/tools/CreateTextDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateTextDocumentTool.ts deleted file mode 100644 index 16dc938bb..000000000 --- a/src/client/views/nodes/chatbot/tools/CreateTextDocumentTool.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { parsedDoc } from '../chatboxcomponents/ChatBox'; -import { ParametersType, ToolInfo } from '../types/tool_types'; -import { Observation } from '../types/types'; -import { BaseTool } from './BaseTool'; -const createTextDocToolParams = [ - { - name: 'text_content', - type: 'string', - description: 'The text content that the document will display', - required: true, - }, - { - name: 'title', - type: 'string', - description: 'The title of the document', - required: true, - }, - // { - // name: 'background_color', - // type: 'string', - // description: 'The background color of the document as a hex string', - // required: false, - // }, - // { - // name: 'font_color', - // type: 'string', - // description: 'The font color of the document as a hex string', - // required: false, - // }, -] as const; - -type CreateTextDocToolParamsType = typeof createTextDocToolParams; - -const createTextDocToolInfo: ToolInfo<CreateTextDocToolParamsType> = { - name: 'createTextDoc', - description: 'Creates a text document with the provided content and title. Use if the user wants to create a textbox or text document of some sort. Can use after a search or other tool to save information.', - citationRules: 'No citation needed.', - parameterRules: createTextDocToolParams, -}; - -export class CreateTextDocTool extends BaseTool<CreateTextDocToolParamsType> { - private _addLinkedDoc: (doc: parsedDoc) => void; - - constructor(addLinkedDoc: (doc: parsedDoc) => void) { - super(createTextDocToolInfo); - this._addLinkedDoc = addLinkedDoc; - } - - async execute(args: ParametersType<CreateTextDocToolParamsType>): Promise<Observation[]> { - try { - this._addLinkedDoc({ doc_type: 'text', data: args.text_content, title: args.title }); - return [{ type: 'text', text: 'Created text document.' }]; - } catch (error) { - return [{ type: 'text', text: 'Error creating text document, ' + error }]; - } - } -} diff --git a/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts new file mode 100644 index 000000000..e6c2421e5 --- /dev/null +++ b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts @@ -0,0 +1,573 @@ +import { Doc, FieldType } from '../../../../../fields/Doc'; +import { DocData } from '../../../../../fields/DocSymbols'; +import { Observation } from '../types/types'; +import { ParametersType, ToolInfo, Parameter } from '../types/tool_types'; +import { BaseTool } from './BaseTool'; +import { DocumentOptions } from '../../../../documents/Documents'; +import { CollectionFreeFormDocumentView } from '../../../nodes/CollectionFreeFormDocumentView'; +import { v4 as uuidv4 } from 'uuid'; +import { LinkManager } from '../../../../util/LinkManager'; +import { DocCast, StrCast } from '../../../../../fields/Types'; +import { supportedDocTypes } from '../types/tool_types'; +import { parsedDoc } from '../chatboxcomponents/ChatBox'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; + +// Define the parameters for the DocumentMetadataTool +const parameterDefinitions: ReadonlyArray<Parameter> = [ + { + name: 'action', + type: 'string', + required: true, + description: 'The action to perform: "get" to retrieve metadata, "edit" to modify metadata, "list" to enumerate documents, "getFieldOptions" to retrieve all available field options, or "create" to create a new document', + }, + { + name: 'documentId', + type: 'string', + required: false, + description: 'The ID of the document to get or edit metadata for. Required for "edit", optional for "get", ignored for "list", "getFieldOptions", and "create"', + }, + { + name: 'fieldEdits', + type: 'string', + required: false, + description: + 'JSON array of field edits for editing fields. Each item should have fieldName and fieldValue. For single field edits, use an array with one item. Example: [{"fieldName":"layout_autoHeight","fieldValue":false},{"fieldName":"height","fieldValue":300}]', + }, + { + name: 'title', + type: 'string', + required: false, + description: 'The title of the document to create. Required for "create" action', + }, + { + name: 'data', + type: 'string', + required: false, + description: 'The data content for the document to create. Required for "create" action', + }, + { + name: 'doc_type', + type: 'string', + required: false, + description: `The type of document to create. Required for "create" action. Options: ${Object.keys(supportedDocTypes).join(',')}`, + }, +] as const; + +type DocumentMetadataToolParamsType = typeof parameterDefinitions; + +// Detailed description with usage guidelines for the DocumentMetadataTool +const toolDescription = `Extracts and modifies metadata from documents in the same Freeform view as the ChatBox, and can create new documents. +This tool helps you work with document properties, understand available fields, edit document metadata, and create new documents. + +The Dash document system organizes fields in two locations: +1. Layout documents: contain visual properties like position, dimensions, and appearance +2. Data documents: contain the actual content and document-specific data + +This tool provides the following capabilities: +- Get metadata from all documents in the current Freeform view +- Get metadata from a specific document +- Edit metadata fields on documents (in either layout or data documents) +- Edit multiple fields at once (useful for updating dependent fields together) +- List all available documents in the current view +- Retrieve all available field options with metadata (IMPORTANT: always call this before editing) +- Understand which fields are stored where (layout vs data document) +- Get detailed information about all available document fields +- Support for all value types: strings, numbers, and booleans +- Create new documents with basic properties + +DOCUMENT CREATION: +- Use action="create" to create new documents with a simplified approach +- Required parameters: title, data, and doc_type +- The tool will create the document with sensible defaults and link it to the current view +- After creation, you can use the edit action to update its properties + +IMPORTANT: Before editing any document metadata, first call 'getFieldOptions' to understand: +- Which fields are available +- The data type of each field +- Special dependencies between fields (like layout_autoHeight and height) +- Proper naming conventions (with or without underscores) + +IMPORTANT: Some fields have dependencies that must be handled for edits to work correctly: +- When editing "height", first set "layout_autoHeight" to false (as a boolean value, not a string) +- When editing "width", first set "layout_autoWidth" to false (as a boolean value, not a string) +- Check document metadata to identify other similar dependencies +- All edits are done using the fieldEdits parameter which accepts an array of fields to modify + +Example: To change document height, disable auto-height and set height in a single operation: +{... inputs: { action: "edit", documentId: "doc123", fieldEdits: [ + { fieldName: "layout_autoHeight", fieldValue: false }, + { fieldName: "height", fieldValue: 300 } +]}}`; + +// Extensive usage guidelines for the tool +const citationRules = `USAGE GUIDELINES: +To GET document metadata: +- Use action="get" with optional documentId to return metadata for one or all documents +- Returns field values, field definitions, and location information (layout vs data document) + +To GET ALL FIELD OPTIONS (call this first): +- Use action="getFieldOptions" to retrieve metadata about all available document fields +- No additional parameters are required +- Returns structured metadata with field names, types, descriptions, and dependencies +- ALWAYS call this before attempting to edit document metadata +- Use this information to understand which fields need special handling + +To CREATE a new document: +- Use action="create" with the following required parameters: + - title: The title of the document to create + - data: The content data for the document (text content, URL, etc.) + - doc_type: The type of document to create (text, web, image, etc.) +- Example: {...inputs: { action: "create", title: "My Notes", data: "This is the content", doc_type: "text" }} +- After creation, you can edit the document with more specific properties + +To EDIT document metadata: +- Use action="edit" with required parameters: + - documentId: The ID of the document to edit + - fieldEdits: JSON array of fields to edit, each with fieldName and fieldValue +- The tool will determine the correct document location automatically +- Field names can be provided with or without leading underscores (e.g., both "width" and "_width" work) +- Common fields like "width" and "height" are automatically mapped to "_width" and "_height" +- All value types are supported: strings, numbers, and booleans +- The tool will apply the edit to the correct document (layout or data) based on existing fields + +SPECIAL FIELD HANDLING: +- Text fields: When editing the 'text' field, provide simple plain text + Example: {...inputs: { action: "edit", documentId: "doc123", fieldEdits: [{ fieldName: "text", fieldValue: "Hello world" }] }} + The tool will automatically convert your text to the proper RichTextField format +- Width/Height: Set layout_autoHeight/layout_autoWidth to false before editing + +RECOMMENDED WORKFLOW: +1. First call action="list" to identify available documents +2. Then call action="getFieldOptions" to understand available fields +3. Get document metadata with action="get" to see current values +4. Edit fields with action="edit" using proper dependencies +OR +1. Create a new document with action="create" +2. Get its ID from the response +3. Edit the document's properties with action="edit" + +HANDLING DEPENDENT FIELDS: +- When editing some fields, you may need to update related dependent fields +- For example, when changing "height", you should also set "layout_autoHeight" to false +- Use the fieldEdits parameter to update dependent fields in a single operation: + {...inputs: { action: "edit", documentId: "doc123", fieldEdits: [ + { fieldName: "layout_autoHeight", fieldValue: false }, + { fieldName: "height", fieldValue: 300 } +]}} +- Always check for dependent fields that might affect your edits, such as: + - height → layout_autoHeight (set to false to allow manual height) + - width → layout_autoWidth (set to false to allow manual width) + - Other auto-sizing related properties + +To LIST available documents: +- Use action="list" to get a simple list of all documents in the current view +- This is useful when you need to identify documents before getting details or editing them + +Editing fields follows these rules: +1. First checks if the field exists on the layout document using Doc.Get +2. If it exists on the layout document, it's updated there +3. If it has an underscore prefix (_), it's created/updated on the layout document +4. Otherwise, the field is created/updated on the data document +5. Fields with leading underscores are automatically handled correctly + +Examples: +- To get field options: { action: "getFieldOptions" } +- To list all documents: { action: "list" } +- To get all document metadata: { action: "get" } +- To get metadata for a specific document: { action: "get", documentId: "doc123" } +- To edit a single field: { action: "edit", documentId: "doc123", fieldEdits: [{ fieldName: "backgroundColor", fieldValue: "#ff0000" }] } +- To edit a width property: { action: "edit", documentId: "doc123", fieldEdits: [{ fieldName: "width", fieldValue: 300 }] } +- To edit text content: { action: "edit", documentId: "doc123", fieldEdits: [{ fieldName: "text", fieldValue: "Simple plain text goes here" }] } +- To disable auto-height: { action: "edit", documentId: "doc123", fieldEdits: [{ fieldName: "layout_autoHeight", fieldValue: false }] } +- To create a text document: { action: "create", title: "My Notes", data: "This is my note content", doc_type: "text" } +- To create a web document: { action: "create", title: "Google", data: "https://www.google.com", doc_type: "web" } +- To edit height with its dependent field together: + { action: "edit", documentId: "doc123", fieldEdits: [ + { fieldName: "layout_autoHeight", fieldValue: false }, + { fieldName: "height", fieldValue: 200 } + ]} +- IMPORTANT: MULTI STEP WORKFLOWS ARE NOT ONLY ALLOWED BUT ENCOURAGED. TAKE THINGS 1 STEP AT A TIME.`; +const documentMetadataToolInfo: ToolInfo<DocumentMetadataToolParamsType> = { + name: 'documentMetadata', + description: toolDescription, + parameterRules: parameterDefinitions, + citationRules: citationRules, +}; + +/** + * A tool for extracting and modifying metadata from documents in a Freeform view. + * This tool collects metadata from both layout and data documents in a Freeform view + * and allows for editing document fields in the correct location. + */ +export class DocumentMetadataTool extends BaseTool<DocumentMetadataToolParamsType> { + private _docManager: AgentDocumentManager; + + constructor(docManager: AgentDocumentManager) { + super(documentMetadataToolInfo); + this._docManager = docManager; + this._docManager.initializeFindDocsFreeform(); + } + + /** + * Executes the document metadata tool + * @param args The arguments for the tool + * @returns An observation with the results of the tool execution + */ + async execute(args: ParametersType<DocumentMetadataToolParamsType>): Promise<Observation[]> { + console.log('DocumentMetadataTool: Executing with args:', args); + + // Find all documents in the Freeform view + this._docManager.initializeFindDocsFreeform(); + + try { + // Validate required input parameters based on action + if (!this.inputValidator(args)) { + return [ + { + type: 'text', + text: `Error: Invalid or missing parameters for action "${args.action}". ${this.getParameterRequirementsByAction(String(args.action))}`, + }, + ]; + } + + // Ensure the action is valid and convert to string + const action = String(args.action); + if (!['get', 'edit', 'list', 'getFieldOptions', 'create'].includes(action)) { + return [ + { + type: 'text', + text: 'Error: Invalid action. Valid actions are "get", "edit", "list", "getFieldOptions", or "create".', + }, + ]; + } + + // Safely convert documentId to string or undefined + const documentId = args.documentId ? String(args.documentId) : undefined; + + // Perform the specified action + switch (action) { + case 'get': { + // Get metadata for a specific document or all documents + const result = this._docManager.getDocumentMetadata(documentId); + console.log('DocumentMetadataTool: Get metadata result:', result); + return [ + { + type: 'text', + text: `Document metadata ${documentId ? 'for document ' + documentId : ''} retrieved successfully:\n${JSON.stringify(result, null, 2)}`, + }, + ]; + } + + case 'edit': { + // Edit a specific field on a document + if (!documentId) { + return [ + { + type: 'text', + text: 'Error: Document ID is required for edit actions.', + }, + ]; + } + + // Ensure document exists + if (!this._docManager.has(documentId)) { + return [ + { + type: 'text', + text: `Error: Document with ID ${documentId} not found.`, + }, + ]; + } + + // Check for fieldEdits parameter + if (!args.fieldEdits) { + return [ + { + type: 'text', + text: 'Error: fieldEdits is required for edit actions. Please provide a JSON array of field edits.', + }, + ]; + } + + try { + // Parse fieldEdits array + const edits = JSON.parse(String(args.fieldEdits)); + if (!Array.isArray(edits) || edits.length === 0) { + return [ + { + type: 'text', + text: 'Error: fieldEdits must be a non-empty array of field edits.', + }, + ]; + } + + // Track results for all edits + const results: { + success: boolean; + message: string; + fieldName?: string; + originalFieldName?: string; + newValue?: any; + warning?: string; + }[] = []; + + let allSuccessful = true; + + // Process each edit + for (const edit of edits) { + // Get fieldValue in its original form + let fieldValue = edit.fieldValue; + + // Only convert to string if it's neither boolean nor number + if (typeof fieldValue !== 'boolean' && typeof fieldValue !== 'number') { + fieldValue = String(fieldValue); + } + + const fieldName = String(edit.fieldName); + + // Edit the field + const result = this._docManager.editDocumentField(documentId, fieldName, fieldValue); + + console.log(`DocumentMetadataTool: Edit field result for ${fieldName}:`, result); + + // Add to results + results.push(result); + + // Update success status + if (!result.success) { + allSuccessful = false; + } + } + + // Format response based on results + let responseText = ''; + if (allSuccessful) { + responseText = `Successfully edited ${results.length} fields on document ${documentId}:\n`; + results.forEach(result => { + responseText += `- Field '${result.originalFieldName}': updated to ${JSON.stringify(result.newValue)}\n`; + + // Add any warnings + if (result.warning) { + responseText += ` Warning: ${result.warning}\n`; + } + }); + } else { + responseText = `Errors occurred while editing fields on document ${documentId}:\n`; + results.forEach(result => { + if (result.success) { + responseText += `- Field '${result.originalFieldName}': updated to ${JSON.stringify(result.newValue)}\n`; + + // Add any warnings + if (result.warning) { + responseText += ` Warning: ${result.warning}\n`; + } + } else { + responseText += `- Error editing '${result.originalFieldName}': ${result.message}\n`; + } + }); + } + + // Get the updated metadata to return + const updatedMetadata = this._docManager.getDocumentMetadata(documentId); + + return [ + { + type: 'text', + text: `${responseText}\nUpdated metadata:\n${JSON.stringify(updatedMetadata, null, 2)}`, + }, + ]; + } catch (error) { + return [ + { + type: 'text', + text: `Error processing fieldEdits: ${error instanceof Error ? error.message : String(error)}`, + }, + ]; + } + } + + case 'list': { + this._docManager.listDocs(); + } + + case 'getFieldOptions': { + // Get all available field options with metadata + const fieldOptions = this._docManager.getAllFieldMetadata(); + + return [ + { + type: 'text', + text: `Document field options retrieved successfully.\nThis information should be consulted before editing document fields to understand available options and dependencies:\n${JSON.stringify(fieldOptions, null, 2)}`, + }, + ]; + } + + case 'create': { + // Create a new document + if (!args.title || !args.data || !args.doc_type) { + return [ + { + type: 'text', + text: 'Error: Title, data, and doc_type are required for create action.', + }, + ]; + } + + const docType = String(args.doc_type); + const title = String(args.title); + const data = String(args.data); + + const id = this._docManager.createDocInDash(docType, data, { title: title }); + + if (!id) { + return [ + { + type: 'text', + text: 'Error: Failed to create document.', + }, + ]; + } + // Get the created document's metadata + const createdMetadata = this._docManager.extractDocumentMetadata(id); + + return [ + { + type: 'text', + text: `Document created successfully. +Document ID: ${id} +Type: ${docType} +Title: "${title}" + +The document has been created with default dimensions and positioning. +You can now use the "edit" action to modify additional properties of this document. + +Next steps: +1. Use the "getFieldOptions" action to understand available editable/addable fields/properties and their dependencies. +2. To modify this document, use: { action: "edit", documentId: "${id}", fieldEdits: [{"fieldName":"property","fieldValue":"value"}] } +3. To add styling, consider setting backgroundColor, fontColor, or other properties +4. For text documents, you can edit the content with: { action: "edit", documentId: "${id}", fieldEdits: [{"fieldName":"text","fieldValue":"New content"}] } + +Full metadata for the created document: +${JSON.stringify(createdMetadata, null, 2)}`, + }, + ]; + } + + default: + return [ + { + type: 'text', + text: 'Error: Unknown action. Valid actions are "get", "edit", "list", "getFieldOptions", or "create".', + }, + ]; + } + } catch (error) { + console.error('DocumentMetadataTool execution error:', error); + return [ + { + type: 'text', + text: `Error executing DocumentMetadataTool: ${error instanceof Error ? error.message : String(error)}`, + }, + ]; + } + } + + /** + * Validates the input parameters for the DocumentMetadataTool + * This custom validator allows numbers and booleans to be passed for fieldValue + * while maintaining compatibility with the standard validation + * + * @param params The parameters to validate + * @returns True if the parameters are valid, false otherwise + */ + inputValidator(params: ParametersType<DocumentMetadataToolParamsType>): boolean { + // Default validation for required fields + if (params.action === undefined) { + return false; + } + + // For create action, validate required parameters + if (params.action === 'create') { + return !!(params.title && params.data && params.doc_type); + } + + // For edit action, validate fieldEdits is provided + if (params.action === 'edit') { + if (!params.documentId || !params.fieldEdits) { + return false; + } + + try { + // Parse fieldEdits and validate its structure + const edits = JSON.parse(String(params.fieldEdits)); + + // Ensure it's an array + if (!Array.isArray(edits)) { + console.log('fieldEdits is not an array'); + return false; + } + + // Ensure each item has fieldName and fieldValue + for (const edit of edits) { + if (!edit.fieldName) { + console.log('An edit is missing fieldName'); + return false; + } + if (edit.fieldValue === undefined) { + console.log('An edit is missing fieldValue'); + return false; + } + } + + // Everything looks good with fieldEdits + return true; + } catch (error) { + console.log('Error parsing fieldEdits:', error); + return false; + } + } + + // For get action with documentId, documentId is required + if (params.action === 'get' && params.documentId === '') { + return false; + } + + // getFieldOptions action doesn't require any additional parameters + if (params.action === 'getFieldOptions') { + return true; + } + + // list action doesn't require any additional parameters + if (params.action === 'list') { + return true; + } + + return true; + } + + /** + * Returns the parameter requirements for a specific action + * @param action The action to get requirements for + * @returns A string describing the required parameters + */ + private getParameterRequirementsByAction(action?: string): string { + if (!action) { + return 'Please specify an action: "get", "edit", "list", "getFieldOptions", or "create".'; + } + + switch (action.toLowerCase()) { + case 'get': + return 'The "get" action accepts an optional documentId parameter.'; + case 'edit': + return 'The "edit" action requires documentId and fieldEdits parameters. fieldEdits must be a JSON array of field edits.'; + case 'list': + return 'The "list" action does not require any additional parameters.'; + case 'getFieldOptions': + return 'The "getFieldOptions" action does not require any additional parameters. It returns metadata about all available document fields.'; + case 'create': + return 'The "create" action requires title, data, and doc_type parameters.'; + default: + return `Unknown action "${action}". Valid actions are "get", "edit", "list", "getFieldOptions", or "create".`; + } + } +} diff --git a/src/client/views/nodes/chatbot/tools/SearchTool.ts b/src/client/views/nodes/chatbot/tools/SearchTool.ts index 6a11407a5..53f5fc109 100644 --- a/src/client/views/nodes/chatbot/tools/SearchTool.ts +++ b/src/client/views/nodes/chatbot/tools/SearchTool.ts @@ -3,6 +3,9 @@ import { Networking } from '../../../../Network'; import { BaseTool } from './BaseTool'; import { Observation } from '../types/types'; import { ParametersType, ToolInfo } from '../types/tool_types'; +import { Agent } from 'http'; +import { AgentDocumentManager } from '../utils/AgentDocumentManager'; +import { StrCast } from '../../../../../fields/Types'; const searchToolParams = [ { @@ -25,12 +28,12 @@ const searchToolInfo: ToolInfo<SearchToolParamsType> = { }; export class SearchTool extends BaseTool<SearchToolParamsType> { - private _addLinkedUrlDoc: (url: string, id: string) => void; + private _docManager: AgentDocumentManager; private _max_results: number; - constructor(addLinkedUrlDoc: (url: string, id: string) => void, max_results: number = 4) { + constructor(docManager: AgentDocumentManager, max_results: number = 3) { super(searchToolInfo); - this._addLinkedUrlDoc = addLinkedUrlDoc; + this._docManager = docManager; this._max_results = max_results; } @@ -46,8 +49,13 @@ export class SearchTool extends BaseTool<SearchToolParamsType> { max_results: this._max_results, })) as { results: { url: string; snippet: string }[] }; const data = results.map((result: { url: string; snippet: string }) => { - const id = uuidv4(); - this._addLinkedUrlDoc(result.url, id); + // Create a web document with the URL + const id = this._docManager.createDocInDash('web', result.url, { + title: `Search Result: ${result.url}`, + text_html: result.snippet, + data_useCors: true, + }); + return { type: 'text' as const, text: `<chunk chunk_id="${id}" chunk_type="url"><url>${result.url}</url><overview>${result.snippet}</overview></chunk>`, diff --git a/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts b/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts index 19ccd0b36..3c7b4e3db 100644 --- a/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts +++ b/src/client/views/nodes/chatbot/tools/WebsiteInfoScraperTool.ts @@ -66,38 +66,118 @@ const websiteInfoScraperToolInfo: ToolInfo<WebsiteInfoScraperToolParamsType> = { }; export class WebsiteInfoScraperTool extends BaseTool<WebsiteInfoScraperToolParamsType> { - private _addLinkedUrlDoc: (url: string, id: string) => void; + private _getLinkedUrlDocId: (url: string) => string[]; - constructor(addLinkedUrlDoc: (url: string, id: string) => void) { + constructor(getLinkedUrlDocIds: (url: string) => string[]) { super(websiteInfoScraperToolInfo); - this._addLinkedUrlDoc = addLinkedUrlDoc; + this._getLinkedUrlDocId = getLinkedUrlDocIds; } - async execute(args: ParametersType<WebsiteInfoScraperToolParamsType>): Promise<Observation[]> { - const urls = args.urls; - - // Create an array of promises, each one handling a website scrape for a URL - const scrapingPromises = urls.map(async url => { + /** + * Attempts to scrape a website with retry logic + * @param url URL to scrape + * @param maxRetries Maximum number of retry attempts + * @returns The scraped content or error message + */ + private async scrapeWithRetry(url: string, maxRetries = 2): Promise<Observation> { + let lastError = ''; + let retryCount = 0; + + // Validate URL format + try { + new URL(url); // This will throw if URL is invalid + } catch (e) { + return { + type: 'text', + text: `Invalid URL format: ${url}. Please provide a valid URL including http:// or https://`, + } as Observation; + } + + while (retryCount <= maxRetries) { try { - const { website_plain_text } = await Networking.PostToServer('/scrapeWebsite', { url }); - const id = uuidv4(); - this._addLinkedUrlDoc(url, id); + // Add a slight delay between retries + if (retryCount > 0) { + console.log(`Retry attempt ${retryCount} for ${url}`); + await new Promise(resolve => setTimeout(resolve, retryCount * 2000)); // Increasing delay for each retry + } + + const response = await Networking.PostToServer('/scrapeWebsite', { url }); + + if (!response || typeof response !== 'object') { + lastError = 'Empty or invalid response from server'; + retryCount++; + continue; + } + + const { website_plain_text } = response as { website_plain_text: string }; + const id = this._getLinkedUrlDocId(url); + + // Validate content quality + if (!website_plain_text) { + lastError = 'Retrieved content was empty'; + retryCount++; + continue; + } + + if (website_plain_text.length < 100) { + console.warn(`Warning: Content from ${url} is very short (${website_plain_text.length} chars)`); + + // Still return it if this is our last try + if (retryCount === maxRetries) { + return { + type: 'text', + text: `<chunk chunk_id="${id}" chunk_type="url">\n${website_plain_text}\nNote: Limited content was retrieved from this URL.\n</chunk>`, + } as Observation; + } + + lastError = 'Retrieved content was too short, trying again'; + retryCount++; + continue; + } + + // Process and return content if it looks good return { type: 'text', text: `<chunk chunk_id="${id}" chunk_type="url">\n${website_plain_text}\n</chunk>`, } as Observation; } catch (error) { - console.log(error); - return { - type: 'text', - text: `An error occurred while scraping the website: ${url}`, - } as Observation; + lastError = error instanceof Error ? error.message : 'Unknown error'; + console.log(`Error scraping ${url} (attempt ${retryCount + 1}):`, error); } - }); + + retryCount++; + } + + // All attempts failed + return { + type: 'text', + text: `Unable to scrape website: ${url}. Error: ${lastError}`, + } as Observation; + } + + async execute(args: ParametersType<WebsiteInfoScraperToolParamsType>): Promise<Observation[]> { + const urls = args.urls; + + // Create an array of promises, each one handling a website scrape for a URL + const scrapingPromises = urls.map(url => this.scrapeWithRetry(url)); // Wait for all scraping promises to resolve const results = await Promise.all(scrapingPromises); + // Check if we got any successful results + const successfulResults = results.filter(result => { + if (result.type !== 'text') return false; + return (result as { type: 'text'; text: string }).text.includes('chunk_id') && !(result as { type: 'text'; text: string }).text.includes('Unable to scrape'); + }); + + // If all scrapes failed, provide a more helpful error message + if (successfulResults.length === 0 && results.length > 0) { + results.push({ + type: 'text', + text: `Note: All website scraping attempts failed. Please try with different URLs or try again later.`, + } as Observation); + } + return results; } } |
