aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts')
-rw-r--r--src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts900
1 files changed, 900 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..9c3a1fbb5
--- /dev/null
+++ b/src/client/views/nodes/chatbot/tools/DocumentMetadataTool.ts
@@ -0,0 +1,900 @@
+import { Doc, FieldType } from '../../../../../fields/Doc';
+import { DocData } from '../../../../../fields/DocSymbols';
+import { Observation } from '../types/types';
+import { ParametersType, ToolInfo } 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';
+
+// Parameter definitions for DocumentMetadataTool
+const documentMetadataToolParams = [
+ {
+ name: 'action',
+ type: 'string',
+ description: 'The action to perform: "get" for retrieving metadata, "edit" for modifying document metadata, or "list" for a simple document list',
+ required: true,
+ },
+ {
+ name: 'documentId',
+ type: 'string',
+ description: 'The ID of the document to get metadata from or edit. If not provided when getting metadata, information about all documents will be returned.',
+ required: false,
+ },
+ {
+ name: 'fieldName',
+ type: 'string',
+ description: 'When editing, the name of the field to modify (with or without leading underscore).',
+ required: false,
+ },
+ {
+ name: 'fieldValue',
+ type: 'string',
+ description: 'When editing, the new value to set for the specified field. Numeric values will be converted to strings automatically.',
+ required: false,
+ },
+ {
+ name: 'documentLocation',
+ type: 'string',
+ description: 'When editing, specify where to modify the field: "layout" (default), "data", or "auto" (determines automatically based on existing fields).',
+ required: false,
+ }
+] as const;
+
+type DocumentMetadataToolParamsType = typeof documentMetadataToolParams;
+
+// Detailed description with usage guidelines for the DocumentMetadataTool
+const toolDescription = `Extracts and modifies metadata from documents in the same Freeform view as the ChatBox.
+This tool helps you work with document properties, understand available fields, and edit document metadata.
+
+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)
+- List all available documents in the current view
+- Understand which fields are stored where (layout vs data document)
+- Get detailed information about all available document fields`;
+
+// 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 EDIT document metadata:
+- Use action="edit" with required documentId, fieldName, and fieldValue parameters
+- Optionally specify documentLocation="layout"|"data"|"auto" (default is "auto")
+- The tool will determine the correct document location automatically unless specified
+- 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"
+- Numeric values are accepted for appropriate fields (width, height, etc.)
+- The tool will apply the edit to the correct document (layout or data)
+
+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. If documentLocation is specified, the field is modified in that location
+2. If documentLocation="auto", the tool checks if the field exists on layout or data document first
+3. If the field doesn't exist in either document, it's added to the location specified or layout document by default
+4. Fields with leading underscores are automatically handled correctly
+
+Examples:
+- 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 field: { action: "edit", documentId: "doc123", fieldName: "backgroundColor", fieldValue: "#ff0000" }
+- To edit a width property: { action: "edit", documentId: "doc123", fieldName: "width", fieldValue: 300 }
+- To edit a field in the data document: { action: "edit", documentId: "doc123", fieldName: "text", fieldValue: "New content", documentLocation: "data" }`;
+
+const documentMetadataToolInfo: ToolInfo<DocumentMetadataToolParamsType> = {
+ name: 'documentMetadata',
+ description: toolDescription,
+ parameterRules: documentMetadataToolParams,
+ 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 freeformView: any;
+ private chatBox: any;
+ private chatBoxDocument: Doc | null = null;
+ private documentsById: Map<string, Doc> = new Map();
+ private layoutDocsById: Map<string, Doc> = new Map();
+ private dataDocsById: Map<string, Doc> = new Map();
+ private fieldMetadata: Record<string, any> = {};
+ private readonly DOCUMENT_ID_FIELD = '_dash_document_id';
+
+ constructor(chatBox: any) {
+ super(documentMetadataToolInfo);
+ this.chatBox = chatBox;
+
+ // Store a direct reference to the ChatBox document
+ if (chatBox && chatBox.Document) {
+ this.chatBoxDocument = chatBox.Document;
+ if (this.chatBoxDocument && this.chatBoxDocument.id) {
+ console.log('DocumentMetadataTool initialized with ChatBox Document:', this.chatBoxDocument.id);
+ } else {
+ console.log('DocumentMetadataTool initialized with ChatBox Document (no ID)');
+ }
+ } else if (chatBox && chatBox.props && chatBox.props.Document) {
+ this.chatBoxDocument = chatBox.props.Document;
+ if (this.chatBoxDocument && this.chatBoxDocument.id) {
+ console.log('DocumentMetadataTool initialized with ChatBox props.Document:', this.chatBoxDocument.id);
+ } else {
+ console.log('DocumentMetadataTool initialized with ChatBox props.Document (no ID)');
+ }
+ } else {
+ console.warn('DocumentMetadataTool initialized without valid ChatBox Document reference');
+ }
+
+ this.initializeFieldMetadata();
+ }
+
+ /**
+ * Extracts field metadata from DocumentOptions class
+ */
+ private initializeFieldMetadata() {
+ // Parse DocumentOptions to extract field definitions
+ const documentOptionsInstance = new DocumentOptions();
+ const documentOptionsEntries = Object.entries(documentOptionsInstance);
+
+ for (const [fieldName, fieldInfo] of documentOptionsEntries) {
+ // Extract field information
+ const fieldData: Record<string, any> = {
+ name: fieldName,
+ withoutUnderscore: fieldName.startsWith('_') ? fieldName.substring(1) : fieldName,
+ description: '',
+ type: 'unknown',
+ required: false,
+ defaultValue: undefined,
+ possibleValues: [],
+ };
+
+ // Check if fieldInfo has description property (it's likely a FInfo instance)
+ if (fieldInfo && typeof fieldInfo === 'object' && 'description' in fieldInfo) {
+ fieldData.description = fieldInfo.description;
+
+ // Extract field type if available
+ if ('fieldType' in fieldInfo) {
+ fieldData.type = fieldInfo.fieldType;
+ }
+
+ // Extract possible values if available
+ if ('values' in fieldInfo && Array.isArray(fieldInfo.values)) {
+ fieldData.possibleValues = fieldInfo.values;
+ }
+ }
+
+ this.fieldMetadata[fieldName] = fieldData;
+ }
+ }
+
+ /**
+ * Gets all documents in the same Freeform view as the ChatBox
+ * Uses the LinkManager to get all linked documents, similar to how ChatBox does it
+ */
+ private findDocumentsInFreeformView() {
+ // Reset collections
+ this.documentsById.clear();
+ this.layoutDocsById.clear();
+ this.dataDocsById.clear();
+
+ try {
+ // Use the LinkManager approach which is proven to work in ChatBox
+ if (this.chatBoxDocument) {
+ console.log('Finding documents linked to ChatBox document with ID:', this.chatBoxDocument.id);
+
+ // Get directly linked documents via LinkManager
+ const linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.chatBoxDocument)
+ .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.chatBoxDocument!)))
+ .map(d => DocCast(d?.annotationOn, d))
+ .filter(d => d);
+
+ console.log(`Found ${linkedDocs.length} linked documents via LinkManager`);
+
+ // Process the linked documents
+ linkedDocs.forEach((doc: Doc) => {
+ if (doc) {
+ this.processDocument(doc);
+ }
+ });
+
+ // Include the ChatBox document itself
+ this.processDocument(this.chatBoxDocument);
+
+ // If we have access to the Document's parent, try to find sibling documents
+ if (this.chatBoxDocument.parent) {
+ const parent = this.chatBoxDocument.parent;
+ console.log('Found parent document, checking for siblings');
+
+ // Check if parent is a Doc type and has a childDocs function
+ if (parent && typeof parent === 'object' && 'childDocs' in parent && typeof parent.childDocs === 'function') {
+ try {
+ const siblingDocs = parent.childDocs();
+ if (Array.isArray(siblingDocs)) {
+ console.log(`Found ${siblingDocs.length} sibling documents via parent.childDocs()`);
+ siblingDocs.forEach((doc: Doc) => {
+ if (doc) {
+ this.processDocument(doc);
+ }
+ });
+ }
+ } catch (e) {
+ console.warn('Error accessing parent.childDocs:', e);
+ }
+ }
+ }
+ } else if (this.chatBox && this.chatBox.linkedDocs) {
+ // If we have direct access to the linkedDocs computed property from ChatBox
+ console.log('Using ChatBox.linkedDocs directly');
+ const linkedDocs = this.chatBox.linkedDocs;
+ if (Array.isArray(linkedDocs)) {
+ console.log(`Found ${linkedDocs.length} documents via ChatBox.linkedDocs`);
+ linkedDocs.forEach((doc: Doc) => {
+ if (doc) {
+ this.processDocument(doc);
+ }
+ });
+ }
+
+ // Process the ChatBox document if available
+ if (this.chatBox.Document) {
+ this.processDocument(this.chatBox.Document);
+ }
+ } else {
+ console.warn('No ChatBox document reference available for finding linked documents');
+ }
+
+ console.log(`DocumentMetadataTool found ${this.documentsById.size} total documents`);
+
+ // If we didn't find any documents, try a fallback method
+ if (this.documentsById.size === 0 && this.chatBox) {
+ console.log('No documents found, trying fallback method');
+
+ // Try to access any field that might contain documents
+ if (this.chatBox.props && this.chatBox.props.documents) {
+ const documents = this.chatBox.props.documents;
+ if (Array.isArray(documents)) {
+ console.log(`Found ${documents.length} documents via ChatBox.props.documents`);
+ documents.forEach((doc: Doc) => {
+ if (doc) {
+ this.processDocument(doc);
+ }
+ });
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error finding documents in Freeform view:', error);
+ }
+ }
+
+ /**
+ * Process a document by ensuring it has an ID and adding it to the appropriate collections
+ * @param doc The document to process
+ */
+ private processDocument(doc: Doc) {
+ // Ensure document has a persistent ID
+ const docId = this.ensureDocumentId(doc);
+
+ // Only add if we haven't already processed this document
+ if (!this.documentsById.has(docId)) {
+ this.documentsById.set(docId, doc);
+
+ // Get layout doc (the document itself or its layout)
+ const layoutDoc = Doc.Layout(doc);
+ if (layoutDoc) {
+ this.layoutDocsById.set(docId, layoutDoc);
+ }
+
+ // Get data doc
+ const dataDoc = doc[DocData];
+ if (dataDoc) {
+ this.dataDocsById.set(docId, dataDoc);
+ }
+ }
+ }
+
+ /**
+ * Ensures a document has a persistent ID stored in its metadata
+ * @param doc The document to ensure has an ID
+ * @returns The document's ID
+ */
+ private ensureDocumentId(doc: Doc): string {
+ let docId: string | undefined;
+
+ // First try to get the ID from our custom field
+ if (doc[this.DOCUMENT_ID_FIELD]) {
+ docId = String(doc[this.DOCUMENT_ID_FIELD]);
+ return docId;
+ }
+
+ // Try different ways to get a document ID
+
+ // 1. Try the direct id property if it exists
+ if (doc.id && typeof doc.id === 'string') {
+ docId = doc.id;
+ }
+ // 2. Try doc._id if it exists
+ else if (doc._id && typeof doc._id === 'string') {
+ docId = doc._id;
+ }
+ // 3. Try doc.data?.id if it exists
+ else if (doc.data && typeof doc.data === 'object' && 'id' in doc.data && typeof doc.data.id === 'string') {
+ docId = doc.data.id;
+ }
+ // 4. If none of the above work, generate a UUID
+ else {
+ docId = uuidv4();
+ console.log(`Generated new UUID for document with title: ${doc.title || 'Untitled'}`);
+ }
+
+ // Store the ID in the document's metadata so it persists
+ try {
+ doc[this.DOCUMENT_ID_FIELD] = docId;
+ } catch (e) {
+ console.warn(`Could not assign ID to document property`, e);
+ }
+
+ return docId;
+ }
+
+ /**
+ * Extracts metadata from a specific document
+ * @param docId The ID of the document to extract metadata from
+ * @returns An object containing the document's metadata
+ */
+ private extractDocumentMetadata(docId: string) {
+ const doc = this.documentsById.get(docId);
+ if (!doc) {
+ return null;
+ }
+
+ const layoutDoc = this.layoutDocsById.get(docId);
+ const dataDoc = this.dataDocsById.get(docId);
+
+ const metadata: Record<string, any> = {
+ id: docId,
+ title: doc.title || '',
+ type: doc.type || '',
+ fields: {
+ layout: {},
+ data: {},
+ },
+ fieldLocationMap: {},
+ };
+
+ // Process all known field definitions
+ Object.keys(this.fieldMetadata).forEach(fieldName => {
+ const fieldDef = this.fieldMetadata[fieldName];
+ const strippedName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName;
+
+ // Check if field exists on layout document
+ let layoutValue = undefined;
+ if (layoutDoc) {
+ layoutValue = layoutDoc[fieldName];
+ if (layoutValue !== undefined) {
+ // Field exists on layout document
+ metadata.fields.layout[fieldName] = this.formatFieldValue(layoutValue);
+ metadata.fieldLocationMap[strippedName] = 'layout';
+ }
+ }
+
+ // Check if field exists on data document
+ let dataValue = undefined;
+ if (dataDoc) {
+ dataValue = dataDoc[fieldName];
+ if (dataValue !== undefined) {
+ // Field exists on data document
+ metadata.fields.data[fieldName] = this.formatFieldValue(dataValue);
+ if (!metadata.fieldLocationMap[strippedName]) {
+ metadata.fieldLocationMap[strippedName] = 'data';
+ }
+ }
+ }
+
+ // For fields with stripped names (without leading underscore),
+ // also check if they exist on documents without the underscore
+ if (fieldName.startsWith('_')) {
+ const nonUnderscoreFieldName = fieldName.substring(1);
+
+ if (layoutDoc) {
+ const nonUnderscoreLayoutValue = layoutDoc[nonUnderscoreFieldName];
+ if (nonUnderscoreLayoutValue !== undefined) {
+ metadata.fields.layout[nonUnderscoreFieldName] = this.formatFieldValue(nonUnderscoreLayoutValue);
+ metadata.fieldLocationMap[nonUnderscoreFieldName] = 'layout';
+ }
+ }
+
+ if (dataDoc) {
+ const nonUnderscoreDataValue = dataDoc[nonUnderscoreFieldName];
+ if (nonUnderscoreDataValue !== undefined) {
+ metadata.fields.data[nonUnderscoreFieldName] = this.formatFieldValue(nonUnderscoreDataValue);
+ if (!metadata.fieldLocationMap[nonUnderscoreFieldName]) {
+ metadata.fieldLocationMap[nonUnderscoreFieldName] = 'data';
+ }
+ }
+ }
+ }
+ });
+
+ // Add common field aliases for easier discovery
+ // This helps users understand both width and _width refer to the same property
+ if (metadata.fields.layout._width !== undefined && metadata.fields.layout.width === undefined) {
+ metadata.fields.layout.width = metadata.fields.layout._width;
+ metadata.fieldLocationMap.width = 'layout';
+ }
+
+ if (metadata.fields.layout._height !== undefined && metadata.fields.layout.height === undefined) {
+ metadata.fields.layout.height = metadata.fields.layout._height;
+ metadata.fieldLocationMap.height = 'layout';
+ }
+
+ return metadata;
+ }
+
+ /**
+ * Edits a specific field on a document
+ * @param docId The ID of the document to edit
+ * @param fieldName The name of the field to edit
+ * @param fieldValue The new value for the field
+ * @param documentLocation Where to edit the field: 'layout', 'data', or 'auto'
+ * @returns Object with success status, message, and additional information
+ */
+ private editDocumentField(docId: string, fieldName: string, fieldValue: string, documentLocation: string = 'auto'): {
+ success: boolean;
+ message: string;
+ fieldName?: string;
+ originalFieldName?: string;
+ newValue?: any;
+ } {
+ // Normalize field name (handle with/without underscore)
+ let normalizedFieldName = fieldName.startsWith('_') ? fieldName : fieldName;
+ const strippedFieldName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName;
+
+ // Handle common field name aliases (width → _width, height → _height)
+ // Many document fields use '_' prefix for layout properties
+ if (fieldName === 'width') {
+ normalizedFieldName = '_width';
+ } else if (fieldName === 'height') {
+ normalizedFieldName = '_height';
+ }
+
+ // Get the documents
+ const doc = this.documentsById.get(docId);
+ if (!doc) {
+ return { success: false, message: `Document with ID ${docId} not found` };
+ }
+
+ const layoutDoc = this.layoutDocsById.get(docId);
+ const dataDoc = this.dataDocsById.get(docId);
+
+ if (!layoutDoc && !dataDoc) {
+ return { success: false, message: `Could not find layout or data document for document with ID ${docId}` };
+ }
+
+ try {
+ // Convert the field value to the appropriate type based on field metadata
+ const convertedValue = this.convertFieldValue(normalizedFieldName, fieldValue);
+
+ // Determine where to set the field value
+ let targetDoc: Doc | undefined;
+ let targetLocation: string;
+
+ if (documentLocation === 'layout') {
+ // Explicitly set on layout document
+ targetDoc = layoutDoc;
+ targetLocation = 'layout';
+ } else if (documentLocation === 'data') {
+ // Explicitly set on data document
+ targetDoc = dataDoc;
+ targetLocation = 'data';
+ } else {
+ // Auto-detect where to set the field
+
+ // Check if field exists on layout document (with or without underscore)
+ const existsOnLayout = layoutDoc &&
+ (layoutDoc[normalizedFieldName] !== undefined ||
+ layoutDoc[strippedFieldName] !== undefined);
+
+ // Check if field exists on data document (with or without underscore)
+ const existsOnData = dataDoc &&
+ (dataDoc[normalizedFieldName] !== undefined ||
+ dataDoc[strippedFieldName] !== undefined);
+
+ if (existsOnLayout) {
+ targetDoc = layoutDoc;
+ targetLocation = 'layout';
+ } else if (existsOnData) {
+ targetDoc = dataDoc;
+ targetLocation = 'data';
+ } else {
+ // Field doesn't exist on either document, default to layout document
+ targetDoc = layoutDoc || dataDoc;
+ targetLocation = layoutDoc ? 'layout' : 'data';
+ }
+ }
+
+ if (!targetDoc) {
+ return { success: false, message: `Target document (${documentLocation}) not available` };
+ }
+
+ // Set the field value on the target document
+ targetDoc[normalizedFieldName] = convertedValue;
+
+ return {
+ success: true,
+ message: `Successfully updated field '${normalizedFieldName}' on ${targetLocation} document (ID: ${docId})`,
+ fieldName: normalizedFieldName,
+ originalFieldName: fieldName,
+ newValue: convertedValue
+ };
+ } catch (error) {
+ console.error('Error editing document field:', error);
+ return {
+ success: false,
+ message: `Error updating field: ${error instanceof Error ? error.message : String(error)}`
+ };
+ }
+ }
+
+ /**
+ * Converts a string field value to the appropriate type based on field metadata
+ * @param fieldName The name of the field
+ * @param fieldValue The string value to convert
+ * @returns The converted value with the appropriate type
+ */
+ private convertFieldValue(fieldName: string, fieldValue: any): any {
+ // If fieldValue is already a number, we don't need to convert it from string
+ if (typeof fieldValue === 'number') {
+ return fieldValue;
+ }
+
+ // If fieldValue is not a string (and not a number), convert it to string
+ if (typeof fieldValue !== 'string') {
+ fieldValue = String(fieldValue);
+ }
+
+ // Get field metadata
+ const normalizedFieldName = fieldName.startsWith('_') ? fieldName : `_${fieldName}`;
+ const strippedFieldName = fieldName.startsWith('_') ? fieldName.substring(1) : fieldName;
+
+ // Check both versions of the field name in metadata
+ const fieldMeta = this.fieldMetadata[normalizedFieldName] || this.fieldMetadata[strippedFieldName];
+
+ // Special handling for width and height without metadata
+ if (!fieldMeta && (fieldName === '_width' || fieldName === '_height' || fieldName === 'width' || fieldName === 'height')) {
+ const num = Number(fieldValue);
+ return isNaN(num) ? fieldValue : num;
+ }
+
+ if (!fieldMeta) {
+ // If no metadata found, just return the string value
+ return fieldValue;
+ }
+
+ // Convert based on field type
+ const fieldType = fieldMeta.type;
+
+ if (fieldType === 'boolean') {
+ // Convert to boolean
+ return fieldValue.toLowerCase() === 'true';
+ } else if (fieldType === 'number') {
+ // Convert to number
+ const num = Number(fieldValue);
+ return isNaN(num) ? fieldValue : num;
+ } else if (fieldType === 'date') {
+ // Try to convert to date (stored as number timestamp)
+ try {
+ return new Date(fieldValue).getTime();
+ } catch (e) {
+ return fieldValue;
+ }
+ } else if (fieldType.includes('list') || fieldType.includes('array')) {
+ // Try to parse as JSON array
+ try {
+ return JSON.parse(fieldValue);
+ } catch (e) {
+ return fieldValue;
+ }
+ } else if (fieldType === 'json' || fieldType === 'object') {
+ // Try to parse as JSON object
+ try {
+ return JSON.parse(fieldValue);
+ } catch (e) {
+ return fieldValue;
+ }
+ }
+
+ // Default to string
+ return fieldValue;
+ }
+
+ /**
+ * Formats a field value for JSON output
+ * @param value The field value to format
+ * @returns A JSON-friendly representation of the field value
+ */
+ private formatFieldValue(value: any): any {
+ if (value === undefined || value === null) {
+ return null;
+ }
+
+ // Handle Doc objects
+ if (value instanceof Doc) {
+ return {
+ type: 'Doc',
+ id: value.id || this.ensureDocumentId(value),
+ title: value.title || '',
+ docType: value.type || '',
+ };
+ }
+
+ // Handle arrays and complex objects
+ if (typeof value === 'object') {
+ // If the object has a toString method, use it
+ if (value.toString && value.toString !== Object.prototype.toString) {
+ return value.toString();
+ }
+
+ try {
+ // Try to convert to JSON string
+ return JSON.stringify(value);
+ } catch (e) {
+ return '[Complex Object]';
+ }
+ }
+
+ // Return primitive values as is
+ return value;
+ }
+
+ /**
+ * Executes the DocumentMetadataTool to extract or edit metadata from documents in the Freeform view
+ * @param args The parameters for the tool execution
+ * @returns A promise that resolves to an array of Observation objects
+ */
+ async execute(args: ParametersType<DocumentMetadataToolParamsType>): Promise<Observation[]> {
+ console.log('DocumentMetadataTool executing with args:', args);
+
+ // Add diagnostic information about the ChatBox instance
+ if (this.chatBox) {
+ console.log('ChatBox instance available:', {
+ hasDocument: !!this.chatBoxDocument,
+ chatBoxProps: this.chatBox.props ? Object.keys(this.chatBox.props) : 'No props',
+ linkedDocsMethod: typeof this.chatBox.linkedDocs === 'function' ? 'Exists (function)' :
+ typeof this.chatBox.linkedDocs === 'object' ? 'Exists (object)' : 'Not available'
+ });
+ } else {
+ console.warn('No ChatBox instance available');
+ }
+
+ // Find all documents in the Freeform view
+ this.findDocumentsInFreeformView();
+
+ // Debug diagnostic information about all documents found
+ if (this.documentsById.size > 0) {
+ console.log('Documents found:', Array.from(this.documentsById.entries()).map(([id, doc]) => ({
+ id,
+ title: doc.title || 'Untitled',
+ type: doc.type || 'Unknown',
+ hasLayout: !!this.layoutDocsById.get(id),
+ hasData: !!this.dataDocsById.get(id)
+ })));
+ }
+
+ // Check if we found any documents
+ if (this.documentsById.size === 0) {
+ console.error('No documents found in Freeform view');
+ return [{
+ type: 'text',
+ text: JSON.stringify({
+ success: false,
+ message: 'No documents found in the current view. Unable to extract or edit metadata.',
+ chatBoxDocumentId: this.chatBoxDocument ? this.chatBoxDocument.id : 'No ChatBox document',
+ diagnostics: {
+ chatBoxAvailable: !!this.chatBox,
+ documentAvailable: !!this.chatBoxDocument,
+ toolInitializedWith: this.chatBox ?
+ (this.chatBox.constructor && this.chatBox.constructor.name) || 'Unknown constructor'
+ : 'No ChatBox'
+ }
+ }, null, 2)
+ }];
+ }
+
+ let result: Record<string, any>;
+
+ // Determine which action to perform
+ const action = args.action?.toLowerCase();
+
+ if (action === 'list') {
+ // Just return a simple list of available documents for quick reference
+ const documentList = Array.from(this.documentsById.entries()).map(([id, doc]) => ({
+ id,
+ title: doc.title || 'Untitled',
+ type: doc.type || 'Unknown'
+ }));
+
+ result = {
+ success: true,
+ message: `Found ${documentList.length} documents in the current view`,
+ documents: documentList
+ };
+ }
+ else if (action === 'edit') {
+ // Edit document metadata
+ if (!args.documentId) {
+ return [{
+ type: 'text',
+ text: JSON.stringify({
+ success: false,
+ message: 'Document ID is required for edit operations',
+ availableDocuments: Array.from(this.documentsById.keys())
+ }, null, 2)
+ }];
+ }
+
+ if (!args.fieldName) {
+ return [{
+ type: 'text',
+ text: JSON.stringify({
+ success: false,
+ message: 'Field name is required for edit operations',
+ }, null, 2)
+ }];
+ }
+
+ if (args.fieldValue === undefined) {
+ return [{
+ type: 'text',
+ text: JSON.stringify({
+ success: false,
+ message: 'Field value is required for edit operations',
+ }, null, 2)
+ }];
+ }
+
+ // Check if the document exists
+ if (!this.documentsById.has(args.documentId)) {
+ return [{
+ type: 'text',
+ text: JSON.stringify({
+ success: false,
+ message: `Document with ID ${args.documentId} not found`,
+ availableDocuments: Array.from(this.documentsById.keys())
+ }, null, 2)
+ }];
+ }
+
+ // Convert fieldValue to string if it's not already
+ // This is to support the Agent passing numeric values directly
+ const fieldValue = typeof args.fieldValue === 'string' ?
+ args.fieldValue :
+ String(args.fieldValue);
+
+ // Perform the edit
+ const editResult = this.editDocumentField(
+ args.documentId,
+ args.fieldName,
+ fieldValue,
+ args.documentLocation || 'auto'
+ );
+
+ // If successful, also get the updated metadata
+ if (editResult.success) {
+ const documentMetadata = this.extractDocumentMetadata(args.documentId);
+ result = {
+ ...editResult,
+ document: documentMetadata,
+ };
+ } else {
+ result = editResult;
+ }
+ } else if (action === 'get') {
+ // Get document metadata
+ if (args.documentId) {
+ // Check if the document exists
+ if (!this.documentsById.has(args.documentId)) {
+ return [{
+ type: 'text',
+ text: JSON.stringify({
+ success: false,
+ message: `Document with ID ${args.documentId} not found`,
+ availableDocuments: Array.from(this.documentsById.keys())
+ }, null, 2)
+ }];
+ }
+
+ // Get metadata for a specific document
+ const documentMetadata = this.extractDocumentMetadata(args.documentId);
+ result = {
+ success: !!documentMetadata,
+ message: documentMetadata ? 'Document metadata retrieved successfully' : `Document with ID ${args.documentId} not found`,
+ document: documentMetadata,
+ fieldDefinitions: this.fieldMetadata,
+ };
+ } else {
+ // Get metadata for all documents
+ const documentsMetadata: Record<string, any> = {};
+ for (const docId of this.documentsById.keys()) {
+ documentsMetadata[docId] = this.extractDocumentMetadata(docId);
+ }
+
+ result = {
+ success: true,
+ message: `Retrieved metadata for ${this.documentsById.size} documents`,
+ documents: documentsMetadata,
+ fieldDefinitions: this.fieldMetadata,
+ documentCount: this.documentsById.size,
+ fieldMetadataCount: Object.keys(this.fieldMetadata).length,
+ };
+ }
+ } else {
+ // Invalid action
+ result = {
+ success: false,
+ message: `Invalid action: ${action}. Valid actions are "list", "get", and "edit".`,
+ availableActions: ["list", "get", "edit"]
+ };
+ }
+
+ // Log the result to the console
+ console.log('Document Metadata Tool Result:', result);
+
+ // Return the result as an observation
+ // Convert to string format as the Observation type only supports 'text' or 'image_url' types
+ return [{
+ type: 'text',
+ text: JSON.stringify(result, null, 2)
+ }];
+ }
+
+ /**
+ * Validates the input parameters for the DocumentMetadataTool
+ * This custom validator allows numbers 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 edit action, documentId, fieldName, and fieldValue are required
+ if (params.action === 'edit') {
+ if (!params.documentId || !params.fieldName || params.fieldValue === undefined) {
+ return false;
+ }
+ }
+
+ // For get action with documentId, documentId is required
+ if (params.action === 'get' && params.documentId === '') {
+ return false;
+ }
+
+ // Allow for numeric fieldValue even though the type is defined as string
+ if (params.fieldValue !== undefined && typeof params.fieldValue === 'number') {
+ console.log('Numeric fieldValue detected, will be converted to string');
+ // We'll convert it later, so don't fail validation
+ return true;
+ }
+
+ return true;
+ }
+} \ No newline at end of file