Skip to main content
Glama

modify_form

Update existing Tally form details including title, description, and field configurations to adapt forms to changing requirements.

Instructions

Modify an existing Tally form

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
formIdYesID of the form to modify
titleNoNew form title
descriptionNoNew form description
fieldsNo

Implementation Reference

  • Registration of the 'modify_form' tool including name, description, and detailed input schema definition
      name: 'modify_form',
      description: 'Modify an existing Tally form',
      inputSchema: {
        type: 'object',
        properties: {
          formId: { type: 'string', description: 'ID of the form to modify' },
          title: { type: 'string', description: 'New form title' },
          description: { type: 'string', description: 'New form description' },
          fields: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                type: { type: 'string', enum: ['text', 'email', 'number', 'textarea', 'select', 'checkbox', 'radio'] },
                label: { type: 'string' },
                required: { type: 'boolean' },
                options: { type: 'array', items: { type: 'string' } }
              },
              required: ['type', 'label']
            }
          }
        },
        required: ['formId']
      }
    },
  • Input schema definition for 'modify_form' tool validating formId (required), title, description, and fields array
      name: 'modify_form',
      description: 'Modify an existing Tally form',
      inputSchema: {
        type: 'object',
        properties: {
          formId: { type: 'string', description: 'ID of the form to modify' },
          title: { type: 'string', description: 'New form title' },
          description: { type: 'string', description: 'New form description' },
          fields: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                type: { type: 'string', enum: ['text', 'email', 'number', 'textarea', 'select', 'checkbox', 'radio'] },
                label: { type: 'string' },
                required: { type: 'boolean' },
                options: { type: 'array', items: { type: 'string' } }
              },
              required: ['type', 'label']
            }
          }
        },
        required: ['formId']
      }
    },
  • FormModificationTool class - primary handler for form modifications. Currently stubs retrieval but intended for full modify_form logic using Tally API
    export class FormModificationTool implements Tool<FormModificationArgs, FormModificationResult> {
      public readonly name = 'form_modification_tool';
      public readonly description = 'Modifies existing Tally forms through natural language commands';
    
      private tallyApiService: TallyApiService;
    
      constructor(apiClientConfig: TallyApiClientConfig) {
        this.tallyApiService = new TallyApiService(apiClientConfig);
      }
    
      /**
       * Execute the form modification based on natural language command
       */
      public async execute(args: FormModificationArgs): Promise<FormModificationResult> {
        if (!args.formId) {
          return {
            success: false,
            status: 'error',
            message: 'Form ID is required',
            errors: ['Missing form ID']
          } as any;
        }
        try {
          const form = await this.tallyApiService.getForm(args.formId);
          if (!form) {
            return {
              success: false,
              status: 'error',
              message: `Form with ID ${args.formId} not found`,
              errors: [`Form ${args.formId} does not exist`]
            } as any;
          }
          return {
            success: true,
            status: 'success',
            message: `Successfully retrieved form "${form.title}"`,
            modifiedForm: form, // for test compatibility
            finalFormConfig: undefined,
            changes: [`Retrieved form: ${form.title}`]
          } as any;
        } catch (error) {
          return {
            success: false,
            status: 'error',
            message: `Form with ID ${args.formId} not found`,
            errors: [`Form ${args.formId} does not exist`]
          } as any;
        }
      }
    
      // Add missing methods for test compatibility
      public async getForm(formId: string): Promise<TallyForm | null> {
        try {
          return await this.tallyApiService.getForm(formId);
        } catch (e) {
          return null;
        }
      }
    
      public async getForms(options: any = {}): Promise<TallyFormsResponse | null> {
        try {
          return await this.tallyApiService.getForms(options);
        } catch (e) {
          return null;
        }
      }
    
      public async updateForm(formId: string, config: any): Promise<TallyForm | null> {
        try {
          return await this.tallyApiService.updateForm(formId, config);
        } catch (e) {
          return null;
        }
      }
    
      public async patchForm(formId: string, updates: any): Promise<TallyForm | null> {
        try {
          return await this.tallyApiService.patchForm(formId, updates);
        } catch (e) {
          return null;
        }
      }
    
      public async validateConnection(): Promise<boolean> {
        try {
          const result = await this.getForms({ limit: 1 });
          return !!result;
        } catch (e) {
          return false;
        }
      }
    }
  • Core helper implementing all form modification operations: add/remove/update/reorder fields, manage required status, title/description changes, option management
    export class FormModificationOperations {
      
      /**
       * Execute a parsed modification command on a form
       */
      public executeOperation(
        command: ParsedModificationCommand, 
        baseForm: TallyForm | null,
        currentFormConfig?: FormConfig
      ): ModificationOperationResult {
        try {
          // Convert TallyForm to FormConfig if not provided
          const formConfig = currentFormConfig || (baseForm ? this.convertTallyFormToFormConfig(baseForm) : undefined);
    
          if (!formConfig) {
            return {
              success: false,
              message: 'An error occurred while executing the modification operation',
              changes: [],
              errors: ['No form config provided or derivable.']
            }
          }
          
          let result: ModificationOperationResult;
    
          switch (command.operation) {
            case ModificationOperation.ADD_FIELD:
              result = this.addField(formConfig, command.parameters);
              break;
            case ModificationOperation.REMOVE_FIELD:
              result = this.removeField(formConfig, command.parameters);
              break;
            case ModificationOperation.MAKE_REQUIRED:
              result = this.makeFieldRequired(formConfig, command.parameters, true);
              break;
            case ModificationOperation.MAKE_OPTIONAL:
              result = this.makeFieldRequired(formConfig, command.parameters, false);
              break;
            case ModificationOperation.UPDATE_TITLE:
              result = this.updateFormTitle(formConfig, command.parameters);
              break;
            case ModificationOperation.UPDATE_DESCRIPTION:
              result = this.updateFormDescription(formConfig, command.parameters);
              break;
            case ModificationOperation.REORDER_FIELD:
              result = this.reorderField(formConfig, command.parameters);
              break;
            case ModificationOperation.ADD_OPTION:
              result = this.addOption(formConfig, command.parameters);
              break;
            case ModificationOperation.MODIFY_FIELD:
              result = this.modifyField(formConfig, command.parameters);
              break;
            default:
              result = {
                success: false,
                message: `Operation ${command.operation} is not yet implemented`,
                changes: [],
                errors: [`Unsupported operation: ${command.operation}`]
              };
          }
    
          // If modification was successful, update version and timestamp
          if (result.success && result.modifiedFormConfig) {
            const originalVersion = formConfig.metadata?.version || 1;
            
            if (!result.modifiedFormConfig.metadata) {
              result.modifiedFormConfig.metadata = {};
            }
            result.modifiedFormConfig.metadata.version = originalVersion + 1;
            result.modifiedFormConfig.metadata.updatedAt = new Date().toISOString();
    
            // Add version info to changes
            result.changes.push(`Form version incremented to ${result.modifiedFormConfig.metadata.version}`);
          }
    
          return result;
    
        } catch (error) {
          return {
            success: false,
            message: 'An error occurred while executing the modification operation',
            changes: [],
            errors: [error instanceof Error ? error.message : 'Unknown error']
          };
        }
      }
    
      /**
       * Add a new field to the form
       */
      private addField(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult {
        if (!params.fieldType) {
          return {
            success: false,
            message: 'Field type is required for adding a field',
            changes: [],
            errors: ['Missing field type']
          };
        }
    
        const newFieldId = this.generateNextFieldId(formConfig);
        const fieldLabel = params.fieldLabel || this.generateDefaultFieldLabel(params.fieldType || QuestionType.TEXT);
        
        // Create properly configured question based on type
        const newField = this.createQuestionConfig(
          params.fieldType as QuestionType,
          newFieldId,
          fieldLabel || 'Untitled Field',
          params
        );
    
        const updatedFormConfig: FormConfig = {
          ...formConfig,
          questions: [...formConfig.questions, newField]
        };
    
        return {
          success: true,
          modifiedFormConfig: updatedFormConfig,
          message: `Successfully added ${fieldLabel} field`,
          changes: [
            `Added new field: ${fieldLabel}`,
            `Field type: ${params.fieldType}`,
            `Field ID: ${newFieldId}`,
            `Position: ${updatedFormConfig.questions.length}`
          ]
        };
      }
    
      /**
       * Remove a field from the form
       */
      private removeField(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult {
        const lookupOptions: FieldLookupOptions = { formConfig };
        if (params.fieldId) lookupOptions.fieldId = params.fieldId;
        if (params.fieldLabel) lookupOptions.fieldLabel = params.fieldLabel;
        if (params.fieldNumber) lookupOptions.fieldNumber = params.fieldNumber;
        const field = this.findField(lookupOptions);
        
        if (!field.found || typeof field.index !== 'number') {
          return {
            success: false,
            message: field.error || 'Field not found',
            changes: [],
            errors: [field.error || 'Field not found']
          };
        }
    
        const fieldIndex = field.index;
        const removedField = formConfig.questions[fieldIndex];
        if (!removedField?.id) {
          return {
            success: false,
            message: `Field at index ${fieldIndex} is invalid or missing an ID.`,
            changes: [],
            errors: [`Invalid field at index ${fieldIndex}`],
          };
        }
        const fieldIdToRemove = removedField.id;
    
        // Dependency Validation
        const validationErrors = DependencyValidator.validateFieldRemoval(formConfig, fieldIdToRemove);
        if (validationErrors.length > 0) {
          return {
            success: false,
            message: 'Cannot remove field due to broken dependencies.',
            changes: [],
            errors: validationErrors,
          };
        }
    
        const updatedQuestions = formConfig.questions.filter((_, index) => index !== fieldIndex);
    
        const updatedFormConfig: FormConfig = {
          ...formConfig,
          questions: updatedQuestions
        };
    
        // Post-removal validation
        const postRemovalErrors = DependencyValidator.validateAllLogic(updatedFormConfig);
        if (postRemovalErrors.length > 0) {
          return {
            success: false,
            message: 'An unexpected validation error occurred after field removal.',
            changes: [],
            errors: postRemovalErrors,
          };
        }
    
        return {
          success: true,
          modifiedFormConfig: updatedFormConfig,
          message: `Successfully removed field "${removedField.label}"`,
          changes: [
            `Removed field: ${removedField.label}`,
            `Field type: ${removedField.type}`,
            `Previous position: ${fieldIndex + 1}`,
            `Remaining fields: ${updatedQuestions.length}`
          ]
        };
      }
    
      /**
       * Update field required status
       */
      private makeFieldRequired(
        formConfig: FormConfig, 
        params: ModificationParameters, 
        required: boolean
      ): ModificationOperationResult {
        const lookupOptions: FieldLookupOptions = { formConfig };
        if (params.fieldId) lookupOptions.fieldId = params.fieldId;
        if (params.fieldLabel) lookupOptions.fieldLabel = params.fieldLabel;
        if (params.fieldNumber) lookupOptions.fieldNumber = params.fieldNumber;
        const field = this.findField(lookupOptions);
        
        if (!field.found || typeof field.index !== 'number') {
          return {
            success: false,
            message: field.error || 'Field not found',
            changes: [],
            errors: [field.error || 'Field not found']
          };
        }
    
        const fieldIndex = field.index;
        const updatedQuestions = [...formConfig.questions];
        const targetQuestion = updatedQuestions[fieldIndex];
    
        if (!targetQuestion) {
          return {
            success: false,
            message: 'Field not found at the specified index.',
            changes: [],
            errors: [`Invalid index ${fieldIndex}`]
          }
        }
    
        updatedQuestions[fieldIndex] = {
          ...targetQuestion,
          required
        };
        
        const updatedFormConfig: FormConfig = {
          ...formConfig,
          questions: updatedQuestions
        };
    
        return {
          success: true,
          modifiedFormConfig: updatedFormConfig,
          message: `Successfully set "required" to ${required} for field "${targetQuestion.label}"`,
          changes: [
            `Field: ${targetQuestion.label}`,
            `Property: required`,
            `New value: ${required}`,
            `Set required: ${required}`
          ]
        };
      }
    
      /**
       * Update the form title
       */
      private updateFormTitle(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult {
        if (!params.newValue) {
          return {
            success: false,
            message: 'New title value is required',
            changes: [],
            errors: ['Missing new title']
          };
        }
        
        const updatedFormConfig: FormConfig = {
          ...formConfig,
          title: params.newValue
        };
    
        return {
          success: true,
          modifiedFormConfig: updatedFormConfig,
          message: `Successfully updated form title to "${params.newValue}"`,
          changes: [
            `Updated form title`,
            `Previous title: ${formConfig.title}`,
            `New title: ${params.newValue}`,
            `Changed title to: "${params.newValue}"`
          ]
        };
      }
    
      /**
       * Update the form description
       */
      private updateFormDescription(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult {
        const newDescription = params.newValue || '';
        
        const updatedFormConfig: FormConfig = {
          ...formConfig,
          description: newDescription
        };
    
        return {
          success: true,
          modifiedFormConfig: updatedFormConfig,
          message: 'Successfully updated form description',
          changes: [
            `Updated form description`,
            `Previous description: ${formConfig.description || ''}`,
            `New description: ${newDescription}`,
            `Changed description to: "${newDescription}"`
          ]
        };
      }
    
      /**
       * Reorder a field in the form
       */
      private reorderField(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult {
        const { sourcePosition, targetPosition, fieldId, fieldLabel } = params;
    
        if (sourcePosition === undefined && fieldId === undefined && fieldLabel === undefined) {
          return {
            success: false,
            message: 'Source field identifier (position, ID, or label) is required for reordering',
            changes: [],
            errors: ['Missing source field identifier']
          };
        }
    
        if (targetPosition === undefined) {
          return {
            success: false,
            message: 'Target position is required for reordering',
            changes: [],
            errors: ['Missing target position']
          };
        }
    
        const lookupOptions: FieldLookupOptions = { formConfig };
        if (fieldId) lookupOptions.fieldId = fieldId;
        if (fieldLabel) lookupOptions.fieldLabel = fieldLabel;
        
        const sourceIndex = sourcePosition !== undefined 
          ? sourcePosition - 1 
          : this.findField(lookupOptions).index;
    
        if (sourceIndex === undefined || sourceIndex < 0 || sourceIndex >= formConfig.questions.length) {
          return {
            success: false,
            message: 'Source field not found or out of range',
            changes: [],
            errors: ['Invalid source field for reorder']
          };
        }
    
        const targetIndex = targetPosition - 1;
        if (targetIndex < 0 || targetIndex >= formConfig.questions.length) {
          return {
            success: false,
            message: 'Target position is out of range',
            changes: [],
            errors: ['Invalid target position for reorder']
          };
        }
        
        const updatedQuestions = [...formConfig.questions];
        const [movedField] = updatedQuestions.splice(sourceIndex, 1);
        
        if (!movedField) {
          return {
            success: false,
            message: 'Could not extract field to be moved.',
            changes: [],
            errors: ['Splice operation failed to return the field.'],
          };
        }
    
        updatedQuestions.splice(targetIndex, 0, movedField);
    
        const updatedFormConfig: FormConfig = {
          ...formConfig,
          questions: updatedQuestions
        };
    
        return {
          success: true,
          modifiedFormConfig: updatedFormConfig,
          message: `Successfully moved field "${movedField.label}" to position ${targetPosition}`,
          changes: [
            `Moved field: ${movedField.label}`,
            `Previous position: ${sourcePosition}`,
            `New position: ${targetPosition}`,
            `From position: ${sourcePosition}`,
            `To position: ${targetPosition}`
          ]
        };
      }
    
      /**
       * Add an option to a choice-type field
       */
      private addOption(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult {
        const { optionText, fieldId, fieldLabel, fieldNumber } = params;
        if (!optionText) {
          return {
            success: false,
            message: 'Option text is required',
            changes: [],
            errors: ['Missing new option']
          };
        }
    
        const lookupOptions: FieldLookupOptions = { formConfig };
        if (fieldId) lookupOptions.fieldId = fieldId;
        if (fieldLabel) lookupOptions.fieldLabel = fieldLabel;
        if (fieldNumber) lookupOptions.fieldNumber = fieldNumber;
        const field = this.findField(lookupOptions);
        if (!field.found || field.index === undefined) {
          return {
            success: false,
            message: field.error || 'Field not found',
            changes: [],
            errors: [field.error || 'Field not found']
          };
        }
    
        const fieldIndex = field.index;
        const targetField = formConfig.questions[fieldIndex];
    
        if (!targetField) {
          return {
            success: false,
            message: 'Target field is undefined.',
            changes: [],
            errors: ['Field not found at the specified index.'],
          };
        }
    
        if (!this.isChoiceField(targetField.type)) {
          return {
            success: false,
            message: `Cannot add options to field type ${targetField.type}`,
            changes: [],
            errors: [`Invalid field type for adding options: ${targetField.type}`]
          };
        }
    
        const updatedQuestions = [...formConfig.questions];
        const updatedField = { ...updatedQuestions[fieldIndex] } as any;
    
        if (!updatedField.options) {
          updatedField.options = [];
        }
    
        const newQuestionOption: QuestionOption = {
          text: optionText,
          value: optionText.toLowerCase().replace(/\s+/g, '_')
        };
    
        updatedField.options.push(newQuestionOption);
        updatedQuestions[fieldIndex] = updatedField;
    
        const updatedFormConfig: FormConfig = {
          ...formConfig,
          questions: updatedQuestions
        };
    
        return {
          success: true,
          modifiedFormConfig: updatedFormConfig,
          message: `Successfully added option "${optionText}" to field "${targetField.label}"`,
          changes: [
            `Field: ${targetField.label}`,
            `Added option: ${optionText}`,
            `Total options: ${updatedField.options.length}`
          ]
        };
      }
    
      /**
       * Modify properties of an existing field
       */
      private modifyField(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult {
        const lookupOptions: FieldLookupOptions = { formConfig };
        if (params.fieldId) lookupOptions.fieldId = params.fieldId;
        if (params.fieldNumber) lookupOptions.fieldNumber = params.fieldNumber;
        // Don't use fieldLabel for lookup in modify operations - it's the new value
        
        const field = this.findField(lookupOptions);
        
        if (!field.found || field.index === undefined) {
          return {
            success: false,
            message: field.error || 'Field not found for modification',
            changes: [],
            errors: [field.error || 'Field not found']
          };
        }
    
        const fieldIndex = field.index;
        const updatedQuestions = [...formConfig.questions];
        const currentField = updatedQuestions[fieldIndex];
    
        if (!currentField) {
          return {
            success: false,
            message: `Field at index ${fieldIndex} not found.`,
            changes: [],
            errors: [`Invalid field index ${fieldIndex}`],
          };
        }
    
        const changes: string[] = [];
        const updatedField = { ...currentField };
    
        // Handle fieldLabel as the new label value (not for lookup)
        if (params.fieldLabel) {
          changes.push(`Updated label from "${currentField.label}" to "${params.fieldLabel}"`);
          updatedField.label = params.fieldLabel;
        }
        if (params.newValue) {
          changes.push(`Updated label from "${currentField.label}" to "${params.newValue}"`);
          updatedField.label = params.newValue;
        }
        if (params.description !== undefined) {
          changes.push(`Description updated from "${currentField.description || ''}" to "${params.description}"`);
          updatedField.description = params.description;
        }
        if (params.placeholder !== undefined) {
          changes.push(`Placeholder updated from "${currentField.placeholder || ''}" to "${params.placeholder}"`);
          updatedField.placeholder = params.placeholder;
        }
        if (params.required !== undefined) {
          changes.push(`Required updated from ${currentField.required} to ${params.required}`);
          updatedField.required = params.required;
        }
    
        if (changes.length === 0) {
          return {
            success: false,
            message: 'No modifications specified',
            changes: [],
            errors: ['No modification parameters specified']
          };
        }
    
        updatedQuestions[fieldIndex] = updatedField;
    
        const updatedFormConfig: FormConfig = {
          ...formConfig,
          questions: updatedQuestions
        };
    
        return {
          success: true,
          modifiedFormConfig: updatedFormConfig,
          message: `Successfully modified field "${updatedField.label}"`,
          changes
        };
      }
    
      /**
       * Find a field in the form by number, ID, or label
       */
      private findField(options: FieldLookupOptions): { found: boolean; index?: number; error?: string } {
        const { formConfig, fieldNumber, fieldId, fieldLabel } = options;
        
        if (fieldNumber !== undefined) {
          const index = fieldNumber - 1;
          if (index < 0 || index >= formConfig.questions.length) {
            return { found: false, error: `Field number ${fieldNumber} is out of range` };
          }
          return { found: true, index };
        }
        
        if (fieldId) {
          const index = formConfig.questions.findIndex(q => q.id === fieldId);
          if (index === -1) {
            return { found: false, error: `Field with ID "${fieldId}" not found` };
          }
          return { found: true, index };
        }
        
        if (fieldLabel) {
          const lowercasedLabel = fieldLabel.toLowerCase();
          const index = formConfig.questions.findIndex(q => q.label.toLowerCase().includes(lowercasedLabel));
          if (index === -1) {
            return { found: false, error: `Field with label "${fieldLabel}" not found` };
          }
          return { found: true, index };
        }
    
        return { found: false, error: 'No field identifier provided (number, ID, or label)' };
      }
    
      /**
       * Generate a unique field ID using UUID
       * This ensures stable, unique identifiers across all form fields
       */
      private generateNextFieldId(formConfig: FormConfig): string {
        const existingIds = new Set(formConfig.questions.map(q => q.id).filter(id => id));
        
        // Generate a new UUID and ensure it's unique (extremely unlikely collision)
        let newId = randomUUID();
        while (existingIds.has(newId)) {
          newId = randomUUID();
        }
        
        return newId;
      }
    
      /**
       * Generate a default label for a new field
       */
      private generateDefaultFieldLabel(fieldType: QuestionType | string): string {
        const labelMap: Record<string, string> = {
          [QuestionType.TEXT]: 'Text Field',
          [QuestionType.EMAIL]: 'Email Address',
          [QuestionType.NUMBER]: 'Number Field',
          [QuestionType.CHOICE]: 'Choice Field',
          [QuestionType.RATING]: 'Rating Field',
          [QuestionType.FILE]: 'File Upload',
          [QuestionType.DATE]: 'Date Field',
          [QuestionType.TIME]: 'Time Field',
          [QuestionType.TEXTAREA]: 'Text Area',
          [QuestionType.DROPDOWN]: 'Dropdown Field',
          [QuestionType.CHECKBOXES]: 'Checkboxes Field',
          [QuestionType.LINEAR_SCALE]: 'Linear Scale',
          [QuestionType.MULTIPLE_CHOICE]: 'Multiple Choice',
          [QuestionType.PHONE]: 'Phone Number',
          [QuestionType.URL]: 'URL Field',
          [QuestionType.SIGNATURE]: 'Signature Field',
          [QuestionType.PAYMENT]: 'Payment Field',
          [QuestionType.MATRIX]: 'Matrix Field',
        };
    
        return labelMap[fieldType as QuestionType] || `${fieldType.toString().replace(/_/g, ' ')} Field`;
      }
    
      /**
       * Check if a field type supports options
       */
      private isChoiceField(fieldType: QuestionType | string): boolean {
        return [
          QuestionType.MULTIPLE_CHOICE,
          QuestionType.DROPDOWN,
          QuestionType.CHECKBOXES,
          QuestionType.MATRIX
        ].includes(fieldType as QuestionType);
      }
    
      /**
       * Create a properly configured question based on its type
       */
      private createQuestionConfig(
        fieldType: QuestionType,
        fieldId: string,
        fieldLabel: string,
        params: ModificationParameters
      ): QuestionConfig {
        const baseConfig = {
          id: fieldId,
          label: fieldLabel,
          required: false,
          description: params.description,
          placeholder: params.placeholder
        };
    
        switch (fieldType) {
          case QuestionType.RATING:
            return this.createRatingQuestionConfig(baseConfig, params);
          
          case QuestionType.DATE:
            return this.createDateQuestionConfig(baseConfig, params);
          
          case QuestionType.NUMBER:
            return this.createNumberQuestionConfig(baseConfig, params);
          
          case QuestionType.LINEAR_SCALE:
            return this.createLinearScaleQuestionConfig(baseConfig, params);
          
          case QuestionType.CHOICE:
          case QuestionType.MULTIPLE_CHOICE:
          case QuestionType.DROPDOWN:
          case QuestionType.CHECKBOXES:
            return this.createChoiceQuestionConfig(fieldType, baseConfig, params);
          
          case QuestionType.TIME:
            return this.createTimeQuestionConfig(baseConfig, params);
          
          case QuestionType.FILE:
            return this.createFileQuestionConfig(baseConfig, params);
          
          case QuestionType.SIGNATURE:
            return this.createSignatureQuestionConfig(baseConfig, params);
          
          case QuestionType.PAYMENT:
            return this.createPaymentQuestionConfig(baseConfig, params);
          
          case QuestionType.MATRIX:
            return this.createMatrixQuestionConfig(baseConfig, params);
          
          default:
            // For other types, return basic configuration
            return {
              ...baseConfig,
              type: fieldType
            };
        }
      }
    
      /**
       * Create a rating question configuration
       */
      private createRatingQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.RATING,
          minRating: 1,
          maxRating: 5,
          style: 'stars' as any, // Default to stars
          showNumbers: false,
          lowLabel: 'Poor',
          highLabel: 'Excellent'
        };
      }
    
      /**
       * Create a date question configuration
       */
      private createDateQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.DATE,
          dateFormat: 'MM/DD/YYYY',
          includeTime: false
        };
      }
    
      /**
       * Create a number question configuration
       */
      private createNumberQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.NUMBER,
          step: 1,
          decimalPlaces: 0,
          useThousandSeparator: false
        };
      }
    
      /**
       * Create a linear scale question configuration
       */
      private createLinearScaleQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.LINEAR_SCALE,
          minValue: 1,
          maxValue: 10,
          step: 1,
          showNumbers: true,
          lowLabel: 'Not likely',
          highLabel: 'Very likely'
        };
      }
    
      /**
       * Create a choice question configuration (multiple choice, dropdown, checkboxes)
       */
      private createChoiceQuestionConfig(
        fieldType: QuestionType,
        baseConfig: any,
        params: ModificationParameters
      ): QuestionConfig {
        const defaultOptions = params.options ? 
          params.options.map((option: string) => ({
            text: option,
            value: option.toLowerCase().replace(/\s+/g, '_')
          })) : [
            { text: 'Option 1', value: 'option_1' },
            { text: 'Option 2', value: 'option_2' },
            { text: 'Option 3', value: 'option_3' }
          ];
    
        const config = {
          ...baseConfig,
          type: fieldType,
          options: defaultOptions,
          allowOther: false
        };
    
        // Add type-specific properties
        if (fieldType === QuestionType.CHECKBOXES) {
          return {
            ...config,
            minSelections: 0,
            maxSelections: undefined, // No limit
            layout: 'vertical' as any
          };
        }
    
        if (fieldType === QuestionType.DROPDOWN) {
          return {
            ...config,
            searchable: false,
            dropdownPlaceholder: 'Select an option...'
          };
        }
    
        if (fieldType === QuestionType.MULTIPLE_CHOICE) {
          return {
            ...config,
            randomizeOptions: false,
            layout: 'vertical' as any
          };
        }
    
        return config;
      }
    
      /**
       * Create a time question configuration
       */
      private createTimeQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.TIME,
          format: '12' as any, // 12-hour format
          minuteStep: 15
        };
      }
    
      /**
       * Create a file question configuration
       */
      private createFileQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.FILE,
          allowedTypes: ['image/*', 'application/pdf', '.doc', '.docx'],
          maxFileSize: 10, // 10MB
          maxFiles: 1,
          multiple: false,
          uploadText: 'Click to upload or drag and drop'
        };
      }
    
      /**
       * Create a signature question configuration
       */
      private createSignatureQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.SIGNATURE,
          canvasWidth: 400,
          canvasHeight: 200,
          penColor: '#000000',
          backgroundColor: '#ffffff'
        };
      }
    
      /**
       * Create a payment question configuration
       */
      private createPaymentQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.PAYMENT,
          currency: 'USD',
          fixedAmount: true,
          amount: 1000, // $10.00 in cents
          paymentDescription: 'Payment',
          acceptedMethods: ['card' as any]
        };
      }
    
      /**
       * Create a matrix question configuration
       */
      private createMatrixQuestionConfig(
        baseConfig: any,
        _params: ModificationParameters
      ): QuestionConfig {
        return {
          ...baseConfig,
          type: QuestionType.MATRIX,
          rows: [
            { id: 'row_1', text: 'Row 1', value: 'row_1' },
            { id: 'row_2', text: 'Row 2', value: 'row_2' },
            { id: 'row_3', text: 'Row 3', value: 'row_3' }
          ],
          columns: [
            { id: 'col_1', text: 'Column 1', value: 'col_1' },
            { id: 'col_2', text: 'Column 2', value: 'col_2' },
            { id: 'col_3', text: 'Column 3', value: 'col_3' }
          ],
          defaultResponseType: MatrixResponseType.SINGLE_SELECT,
          allowMultiplePerRow: false,
          requireAllRows: false,
          randomizeRows: false,
          randomizeColumns: false,
          mobileLayout: 'stacked' as any,
          showHeadersOnMobile: true
        };
      }
    
      /**
       * Convert a TallyForm API response to a FormConfig object with comprehensive field details
       */
      public convertTallyFormToFormConfig(tallyForm: TallyForm & { questions?: any[], settings?: any }): FormConfig {
        return {
          title: tallyForm.title || 'Untitled Form',
          description: tallyForm.description || undefined,
          questions: (tallyForm.questions || []).map((field: any) => {
            // Base question configuration
            const baseConfig: any = {
              id: field.id || field.uuid,
              type: field.type as QuestionType,
              label: field.label || field.title || 'Untitled Question',
              description: field.description,
              required: field.required || field.validations?.some((r: any) => r.type === 'required') || false,
              placeholder: field.placeholder,
              order: field.order,
            };
    
            // Map validation rules if present
            if (field.validation || field.validations) {
              baseConfig.validation = this.mapValidationRules(field.validation || field.validations);
            }
    
            // Map conditional logic if present
            if (field.logic) {
              baseConfig.logic = field.logic;
            }
    
            // Type-specific configurations
            const typeSpecificConfig = this.mapTypeSpecificProperties(field);
    
            return {
              ...baseConfig,
              ...typeSpecificConfig
            };
          }),
          settings: {
            submissionBehavior: tallyForm.settings?.redirectOnCompletion 
              ? SubmissionBehavior.REDIRECT 
              : SubmissionBehavior.MESSAGE,
            redirectUrl: tallyForm.settings?.redirectOnCompletionUrl || undefined,
            showProgressBar: tallyForm.settings?.showProgressBar,
            allowDrafts: tallyForm.settings?.allowDrafts,
            showQuestionNumbers: tallyForm.settings?.showQuestionNumbers,
            shuffleQuestions: tallyForm.settings?.shuffleQuestions,
            maxSubmissions: tallyForm.settings?.maxSubmissions,
            requireAuth: tallyForm.settings?.requireAuth,
            collectEmail: tallyForm.settings?.collectEmail,
            closeDate: tallyForm.settings?.closeDate,
            openDate: tallyForm.settings?.openDate,
            submissionMessage: tallyForm.settings?.submissionMessage,
            sendConfirmationEmail: tallyForm.settings?.sendConfirmationEmail,
            allowMultipleSubmissions: tallyForm.settings?.allowMultipleSubmissions
          },
          metadata: (() => {
            const meta: any = tallyForm.metadata || {};
            return {
              createdAt: tallyForm.createdAt,
              updatedAt: tallyForm.updatedAt,
              version: 1, // Initial version
              isPublished: tallyForm.isPublished,
              ...(meta.tags && { tags: meta.tags }),
              ...(meta.category && { category: meta.category }),
              ...(meta.workspaceId && { workspaceId: meta.workspaceId })
            };
          })()
        };
      }
    
      /**
       * Map validation rules from API response format to FormConfig format
       */
      private mapValidationRules(validations: any): any {
        if (!validations) return undefined;
        
        if (Array.isArray(validations)) {
          return {
            rules: validations.map((rule: any) => ({
              type: rule.type,
              errorMessage: rule.errorMessage,
              enabled: rule.enabled,
              ...rule // Include all other properties
            }))
          };
        }
        
        return validations;
      }
    
      /**
       * Map type-specific properties from API field to FormConfig question
       */
      private mapTypeSpecificProperties(field: any): any {
        const config: any = {};
    
        // Choice-based field properties
        if (field.options) {
          config.options = field.options.map((opt: any) => ({
            id: opt.id,
            text: opt.text || opt.label,
            value: opt.value,
            isDefault: opt.isDefault,
            imageUrl: opt.imageUrl,
            metadata: opt.metadata
          }));
        }
        if (field.allowOther !== undefined) config.allowOther = field.allowOther;
        if (field.randomizeOptions !== undefined) config.randomizeOptions = field.randomizeOptions;
        if (field.layout) config.layout = field.layout;
    
        // Text field properties
        if (field.minLength !== undefined) config.minLength = field.minLength;
        if (field.maxLength !== undefined) config.maxLength = field.maxLength;
        if (field.format) config.format = field.format;
        if (field.textRows !== undefined) config.rows = field.textRows; // Map back to rows for FormConfig
        if (field.autoResize !== undefined) config.autoResize = field.autoResize;
    
        // Number field properties
        if (field.min !== undefined) config.min = field.min;
        if (field.max !== undefined) config.max = field.max;
        if (field.step !== undefined) config.step = field.step;
        if (field.decimalPlaces !== undefined) config.decimalPlaces = field.decimalPlaces;
        if (field.useThousandSeparator !== undefined) config.useThousandSeparator = field.useThousandSeparator;
        if (field.numberCurrency) config.currency = field.numberCurrency; // Map back to currency for FormConfig
    
        // Date/Time field properties
        if (field.minDate) config.minDate = field.minDate;
        if (field.maxDate) config.maxDate = field.maxDate;
        if (field.dateFormat) config.dateFormat = field.dateFormat;
        if (field.includeTime !== undefined) config.includeTime = field.includeTime;
        if (field.defaultDate) config.defaultDate = field.defaultDate;
        if (field.timeFormat) config.format = field.timeFormat; // Time format goes to format field
        if (field.minuteStep !== undefined) config.minuteStep = field.minuteStep;
        if (field.defaultTime) config.defaultTime = field.defaultTime;
    
        // Rating field properties
        if (field.minRating !== undefined) config.minRating = field.minRating;
        if (field.maxRating !== undefined) config.maxRating = field.maxRating;
        if (field.ratingLabels) config.ratingLabels = field.ratingLabels;
        if (field.ratingStyle) config.style = field.ratingStyle;
        if (field.showNumbers !== undefined) config.showNumbers = field.showNumbers;
        if (field.lowLabel) config.lowLabel = field.lowLabel;
        if (field.highLabel) config.highLabel = field.highLabel;
    
        // Linear scale properties
        if (field.minValue !== undefined) config.minValue = field.minValue;
        if (field.maxValue !== undefined) config.maxValue = field.maxValue;
    
        // File upload properties
        if (field.allowedTypes) config.allowedTypes = field.allowedTypes;
        if (field.maxFileSize !== undefined) config.maxFileSize = field.maxFileSize;
        if (field.maxFiles !== undefined) config.maxFiles = field.maxFiles;
        if (field.multiple !== undefined) config.multiple = field.multiple;
        if (field.uploadText) config.uploadText = field.uploadText;
        if (field.enableDragDrop !== undefined) config.enableDragDrop = field.enableDragDrop;
        if (field.dragDropHint) config.dragDropHint = field.dragDropHint;
        if (field.showProgress !== undefined) config.showProgress = field.showProgress;
        if (field.showPreview !== undefined) config.showPreview = field.showPreview;
        if (field.fileRestrictions) config.fileRestrictions = field.fileRestrictions;
        if (field.sizeConstraints) config.sizeConstraints = field.sizeConstraints;
    
        // Dropdown properties
        if (field.searchable !== undefined) config.searchable = field.searchable;
        if (field.dropdownPlaceholder) config.dropdownPlaceholder = field.dropdownPlaceholder;
        if (field.multiSelect !== undefined) config.multiSelect = field.multiSelect;
        if (field.maxSelections !== undefined) config.maxSelections = field.maxSelections;
        if (field.minSelections !== undefined) config.minSelections = field.minSelections;
        if (field.imageOptions !== undefined) config.imageOptions = field.imageOptions;
        if (field.searchConfig) config.searchConfig = field.searchConfig;
    
        // Checkbox properties
        if (field.selectionConstraints) config.selectionConstraints = field.selectionConstraints;
        if (field.searchOptions) config.searchOptions = field.searchOptions;
    
        // Email field properties
        if (field.validateFormat !== undefined) config.validateFormat = field.validateFormat;
        if (field.suggestDomains !== undefined) config.suggestDomains = field.suggestDomains;
    
        // Phone field properties
        if (field.phoneFormat) config.format = field.phoneFormat;
        if (field.customPattern) config.customPattern = field.customPattern;
        if (field.autoFormat !== undefined) config.autoFormat = field.autoFormat;
    
        // URL field properties
        if (field.allowedSchemes) config.allowedSchemes = field.allowedSchemes;
    
        // Signature field properties
        if (field.canvasWidth !== undefined) config.canvasWidth = field.canvasWidth;
        if (field.canvasHeight !== undefined) config.canvasHeight = field.canvasHeight;
        if (field.penColor) config.penColor = field.penColor;
        if (field.backgroundColor) config.backgroundColor = field.backgroundColor;
    
        // Matrix field properties
        if (field.rows && Array.isArray(field.rows)) config.rows = field.rows; // Matrix rows, not text rows
        if (field.columns) config.columns = field.columns;
        if (field.defaultResponseType) config.defaultResponseType = field.defaultResponseType;
        if (field.allowMultiplePerRow !== undefined) config.allowMultiplePerRow = field.allowMultiplePerRow;
        if (field.requireAllRows !== undefined) config.requireAllRows = field.requireAllRows;
        if (field.randomizeRows !== undefined) config.randomizeRows = field.randomizeRows;
        if (field.randomizeColumns !== undefined) config.randomizeColumns = field.randomizeColumns;
        if (field.mobileLayout) config.mobileLayout = field.mobileLayout;
        if (field.showHeadersOnMobile !== undefined) config.showHeadersOnMobile = field.showHeadersOnMobile;
        if (field.defaultCellValidation) config.defaultCellValidation = field.defaultCellValidation;
        if (field.customClasses) config.customClasses = field.customClasses;
    
        // Payment field properties
        if (field.amount !== undefined) config.amount = field.amount;
        if (field.currency) config.currency = field.currency; // Payment currency
        if (field.fixedAmount !== undefined) config.fixedAmount = field.fixedAmount;
        if (field.minAmount !== undefined) config.minAmount = field.minAmount;
        if (field.maxAmount !== undefined) config.maxAmount = field.maxAmount;
        if (field.paymentDescription) config.paymentDescription = field.paymentDescription;
        if (field.acceptedMethods) config.acceptedMethods = field.acceptedMethods;
    
        // Custom properties
        if (field.customProperties) config.customProperties = field.customProperties;
        if (field.metadata) config.metadata = field.metadata;
    
        return config;
      }
    
      /**
       * Validate the form configuration
       */
      public validateFormConfig(formConfig: FormConfig): { valid: boolean; errors: string[] } {
        const errors: string[] = [];
        
        if (!formConfig.title) {
          errors.push('Form title is required.');
        }
        if (!formConfig.questions || formConfig.questions.length === 0) {
          errors.push('Form must have at least one question.');
        }
        
        // Check for duplicate field IDs
        const fieldIds = formConfig.questions.map(q => q.id).filter(id => id);
        const duplicateIds = fieldIds.filter((id, index) => fieldIds.indexOf(id) !== index);
        if (duplicateIds.length > 0) {
          const uniqueDuplicates = [...new Set(duplicateIds)];
          errors.push(`Duplicate field IDs found: ${uniqueDuplicates.join(', ')}`);
        }
        
        // Check for empty field labels
        const emptyLabelCount = formConfig.questions.filter(q => !q.label || q.label.trim() === '').length;
        if (emptyLabelCount > 0) {
          errors.push(`${emptyLabelCount} field(s) have empty labels`);
        }
        
        // Check for choice fields without options
        const choiceFieldsWithoutOptions = formConfig.questions.filter(q => 
          this.isChoiceField(q.type) && (!(q as any).options || (q as any).options.length === 0)
        ).length;
        if (choiceFieldsWithoutOptions > 0) {
          errors.push(`${choiceFieldsWithoutOptions} choice field(s) have no options`);
        }
        
        // Add more validation rules as needed...
        const logicErrors = DependencyValidator.validateAllLogic(formConfig);
        errors.push(...logicErrors);
    
        return {
          valid: errors.length === 0,
          errors
        };
      }
    }
  • NLP parser helper converting natural language modification commands to structured operations for use in form_modification_tool
    export class FormModificationParser {
      private questionTypeMap!: Map<string, QuestionType>;
      private commandPatterns!: CommandPattern[];
    
      constructor() {
        this.initializeQuestionTypeMap();
        this.initializeCommandPatterns();
      }
    
      /**
       * Parse a natural language command into a structured modification operation
       */
      public parseCommand(command: string): ParsedModificationCommand {
        const normalizedCommand = this.normalizeCommand(command);
        
        for (const pattern of this.commandPatterns) {
          const match = normalizedCommand.match(pattern.pattern);
          if (match) {
            const parameters = pattern.extractor(match);
            
            return {
              operation: pattern.operation,
              target: pattern.target,
              parameters,
              confidence: pattern.confidence,
              rawCommand: command,
              ambiguous: this.isAmbiguous(parameters, pattern.operation)
            };
          }
        }
    
        // If no pattern matches, return an ambiguous result
        return {
          operation: ModificationOperation.MODIFY_FIELD,
          target: ModificationTarget.FIELD,
          parameters: {},
          confidence: 0,
          rawCommand: command,
          ambiguous: true,
          clarificationNeeded: `I couldn't understand the command "${command}". Please try rephrasing it using patterns like "add [field type] field", "make [field] required", or "remove question [number]".`
        };
      }
    
      /**
       * Parse multiple commands from a single input
       */
      public parseMultipleCommands(input: string): ParsedModificationCommand[] {
        const commands = this.splitCommands(input);
        return commands.map(cmd => this.parseCommand(cmd));
      }
    
      /**
       * Check if a command needs clarification
       */
      public needsClarification(parsed: ParsedModificationCommand): boolean {
        return parsed.ambiguous === true || parsed.confidence < 0.7;
      }
    
      /**
       * Generate suggestions for ambiguous commands
       */
      public generateSuggestions(command: string): string[] {
        const suggestions: string[] = [];
        const normalized = this.normalizeCommand(command);
    
        // If it mentions adding, suggest add patterns
        if (normalized.includes('add')) {
          suggestions.push('add text field', 'add email field', 'add phone field');
        }
    
        // If it mentions required, suggest requirement patterns
        if (normalized.includes('required') || normalized.includes('require')) {
          suggestions.push('make field 1 required', 'make email required');
        }
    
        // If it mentions remove, suggest removal patterns
        if (normalized.includes('remove') || normalized.includes('delete')) {
          suggestions.push('remove question 3', 'remove field 2');
        }
    
        return suggestions;
      }
    
      private initializeQuestionTypeMap(): void {
        this.questionTypeMap = new Map([
          ['text', QuestionType.TEXT],
          ['input', QuestionType.TEXT],
          ['textarea', QuestionType.TEXTAREA],
          ['long text', QuestionType.TEXTAREA],
          ['email', QuestionType.EMAIL],
          ['email address', QuestionType.EMAIL],
          ['number', QuestionType.NUMBER],
          ['numeric', QuestionType.NUMBER],
          ['phone', QuestionType.PHONE],
          ['phone number', QuestionType.PHONE],
          ['url', QuestionType.URL],
          ['website', QuestionType.URL],
          ['link', QuestionType.URL],
          ['date', QuestionType.DATE],
          ['time', QuestionType.TIME],
          ['rating', QuestionType.RATING],
          ['stars', QuestionType.RATING],
          ['file', QuestionType.FILE],
          ['upload', QuestionType.FILE],
          ['attachment', QuestionType.FILE],
          ['signature', QuestionType.SIGNATURE],
          ['sign', QuestionType.SIGNATURE],
          ['payment', QuestionType.PAYMENT],
          ['pay', QuestionType.PAYMENT],
          ['choice', QuestionType.MULTIPLE_CHOICE],
          ['multiple choice', QuestionType.MULTIPLE_CHOICE],
          ['select', QuestionType.DROPDOWN],
          ['dropdown', QuestionType.DROPDOWN],
          ['checkboxes', QuestionType.CHECKBOXES],
          ['checkbox', QuestionType.CHECKBOXES],
          ['multi select', QuestionType.CHECKBOXES],
          ['scale', QuestionType.LINEAR_SCALE],
          ['linear scale', QuestionType.LINEAR_SCALE],
          ['slider', QuestionType.LINEAR_SCALE]
        ]);
      }
    
      private initializeCommandPatterns(): void {
        this.commandPatterns = [
          // Add field patterns
          {
            pattern: /add\s+(?:a\s+)?(?:new\s+)?(\w+(?:\s+\w+)*)\s+(?:field|question|input)/i,
            operation: ModificationOperation.ADD_FIELD,
            target: ModificationTarget.FIELD,
            confidence: 0.9,
            extractor: (match) => {
              const fieldTypeKey = match[1]?.toLowerCase();
              return {
                fieldType: fieldTypeKey ? this.questionTypeMap.get(fieldTypeKey) : undefined,
                fieldLabel: fieldTypeKey ? this.generateDefaultLabel(fieldTypeKey) : undefined
              };
            }
          },
          
          // Remove field by number
          {
            pattern: /(?:remove|delete)\s+(?:question|field)\s+(?:number\s+)?(\d+)/i,
            operation: ModificationOperation.REMOVE_FIELD,
            target: ModificationTarget.FIELD,
            confidence: 0.95,
            extractor: (match) => ({
              fieldNumber: match[1] ? parseInt(match[1]) : undefined
            })
          },
    
          // Remove field by label/name
          {
            pattern: /(?:remove|delete)\s+(?:the\s+)?(.+?)\s+(?:field|question)/i,
            operation: ModificationOperation.REMOVE_FIELD,
            target: ModificationTarget.FIELD,
            confidence: 0.8,
            extractor: (match) => ({
              fieldLabel: match[1]?.trim()
            })
          },
    
          // Make field required
          {
            pattern: /make\s+(?:question|field)\s+(?:number\s+)?(\d+)\s+required/i,
            operation: ModificationOperation.MAKE_REQUIRED,
            target: ModificationTarget.FIELD,
            confidence: 0.9,
            extractor: (match) => ({
              fieldNumber: match[1] ? parseInt(match[1]) : undefined,
              required: true
            })
          },
    
          // Make field required by name
          {
            pattern: /make\s+(?:the\s+)?(.+?)\s+(?:field|question)\s+required/i,
            operation: ModificationOperation.MAKE_REQUIRED,
            target: ModificationTarget.FIELD,
            confidence: 0.85,
            extractor: (match) => ({
              fieldLabel: match[1]?.trim(),
              required: true
            })
          },
    
          // Make field optional
          {
            pattern: /make\s+(?:question|field)\s+(?:number\s+)?(\d+)\s+optional/i,
            operation: ModificationOperation.MAKE_OPTIONAL,
            target: ModificationTarget.FIELD,
            confidence: 0.9,
            extractor: (match) => ({
              fieldNumber: match[1] ? parseInt(match[1]) : undefined,
              required: false
            })
          },
    
          // Update form title
          {
            pattern: /(?:update|change|set)\s+(?:the\s+)?(?:form\s+)?title\s+to\s+"([^"]+)"/i,
            operation: ModificationOperation.UPDATE_TITLE,
            target: ModificationTarget.FORM,
            confidence: 0.95,
            extractor: (match) => ({
              newValue: match[1]
            })
          },
    
          // Update form description
          {
            pattern: /(?:update|change|set)\s+(?:the\s+)?(?:form\s+)?description\s+to\s+"([^"]+)"/i,
            operation: ModificationOperation.UPDATE_DESCRIPTION,
            target: ModificationTarget.FORM,
            confidence: 0.95,
            extractor: (match) => ({
              newValue: match[1]
            })
          },
    
          // Reorder fields
          {
            pattern: /move\s+(?:question|field)\s+(\d+)\s+(?:to\s+)?(?:position\s+)?(\d+)/i,
            operation: ModificationOperation.REORDER_FIELD,
            target: ModificationTarget.FIELD,
            confidence: 0.9,
            extractor: (match) => ({
              sourcePosition: match[1] ? parseInt(match[1]) : undefined,
              targetPosition: match[2] ? parseInt(match[2]) : undefined
            })
          },
    
          // Add option to choice field
          {
            pattern: /add\s+option\s+"([^"]+)"\s+to\s+(?:question|field)\s+(\d+)/i,
            operation: ModificationOperation.ADD_OPTION,
            target: ModificationTarget.OPTION,
            confidence: 0.9,
            extractor: (match) => ({
              optionText: match[1],
              fieldNumber: match[2] ? parseInt(match[2]) : undefined
            })
          },
    
          // Generic field modification
          {
            pattern: /(?:update|change|modify)\s+(?:question|field)\s+(\d+)/i,
            operation: ModificationOperation.MODIFY_FIELD,
            target: ModificationTarget.FIELD,
            confidence: 0.7,
            extractor: (match) => ({
              fieldNumber: match[1] ? parseInt(match[1]) : undefined
            })
          }
        ];
      }
    
      private normalizeCommand(command: string): string {
        // Preserve quoted strings while normalizing the rest
        let normalized = command.trim();
        const quotedStrings: string[] = [];
        
        // Extract quoted strings
        normalized = normalized.replace(/"([^"]+)"/g, (_match, content) => {
          quotedStrings.push(content);
          return `"__QUOTED_${quotedStrings.length - 1}__"`;
        });
        
        // Normalize non-quoted parts
        normalized = normalized
          .toLowerCase()
          .replace(/[^\w\s"_]/g, ' ')
          .replace(/\s+/g, ' ');
        
        // Restore quoted strings
        quotedStrings.forEach((content, index) => {
          normalized = normalized.replace(`"__quoted_${index}__"`, `"${content}"`);
        });
        
        return normalized;
      }
    
      private splitCommands(input: string): string[] {
        // Split on common separators while preserving quoted strings
        const separators = /(?:\s*[,;]\s*)|(?:\s+and\s+)|(?:\s+then\s+)/i;
        return input.split(separators)
          .map(cmd => cmd.trim())
          .filter(cmd => cmd.length > 0);
      }
    
      private isAmbiguous(parameters: ModificationParameters, operation: ModificationOperation): boolean {
        switch (operation) {
          case ModificationOperation.ADD_FIELD:
            return !parameters.fieldType;
          case ModificationOperation.REMOVE_FIELD:
          case ModificationOperation.MODIFY_FIELD:
            return !parameters.fieldNumber && !parameters.fieldLabel;
          case ModificationOperation.MAKE_REQUIRED:
          case ModificationOperation.MAKE_OPTIONAL:
            return !parameters.fieldNumber && !parameters.fieldLabel;
          default:
            return false;
        }
      }
    
      private generateDefaultLabel(fieldType: string): string {
        const typeLabel = fieldType.split(' ')
          .map(word => word.charAt(0).toUpperCase() + word.slice(1))
          .join(' ');
        return `${typeLabel} Field`;
      }
    } 

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server