Modify executor to treat evaluator as an 'output' block instead of a 'router' block, looping logic still needs to be updated but forward path works

This commit is contained in:
Waleed Latif
2025-02-13 00:59:25 -08:00
parent e2a5e39b0b
commit af323a71ab
5 changed files with 222 additions and 350 deletions
+89 -118
View File
@@ -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<string, any>
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<string, number>
}
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<EvaluatorResponse> = {
type: 'evaluator',
@@ -162,11 +89,46 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
},
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<EvaluatorResponse> = {
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<EvaluatorResponse> = {
layout: 'full',
hidden: true,
value: (params: Record<string, any>) => {
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,
})
},
},
],
+41 -2
View File
@@ -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<TagDropdownProps> = ({
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<TagDropdownProps> = ({
// 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<TagDropdownProps> = ({
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}`)
+48 -217
View File
@@ -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<string, string>()
const evaluatorDecisions = new Map<string, string>()
const activeConditionalPaths = new Map<string, string>()
// 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<BlockOutput> {
// 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)
}
}
+44 -5
View File
@@ -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}`
}
-8
View File
@@ -50,15 +50,9 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
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<WorkflowStoreWithHistory>()(
}
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<WorkflowStoreWithHistory>()(
}
}
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)