mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
+89
-118
@@ -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,
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
@@ -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
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user