Skip to main content
Glama
form-modification-parser.ts12.2 kB
import { QuestionType } from '../models'; /** * Types of operations that can be performed on forms */ export enum ModificationOperation { ADD_FIELD = 'add_field', REMOVE_FIELD = 'remove_field', MODIFY_FIELD = 'modify_field', REORDER_FIELD = 'reorder_field', MAKE_REQUIRED = 'make_required', MAKE_OPTIONAL = 'make_optional', UPDATE_TITLE = 'update_title', UPDATE_DESCRIPTION = 'update_description', ADD_OPTION = 'add_option', REMOVE_OPTION = 'remove_option' } /** * Target types for modification operations */ export enum ModificationTarget { FIELD = 'field', QUESTION = 'question', OPTION = 'option', FORM = 'form' } /** * Parsed modification command structure */ export interface ParsedModificationCommand { operation: ModificationOperation; target: ModificationTarget; parameters: ModificationParameters; confidence: number; // 0-1, how confident we are in the parsing rawCommand: string; ambiguous?: boolean; clarificationNeeded?: string; } /** * Parameters for modification operations */ export interface ModificationParameters { fieldType?: QuestionType | undefined; fieldId?: string | undefined; fieldNumber?: number | undefined; fieldLabel?: string | undefined; targetPosition?: number | undefined; sourcePosition?: number | undefined; newValue?: string | undefined; optionText?: string | undefined; optionValue?: string | undefined; required?: boolean | undefined; description?: string | undefined; placeholder?: string | undefined; options?: string[] | undefined; } /** * Pattern matching rules for command recognition */ interface CommandPattern { pattern: RegExp; operation: ModificationOperation; target: ModificationTarget; confidence: number; extractor: (match: RegExpMatchArray) => Partial<ModificationParameters>; } 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