diff options
Diffstat (limited to 'src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts')
-rw-r--r-- | src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts | 573 |
1 files changed, 573 insertions, 0 deletions
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".`; + } + } +} |