diff --git a/blocks/blocks/evaluator.ts b/blocks/blocks/evaluator.ts index cc87be05e3..de47dc203d 100644 --- a/blocks/blocks/evaluator.ts +++ b/blocks/blocks/evaluator.ts @@ -3,14 +3,13 @@ import { ToolResponse } from '@/tools/types' import { MODEL_TOOLS, ModelType } from '../consts' import { BlockConfig, ParamType } from '../types' -interface TargetBlock { - id: string - type?: string - title?: string - description?: string - category?: string - subBlocks?: Record - currentState?: any +interface Metric { + name: string + description: string + range: { + min: number + max: number + } } interface EvaluatorResponse extends ToolResponse { @@ -22,111 +21,39 @@ interface EvaluatorResponse extends ToolResponse { completion?: number total?: number } - evaluation: { - score: number - reasoning: string - metrics: Record - } - selectedPath: { - blockId: string - blockType: string - blockTitle: string - } - justification: string - history: Array<{ response: string; justification: string }> + [metricName: string]: any // Allow dynamic metric fields } } -export const generateEvaluatorPrompt = ( - evaluationCriteria: string, - content: string, - targetBlocks?: TargetBlock[], - history?: Array<{ response: string; justification: string }> -): string => { - const basePrompt = `You are an objective evaluation agent. Analyze the content against the provided criteria and determine the next step based on the evaluation score. +export const generateEvaluatorPrompt = (metrics: Metric[], content: string): string => { + const metricsDescription = metrics + .map( + (metric) => `${metric.name} (${metric.range.min}-${metric.range.max}): ${metric.description}` + ) + .join('\n') + + return `You are an objective evaluation agent. Analyze the content against the provided metrics and provide detailed scoring. Evaluation Instructions: -1. Score the content (0 to 1) using these metrics: - - Accuracy: How well does it meet requirements? - - Completeness: Are all aspects addressed? - - Quality: Is it clear and professional? - - Relevance: Does it match the criteria? +- For each metric, provide a numeric score within the specified range +- Your response must be a valid JSON object with each metric as a number field +- Do not include explanations in the JSON - only numeric scores -2. Calculate final score: - - Average all metrics - - Round to 2 decimal places${ - history && history.length > 0 - ? ` +Metrics to evaluate: +${metricsDescription} -Previous Attempts: -${history - .map( - (entry, i) => ` -Attempt ${i + 1}: -Response: ${entry.response} -Evaluation: ${entry.justification} ----` - ) - .join('\n')}` - : '' - } - -Content: -${content} - -Criteria: -${evaluationCriteria}` - - // If no target blocks, just return the evaluation without routing - if (!targetBlocks || targetBlocks.length === 0) { - return `${basePrompt} - -IMPORTANT: When there are no target blocks, you must use exactly "end" as the decision value. Do not use any other word. - -Response Format: -Return a JSON object with the following structure: -{ - "decision": "end", // You must use exactly "end" here - this is a required system keyword - "justification": "Brief explanation of the pure evaluation of the content. DO NOT include any information about the target blocks." +Content to evaluate: +${content}` } -Remember: -1. Your response must be ONLY the JSON object - no additional text, formatting, or explanation. -2. The "decision" field MUST be exactly "end" - this is a required keyword that the system expects.` - } - - const targetBlocksInfo = ` -Available Destinations: -${targetBlocks - .map( - (block) => ` -ID: ${block.id} -Type: ${block.type} -Title: ${block.title} -Description: ${block.description}` - ) - .join('\n---\n')} - -Routing Rules: -${ - targetBlocks.length === 1 - ? `- Route back to the only available block (${targetBlocks[0].id}) to continue the iteration` - : `- Score greater than or equal to 0.85: Choose success path block -- Score less than 0.85: Choose failure path block` -}` - - return `${basePrompt}${targetBlocksInfo} - -Response Format: -Return a JSON object with the following structure: -{ - "decision": "block-id-here", - "justification": "Brief explanation of the pure evaluation of the content. DO NOT include any information about the target blocks." -} - -Remember: Your response must be ONLY the JSON object - no additional text, formatting, or explanation. -If there is only one available destination, return that block's ID in the decision field regardless of the score.` -} +// Simplified response format generator that matches the agent block schema structure +const generateResponseFormat = (metrics: Metric[]) => ({ + fields: metrics.map((metric) => ({ + name: metric.name, + type: 'number', + description: `${metric.description} (Score between ${metric.range.min}-${metric.range.max})`, + })), +}) export const EvaluatorBlock: BlockConfig = { type: 'evaluator', @@ -162,11 +89,46 @@ export const EvaluatorBlock: BlockConfig = { }, workflow: { inputs: { - prompt: { type: 'string' as ParamType, required: true }, + metrics: { + type: 'json' as ParamType, + required: true, + description: 'Array of metrics to evaluate against', + schema: { + type: 'array', + properties: {}, + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name of the metric', + }, + description: { + type: 'string', + description: 'Description of what this metric measures', + }, + range: { + type: 'object', + properties: { + min: { + type: 'number', + description: 'Minimum possible score', + }, + max: { + type: 'number', + description: 'Maximum possible score', + }, + }, + required: ['min', 'max'], + }, + }, + required: ['name', 'description', 'range'], + }, + }, + }, model: { type: 'string' as ParamType, required: true }, apiKey: { type: 'string' as ParamType, required: true }, content: { type: 'string' as ParamType, required: true }, - history: { type: 'json' as ParamType, required: false }, }, outputs: { response: { @@ -174,17 +136,24 @@ export const EvaluatorBlock: BlockConfig = { content: 'string', model: 'string', tokens: 'any', - evaluation: 'json', - selectedPath: 'json', - justification: 'string', - history: 'json', + }, + dependsOn: { + subBlockId: 'metrics', + condition: { + whenEmpty: { + content: 'string', + model: 'string', + tokens: 'any', + }, + whenFilled: 'json', + }, }, }, }, subBlocks: [ { - id: 'prompt', - title: 'Evaluation Criteria', + id: 'metrics', + title: 'Evaluation Metrics', type: 'eval-input', layout: 'full', }, @@ -218,12 +187,14 @@ export const EvaluatorBlock: BlockConfig = { layout: 'full', hidden: true, value: (params: Record) => { - return generateEvaluatorPrompt( - params.prompt || '', - params.content || '', - undefined, - params.history || [] - ) + const metrics = params.metrics || [] + const content = params.content || '' + const responseFormat = generateResponseFormat(metrics) + + return JSON.stringify({ + systemPrompt: generateEvaluatorPrompt(metrics, content), + responseFormat, + }) }, }, ], diff --git a/components/ui/tag-dropdown.tsx b/components/ui/tag-dropdown.tsx index 467b49e316..a4768c3592 100644 --- a/components/ui/tag-dropdown.tsx +++ b/components/ui/tag-dropdown.tsx @@ -8,6 +8,15 @@ interface Field { description?: string } +interface Metric { + name: string + description: string + range: { + min: number + max: number + } +} + interface TagDropdownProps { visible: boolean onSelect: (newValue: string) => void @@ -68,7 +77,23 @@ export const TagDropdown: React.FC = ({ const blockName = sourceBlock.name || sourceBlock.type const normalizedBlockName = blockName.replace(/\s+/g, '').toLowerCase() - // Check for response format first + // First check for evaluator metrics + if (sourceBlock.type === 'evaluator') { + try { + const metricsValue = sourceBlock.subBlocks?.metrics?.value as unknown as Metric[] + if (Array.isArray(metricsValue)) { + return { + tags: metricsValue.map( + (metric) => `${normalizedBlockName}.response.${metric.name.toLowerCase()}` + ), + } + } + } catch (e) { + console.error('Error parsing metrics:', e) + } + } + + // Then check for response format try { const responseFormatValue = sourceBlock.subBlocks?.responseFormat?.value if (typeof responseFormatValue === 'string' && responseFormatValue) { @@ -87,7 +112,6 @@ export const TagDropdown: React.FC = ({ // Fall back to default outputs if no response format const outputPaths = getOutputPaths(sourceBlock.outputs) - return { tags: outputPaths.map((path) => `${normalizedBlockName}.${path}`), } @@ -117,6 +141,21 @@ export const TagDropdown: React.FC = ({ console.error('Error parsing response format:', e) } + if (sourceBlock.type === 'evaluator') { + try { + const metricsValue = sourceBlock.subBlocks?.metrics?.value as unknown as Metric[] + if (Array.isArray(metricsValue)) { + return { + tags: metricsValue.map( + (metric) => `${normalizedBlockName}.response.${metric.name.toLowerCase()}` + ), + } + } + } catch (e) { + console.error('Error parsing metrics:', e) + } + } + // Fall back to default outputs if no response format const outputPaths = getOutputPaths(sourceBlock.outputs) return outputPaths.map((path) => `${normalizedBlockName}.${path}`) diff --git a/executor/index.ts b/executor/index.ts index 6ed3cfdc05..09d0d178a7 100644 --- a/executor/index.ts +++ b/executor/index.ts @@ -1,5 +1,4 @@ import { getAllBlocks } from '@/blocks' -import { generateEvaluatorPrompt } from '@/blocks/blocks/evaluator' import { generateRouterPrompt } from '@/blocks/blocks/router' import { BlockOutput } from '@/blocks/types' import { BlockConfig } from '@/blocks/types' @@ -79,7 +78,8 @@ export class Executor { * * Key Features: * - Executes blocks with no dependencies in parallel using topological sorting - * - Handles special blocks (router, evaluator, condition) and their path decisions + * - Handles special blocks (router, condition) for path decisions + * - Handles agent and evaluator blocks for structured output * - Manages feedback loops with iteration limits * - Tracks and updates block states in the execution context * @@ -114,22 +114,6 @@ export class Executor { if (conn.condition) { countEdge = false - } else if (sourceBlock && sourceBlock.metadata?.type === 'evaluator') { - // For evaluator edges, count the dependency only if the target block's config references the evaluator output - const targetBlock = blocks.find((b) => b.id === conn.target) - if (targetBlock) { - const paramsStr = JSON.stringify(targetBlock.config.params || {}) - const evaluatorRef = `<${sourceBlock.metadata?.title?.toLowerCase().replace(/\s+/g, '')}` - const altEvaluatorRef = `<${sourceBlock.id}` - - // If target block references evaluator output, count the edge - if (paramsStr.includes(evaluatorRef) || paramsStr.includes(altEvaluatorRef)) { - countEdge = true - } else { - // For paths that don't use evaluator output, handle via decisions - countEdge = false - } - } } if (countEdge) { @@ -158,7 +142,6 @@ export class Executor { // Maps for decisions const routerDecisions = new Map() - const evaluatorDecisions = new Map() const activeConditionalPaths = new Map() // Initial queue: all blocks with zero inDegree @@ -185,11 +168,6 @@ export class Executor { if (!this.isInChosenPath(blockId, chosenPath, routerId)) return false } - // Check evaluator decisions - for (const [evaluatorId, chosenPath] of evaluatorDecisions) { - if (!this.isInChosenPath(blockId, chosenPath, evaluatorId)) return false - } - // Check conditional paths for (const [conditionBlockId, selectedConditionId] of activeConditionalPaths) { const connection = connections.find( @@ -227,31 +205,9 @@ export class Executor { } } routerDecisions.set(block.id, routerResult.response.selectedPath.blockId) - } else if (block.metadata?.type === 'evaluator') { - const evaluatorResult = result as { - response: { - content: string - model: string - tokens: { prompt: number; completion: number; total: number } - selectedPath: { blockId: string } - justification: string - history: Array<{ response: string; justification: string }> - } - } - evaluatorDecisions.set(block.id, evaluatorResult.response.selectedPath.blockId) } else if (block.metadata?.type === 'condition') { - const conditionResult = result as { - response: { - condition: { - selectedConditionId: string - result: boolean - } - } - } - activeConditionalPaths.set( - block.id, - conditionResult.response.condition.selectedConditionId - ) + const conditionResult = await this.executeConditionalBlock(block, context) + activeConditionalPaths.set(block.id, conditionResult.selectedConditionId) } return blockId }) @@ -264,27 +220,7 @@ export class Executor { for (const conn of outgoingConns) { const sourceBlock = blocks.find((b) => b.id === conn.source) - if (sourceBlock?.metadata?.type === 'evaluator') { - // Only add to queue if this is the chosen path - const chosenPath = evaluatorDecisions.get(sourceBlock.id) - if (conn.target === chosenPath) { - // CHANGED: Don't rely on inDegree for loop targets - const targetBlock = blocks.find((b) => b.id === conn.target) - const isInLoop = Object.values(this.workflow.loops || {}).some((loop) => - loop.nodes.includes(conn.target) - ) - - if (isInLoop) { - // If target is in a loop, queue it directly - queue.push(conn.target) - } else { - // For non-loop targets, use normal inDegree logic - const newDegree = (inDegree.get(conn.target) || 0) - 1 - inDegree.set(conn.target, newDegree) - if (newDegree === 0) queue.push(conn.target) - } - } - } else if (sourceBlock?.metadata?.type === 'router') { + if (sourceBlock?.metadata?.type === 'router') { // Only add to queue if this is the chosen path const chosenPath = routerDecisions.get(sourceBlock.id) if (conn.target === chosenPath) { @@ -317,24 +253,21 @@ export class Executor { if (executedLoopBlocks.length > 0) { const iterations = loopIterations.get(loopId) || 0 if (iterations < loop.maxIterations - 1) { - // Check if the evaluator chose a block within the loop - const evaluatorInLoop = executedLoopBlocks.find((blockId) => { - const block = blocks.find((b) => b.id === blockId) - return block?.metadata?.type === 'evaluator' + // Check if any block in the loop has outgoing connections to other blocks in the loop + const hasLoopConnection = executedLoopBlocks.some((blockId) => { + const outgoingConns = connections.filter((conn) => conn.source === blockId) + return outgoingConns.some((conn) => loopBlocks.has(conn.target)) }) - if (evaluatorInLoop) { - const chosenPath = evaluatorDecisions.get(evaluatorInLoop) - if (chosenPath && loopBlocks.has(chosenPath)) { - // Reset the loop blocks' inDegrees and add them back to queue if needed - resetLoopBlocksDegrees(loopId) - for (const blockId of loop.nodes) { - if (inDegree.get(blockId) === 0) { - queue.push(blockId) - } + if (hasLoopConnection) { + // Reset the loop blocks' inDegrees and add them back to queue if needed + resetLoopBlocksDegrees(loopId) + for (const blockId of loop.nodes) { + if (inDegree.get(blockId) === 0) { + queue.push(blockId) } - loopIterations.set(loopId, iterations + 1) } + loopIterations.set(loopId, iterations + 1) } } } @@ -350,7 +283,11 @@ export class Executor { * * Process: * 1. Validates block state and configuration - * 2. Executes based on block type + * 2. Executes based on block type: + * - Router: Makes routing decisions + * - Evaluator: Analyzes content and returns metrics + * - Condition: Evaluates conditions and selects paths + * - Agent: Processes with LLM and optional tools * 3. Logs execution details * 4. Stores results in context * @@ -396,16 +333,7 @@ export class Executor { } } else if (block.metadata?.type === 'evaluator') { const evaluatorOutput = await this.executeEvaluatorBlock(block, context) - output = { - response: { - content: evaluatorOutput.content, - model: evaluatorOutput.model, - tokens: evaluatorOutput.tokens, - selectedPath: evaluatorOutput.selectedPath, - justification: evaluatorOutput.justification, - history: evaluatorOutput.history, - }, - } + output = evaluatorOutput } else if (block.metadata?.type === 'condition') { const conditionResult = await this.executeConditionalBlock(block, context) output = { @@ -648,108 +576,47 @@ export class Executor { } /** - * Executes an evaluator block which analyzes content against criteria and chooses a path. + * Executes an evaluator block which analyzes content against metrics. * * Process: - * 1. Resolves inputs and gets possible target blocks + * 1. Resolves inputs including metrics configuration * 2. Generates and sends evaluation prompt to the model - * 3. Processes response to determine chosen path + * 3. Processes response to extract metric scores and reasoning * 4. Stores evaluation result in context * + * The evaluator block returns structured output with scores and reasoning for each metric, + * which can be referenced by other blocks (e.g., condition blocks) to make routing decisions. + * * @param block - The evaluator block to execute * @param context - Current execution context - * @returns Promise with evaluation result including chosen path + * @returns Promise with evaluation result including metric scores */ private async executeEvaluatorBlock( block: SerializedBlock, context: ExecutionContext - ): Promise<{ - content: string - model: string - tokens: { - prompt: number - completion: number - total: number - } - selectedPath: { - blockId: string - blockType: string - blockTitle: string - } - justification: string - history: Array<{ response: string; justification: string }> - }> { - // Resolve inputs for the evaluator block. + ): Promise { + // Resolve inputs for the evaluator block const resolvedInputs = this.resolveInputs(block, context) - // Get all possible target blocks from outgoing connections - const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id) - - const targetBlocks = outgoingConnections.map((conn) => { - const targetBlock = this.workflow.blocks.find((b) => b.id === conn.target) - if (!targetBlock) { - throw new Error(`Target block ${conn.target} not found`) - } - return { - id: targetBlock.id, - type: targetBlock.metadata?.type || 'unknown', - title: targetBlock.metadata?.title || 'Untitled Block', - description: targetBlock.metadata?.description, - subBlocks: targetBlock.config.params, - currentState: context.blockStates.get(targetBlock.id), - } - }) - - // Get history from previous state if it exists, otherwise initialize empty - let history: Array<{ response: string; justification: string }> = [] - const previousState = context.blockStates.get(block.id) - if (previousState && typeof previousState === 'object' && 'response' in previousState) { - const response = previousState.response - if (response && typeof response === 'object' && 'history' in response) { - history = response.history as Array<{ response: string; justification: string }> - } - } - const model = resolvedInputs.model || 'gpt-4o' const providerId = getProviderFromModel(model) - // Generate and execute the evaluator prompt + // Execute the evaluator prompt with structured output format const response = await executeProviderRequest(providerId, { model: resolvedInputs.model, - systemPrompt: generateEvaluatorPrompt( - resolvedInputs.prompt, - resolvedInputs.content, - targetBlocks, - history - ), - messages: [{ role: 'user', content: resolvedInputs.prompt }], + systemPrompt: resolvedInputs.systemPrompt?.systemPrompt, + responseFormat: resolvedInputs.systemPrompt?.responseFormat, + messages: [{ role: 'user', content: resolvedInputs.content }], temperature: resolvedInputs.temperature || 0, apiKey: resolvedInputs.apiKey, }) - // Parse the evaluator response as JSON - let evaluatorResponse - try { - evaluatorResponse = JSON.parse(response.content.trim()) - } catch (e) { - throw new Error(`Invalid evaluator response format: ${response.content}`) - } + // Parse the response content to get metrics + const parsedContent = JSON.parse(response.content) - const chosenBlockId = evaluatorResponse.decision - const justification = evaluatorResponse.justification - - // Update history with current response and evaluation - const updatedHistory = [ - ...history, - { - response: resolvedInputs.content, - justification, - }, - ] - - // Handle case where evaluator has no targets - if (chosenBlockId === 'end') { - const result = { + // Create the result in the expected format + const result = { + response: { content: resolvedInputs.content, model: response.model, tokens: { @@ -757,48 +624,15 @@ export class Executor { completion: response.tokens?.completion || 0, total: response.tokens?.total || 0, }, - selectedPath: { - blockId: '', - blockType: '', - blockTitle: '', - }, - justification, - history: updatedHistory, - } - - context.blockStates.set(block.id, { - response: result, - }) - - return result - } - - const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId) - if (!chosenBlock) { - throw new Error(`Invalid evaluation decision: ${chosenBlockId}`) - } - - const result = { - content: resolvedInputs.content, - model: response.model, - tokens: { - prompt: response.tokens?.prompt || 0, - completion: response.tokens?.completion || 0, - total: response.tokens?.total || 0, + // Also add each metric as a direct field for easy access + ...Object.fromEntries( + Object.entries(parsedContent).map(([key, value]) => [key.toLowerCase(), value]) + ), }, - selectedPath: { - blockId: chosenBlock.id, - blockType: chosenBlock.type, - blockTitle: chosenBlock.title, - }, - justification, - history: updatedHistory, } - context.blockStates.set(block.id, { - response: result, - }) - + // Store the result in block states + context.blockStates.set(block.id, result) return result } @@ -838,12 +672,9 @@ export class Executor { // Get all outgoing connections from current block const connections = this.workflow.connections.filter((conn) => conn.source === currentId) for (const conn of connections) { - // Don't follow connections from other routers/evaluators + // Don't follow connections from other routers const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source) - if ( - sourceBlock?.metadata?.type !== 'router' && - sourceBlock?.metadata?.type !== 'evaluator' - ) { + if (sourceBlock?.metadata?.type !== 'router') { queue.push(conn.target) } } diff --git a/providers/service.ts b/providers/service.ts index c01d9a9266..db7c27dfa1 100644 --- a/providers/service.ts +++ b/providers/service.ts @@ -6,22 +6,52 @@ import { ProviderRequest, ProviderResponse, TokenInfo } from './types' function generateStructuredOutputInstructions(responseFormat: any): string { if (!responseFormat?.fields) return '' - const fields = responseFormat.fields + function generateFieldStructure(field: any): string { + if (field.type === 'object' && field.properties) { + return `{ + ${Object.entries(field.properties) + .map(([key, prop]: [string, any]) => `"${key}": ${prop.type === 'number' ? '0' : '"value"'}`) + .join(',\n ')} + }` + } + return field.type === 'string' + ? '"value"' + : field.type === 'number' + ? '0' + : field.type === 'boolean' + ? 'true/false' + : '[]' + } + + const exampleFormat = responseFormat.fields + .map((field: any) => ` "${field.name}": ${generateFieldStructure(field)}`) + .join(',\n') + + const fieldDescriptions = responseFormat.fields .map((field: any) => { - return `${field.name} (${field.type})${field.description ? `: ${field.description}` : ''}` + let desc = `${field.name} (${field.type})` + if (field.description) desc += `: ${field.description}` + if (field.type === 'object' && field.properties) { + desc += '\nProperties:' + Object.entries(field.properties).forEach(([key, prop]: [string, any]) => { + desc += `\n - ${key} (${(prop as any).type}): ${(prop as any).description || ''}` + }) + } + return desc }) .join('\n') return ` Please provide your response in the following JSON format: { - ${responseFormat.fields.map((field: any) => `"${field.name}": "${field.type === 'string' ? 'value' : field.type === 'number' ? '0' : field.type === 'boolean' ? 'true/false' : '[]'}"`).join(',\n ')} +${exampleFormat} } Field descriptions: -${fields} +${fieldDescriptions} -Your response MUST be valid JSON and include all the specified fields with their correct types.` +Your response MUST be valid JSON and include all the specified fields with their correct types. +Each metric should be an object containing 'score' (number) and 'reasoning' (string).` } export async function executeProviderRequest( @@ -74,9 +104,18 @@ export async function executeProviderRequest( // Try to parse the content as JSON const parsedContent = JSON.parse(content) + console.log('Response Format:', JSON.stringify(request.responseFormat, null, 2)) + console.log('Parsed Content:', JSON.stringify(parsedContent, null, 2)) + // Validate that all required fields are present and have correct types const validationErrors = request.responseFormat.fields .map((field: any) => { + console.log(`Validating field ${field.name}:`, { + expectedType: field.type, + actualValue: parsedContent[field.name], + actualType: typeof parsedContent[field.name], + }) + if (!(field.name in parsedContent)) { return `Missing field: ${field.name}` } diff --git a/stores/workflow/store.ts b/stores/workflow/store.ts index 87b9bea3fe..883e7a7188 100644 --- a/stores/workflow/store.ts +++ b/stores/workflow/store.ts @@ -50,15 +50,9 @@ export const useWorkflowStore = create()( subBlockId === 'responseFormat' && typeof processedValue === 'string' ) { - console.log('Validating responseFormat input:', { - type: typeof processedValue, - rawValue: processedValue, - }) - try { // Parse the input string to validate JSON but keep original string value const parsed = JSON.parse(processedValue) - console.log('Parsed responseFormat:', parsed) // Simple validation of required schema structure if (!parsed.fields || !Array.isArray(parsed.fields)) { @@ -67,7 +61,6 @@ export const useWorkflowStore = create()( } for (const field of parsed.fields) { - console.log('Validating field:', field) if (!field.name || !field.type) { console.error('Validation failed: field missing name or type', field) throw new Error('Each field must have a name and type') @@ -80,7 +73,6 @@ export const useWorkflowStore = create()( } } - console.log('responseFormat validation successful') // Don't modify the value, keep it as the original string } catch (error: any) { console.error('responseFormat validation error:', error)