mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
Removed extraneous logs, and treat evaluator block as a pass-through. Added jsdoc annotations to executor functions
This commit is contained in:
+437
-401
@@ -1,19 +1,3 @@
|
||||
/**
|
||||
* Executor for running agentic workflows in parallel.
|
||||
*
|
||||
* High-Level Overview:
|
||||
* - This class is responsible for running workflows using a layered topological sort.
|
||||
* - Blocks that have no unresolved dependencies are executed in parallel.
|
||||
* - Depending on the block type (router, condition, agent, or regular tool), different execution
|
||||
* logic is applied. For example, condition blocks evaluate multiple branches and record the
|
||||
* chosen branch via its condition ID so that only that path is executed.
|
||||
* - Each block's output is stored in the ExecutionContext so that subsequent blocks can reference them.
|
||||
* - Detailed logs are collected for each block to assist with debugging.
|
||||
*
|
||||
* Error Handling:
|
||||
* - If a block fails, an error is thrown, halting the workflow.
|
||||
* - Meaningful error messages are provided.
|
||||
*/
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import { generateEvaluatorPrompt } from '@/blocks/blocks/evaluator'
|
||||
import { generateRouterPrompt } from '@/blocks/blocks/router'
|
||||
@@ -25,6 +9,10 @@ import { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { executeTool, getTool } from '@/tools'
|
||||
import { BlockLog, ExecutionContext, ExecutionResult, Tool } from './types'
|
||||
|
||||
/**
|
||||
* Main executor class for running agentic workflows.
|
||||
* Handles parallel execution, state management, and special block types.
|
||||
*/
|
||||
export class Executor {
|
||||
constructor(
|
||||
private workflow: SerializedWorkflow,
|
||||
@@ -34,7 +22,11 @@ export class Executor {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Main entry point that executes the entire workflow in layered parallel fashion.
|
||||
* Main entry point for workflow execution.
|
||||
* Initializes context, executes blocks, and returns results.
|
||||
*
|
||||
* @param workflowId - Unique identifier for the workflow
|
||||
* @returns Promise<ExecutionResult> - Execution results including success/failure, output, and logs
|
||||
*/
|
||||
async execute(workflowId: string): Promise<ExecutionResult> {
|
||||
const startTime = new Date()
|
||||
@@ -83,12 +75,16 @@ export class Executor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes workflow blocks layer-by-layer. Blocks with no dependencies are processed together.
|
||||
* Executes workflow blocks layer-by-layer in parallel, handling loops and conditional paths.
|
||||
*
|
||||
* Notes:
|
||||
* - Maintains in-degrees and adjacency lists for blocks (i.e. dependencies).
|
||||
* - Blocks with condition or router types update routing/conditional decisions.
|
||||
* - Only the branch corresponding to the evaluated condition is executed.
|
||||
* Key Features:
|
||||
* - Executes blocks with no dependencies in parallel using topological sorting
|
||||
* - Handles special blocks (router, evaluator, condition) and their path decisions
|
||||
* - Manages feedback loops with iteration limits
|
||||
* - Tracks and updates block states in the execution context
|
||||
*
|
||||
* @param context - The execution context containing block states and logs
|
||||
* @returns Promise<BlockOutput> - The output of the last executed block
|
||||
*/
|
||||
private async executeInParallel(context: ExecutionContext): Promise<BlockOutput> {
|
||||
const { blocks, connections } = this.workflow
|
||||
@@ -348,10 +344,19 @@ export class Executor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a single block. Deduces the tool to call, validates parameters,
|
||||
* makes the request, and transforms the response.
|
||||
* Executes a single block with appropriate tool or provider.
|
||||
* Handles different block types (router, evaluator, condition, agent).
|
||||
*
|
||||
* The result is logged and returned.
|
||||
* Process:
|
||||
* 1. Validates block state and configuration
|
||||
* 2. Executes based on block type
|
||||
* 3. Logs execution details
|
||||
* 4. Stores results in context
|
||||
*
|
||||
* @param block - Block to execute
|
||||
* @param inputs - Resolved inputs for the block
|
||||
* @param context - Current execution context
|
||||
* @returns Promise<BlockOutput> - Block execution results
|
||||
*/
|
||||
private async executeBlock(
|
||||
block: SerializedBlock,
|
||||
@@ -550,9 +555,413 @@ export class Executor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves template references in a block's configuration (e.g., "<blockId.property>"),
|
||||
* as well as environment variables (format: "{{ENV_VAR}}").
|
||||
* The values are pulled from the context's blockStates and environmentVariables.
|
||||
* Executes a router block to determine the next execution path.
|
||||
*
|
||||
* Process:
|
||||
* 1. Resolves inputs and gets possible target blocks
|
||||
* 2. Generates and sends routing prompt to the model
|
||||
* 3. Processes response to determine chosen path
|
||||
* 4. Validates and returns routing decision
|
||||
*
|
||||
* @param block - The router block to execute
|
||||
* @param context - Current execution context
|
||||
* @returns Promise with routing result including chosen path
|
||||
*/
|
||||
private async executeRouterBlock(
|
||||
block: SerializedBlock,
|
||||
context: ExecutionContext
|
||||
): Promise<{
|
||||
content: string
|
||||
model: string
|
||||
tokens: {
|
||||
prompt: number
|
||||
completion: number
|
||||
total: number
|
||||
}
|
||||
selectedPath: {
|
||||
blockId: string
|
||||
blockType: string
|
||||
blockTitle: string
|
||||
}
|
||||
}> {
|
||||
// Resolve inputs for the router block.
|
||||
const resolvedInputs = this.resolveInputs(block, context)
|
||||
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,
|
||||
title: targetBlock.metadata?.title,
|
||||
description: targetBlock.metadata?.description,
|
||||
subBlocks: targetBlock.config.params,
|
||||
currentState: context.blockStates.get(targetBlock.id),
|
||||
}
|
||||
})
|
||||
|
||||
const routerConfig = {
|
||||
prompt: resolvedInputs.prompt,
|
||||
model: resolvedInputs.model,
|
||||
apiKey: resolvedInputs.apiKey,
|
||||
temperature: resolvedInputs.temperature || 0,
|
||||
}
|
||||
|
||||
const model = routerConfig.model || 'gpt-4o'
|
||||
const providerId = getProviderFromModel(model)
|
||||
|
||||
// Generate and send the router prompt.
|
||||
const response = await executeProviderRequest(providerId, {
|
||||
model: routerConfig.model,
|
||||
systemPrompt: generateRouterPrompt(routerConfig.prompt, targetBlocks),
|
||||
messages: [{ role: 'user', content: routerConfig.prompt }],
|
||||
temperature: routerConfig.temperature,
|
||||
apiKey: routerConfig.apiKey,
|
||||
})
|
||||
|
||||
const chosenBlockId = response.content.trim().toLowerCase()
|
||||
const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId)
|
||||
if (!chosenBlock) {
|
||||
throw new Error(`Invalid routing decision: ${chosenBlockId}`)
|
||||
}
|
||||
|
||||
const tokens = response.tokens || { prompt: 0, completion: 0, total: 0 }
|
||||
return {
|
||||
content: resolvedInputs.prompt,
|
||||
model: response.model,
|
||||
tokens: {
|
||||
prompt: tokens.prompt || 0,
|
||||
completion: tokens.completion || 0,
|
||||
total: tokens.total || 0,
|
||||
},
|
||||
selectedPath: {
|
||||
blockId: chosenBlock.id,
|
||||
blockType: chosenBlock.type || 'unknown',
|
||||
blockTitle: chosenBlock.title || 'Untitled Block',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an evaluator block which analyzes content against criteria and chooses a path.
|
||||
*
|
||||
* Process:
|
||||
* 1. Resolves inputs and gets possible target blocks
|
||||
* 2. Generates and sends evaluation prompt to the model
|
||||
* 3. Processes response to determine chosen path
|
||||
* 4. Stores evaluation result in context
|
||||
*
|
||||
* @param block - The evaluator block to execute
|
||||
* @param context - Current execution context
|
||||
* @returns Promise with evaluation result including chosen path
|
||||
*/
|
||||
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
|
||||
}
|
||||
}> {
|
||||
// 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,
|
||||
title: targetBlock.metadata?.title,
|
||||
description: targetBlock.metadata?.description,
|
||||
subBlocks: targetBlock.config.params,
|
||||
currentState: context.blockStates.get(targetBlock.id),
|
||||
}
|
||||
})
|
||||
|
||||
const evaluatorConfig = {
|
||||
prompt: resolvedInputs.prompt,
|
||||
content: resolvedInputs.content,
|
||||
model: resolvedInputs.model,
|
||||
apiKey: resolvedInputs.apiKey,
|
||||
temperature: resolvedInputs.temperature || 0,
|
||||
}
|
||||
|
||||
const model = evaluatorConfig.model || 'gpt-4o'
|
||||
const providerId = getProviderFromModel(model)
|
||||
|
||||
// Generate and execute the evaluator prompt
|
||||
const response = await executeProviderRequest(providerId, {
|
||||
model: evaluatorConfig.model,
|
||||
systemPrompt: generateEvaluatorPrompt(
|
||||
evaluatorConfig.prompt,
|
||||
evaluatorConfig.content,
|
||||
targetBlocks
|
||||
),
|
||||
messages: [{ role: 'user', content: evaluatorConfig.prompt }],
|
||||
temperature: evaluatorConfig.temperature,
|
||||
apiKey: evaluatorConfig.apiKey,
|
||||
})
|
||||
|
||||
const chosenBlockId = response.content.trim().toLowerCase()
|
||||
|
||||
const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId)
|
||||
if (!chosenBlock) {
|
||||
throw new Error(`Invalid evaluation decision: ${chosenBlockId}`)
|
||||
}
|
||||
|
||||
// Store the evaluation result in the context
|
||||
const tokens = response.tokens || { prompt: 0, completion: 0, total: 0 }
|
||||
const result = {
|
||||
content: evaluatorConfig.content,
|
||||
model: response.model,
|
||||
tokens: {
|
||||
prompt: tokens.prompt || 0,
|
||||
completion: tokens.completion || 0,
|
||||
total: tokens.total || 0,
|
||||
},
|
||||
selectedPath: {
|
||||
blockId: chosenBlock.id,
|
||||
blockType: chosenBlock.type || 'unknown',
|
||||
blockTitle: chosenBlock.title || 'Untitled Block',
|
||||
},
|
||||
}
|
||||
|
||||
// ADDED: Explicitly store the evaluation decision in the context
|
||||
context.blockStates.set(block.id, {
|
||||
response: result,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a block is reachable along the chosen path from a decision block.
|
||||
*
|
||||
* Uses breadth-first search to:
|
||||
* 1. Start from the chosen block
|
||||
* 2. Follow valid connections
|
||||
* 3. Skip paths from other routers/evaluators
|
||||
* 4. Check if target block is reachable
|
||||
*
|
||||
* @param blockId - ID of block to check
|
||||
* @param chosenBlockId - ID of the chosen target block
|
||||
* @param decisionBlockId - ID of the router/evaluator making the decision
|
||||
* @returns boolean - Whether the block is reachable
|
||||
*/
|
||||
private isInChosenPath(blockId: string, chosenBlockId: string, decisionBlockId: string): boolean {
|
||||
const visited = new Set<string>()
|
||||
const queue = [chosenBlockId]
|
||||
|
||||
// Add the decision block (router/evaluator) itself as valid
|
||||
if (blockId === decisionBlockId) {
|
||||
return true
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift()!
|
||||
if (visited.has(currentId)) continue
|
||||
visited.add(currentId)
|
||||
|
||||
// If we found the block we're looking for
|
||||
if (currentId === blockId) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source)
|
||||
if (
|
||||
sourceBlock?.metadata?.type !== 'router' &&
|
||||
sourceBlock?.metadata?.type !== 'evaluator'
|
||||
) {
|
||||
queue.push(conn.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a condition block that evaluates logical conditions and selects a path.
|
||||
*
|
||||
* Process:
|
||||
* 1. Parses and evaluates conditions in order (if/else-if/else)
|
||||
* 2. Uses source block's output for evaluation context
|
||||
* 3. Selects matching path based on condition result
|
||||
* 4. Stores decision in context for downstream execution
|
||||
*
|
||||
* @param block - The condition block to execute
|
||||
* @param context - Current execution context
|
||||
* @returns Promise with condition result and selected path
|
||||
*/
|
||||
private async executeConditionalBlock(
|
||||
block: SerializedBlock,
|
||||
context: ExecutionContext
|
||||
): Promise<{
|
||||
content: string
|
||||
condition: boolean
|
||||
selectedConditionId: string
|
||||
sourceOutput: BlockOutput
|
||||
selectedPath: {
|
||||
blockId: string
|
||||
blockType: string
|
||||
blockTitle: string
|
||||
}
|
||||
}> {
|
||||
const conditions = JSON.parse(block.config.params.conditions)
|
||||
|
||||
// Identify the source block that feeds into this condition block.
|
||||
const sourceBlockId = this.workflow.connections.find((conn) => conn.target === block.id)?.source
|
||||
|
||||
if (!sourceBlockId) {
|
||||
throw new Error(`No source block found for condition block ${block.id}`)
|
||||
}
|
||||
|
||||
const sourceOutput = context.blockStates.get(sourceBlockId)
|
||||
if (!sourceOutput) {
|
||||
throw new Error(`No output found for source block ${sourceBlockId}`)
|
||||
}
|
||||
|
||||
const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id)
|
||||
|
||||
let conditionMet = false
|
||||
let selectedConnection: { target: string; sourceHandle?: string } | null = null
|
||||
let selectedCondition: { id: string; title: string; value: string } | null = null
|
||||
|
||||
// Evaluate conditions one by one.
|
||||
for (const condition of conditions) {
|
||||
try {
|
||||
// Resolve the condition expression using the current context.
|
||||
const resolvedCondition = this.resolveInputs(
|
||||
{
|
||||
id: block.id,
|
||||
config: { params: { condition: condition.value }, tool: block.config.tool },
|
||||
metadata: block.metadata,
|
||||
position: block.position,
|
||||
inputs: block.inputs,
|
||||
outputs: block.outputs,
|
||||
enabled: block.enabled,
|
||||
},
|
||||
context
|
||||
)
|
||||
const evalContext = {
|
||||
...(typeof sourceOutput === 'object' && sourceOutput !== null ? sourceOutput : {}),
|
||||
agent1: sourceOutput,
|
||||
}
|
||||
conditionMet = new Function(
|
||||
'context',
|
||||
`with(context) { return ${resolvedCondition.condition} }`
|
||||
)(evalContext)
|
||||
|
||||
// Cast the connection so that TypeScript knows it has a target property.
|
||||
const connection = outgoingConnections.find(
|
||||
(conn) => conn.sourceHandle === `condition-${condition.id}`
|
||||
) as { target: string; sourceHandle?: string } | undefined
|
||||
|
||||
if (connection) {
|
||||
// For if/else-if, require conditionMet to be true.
|
||||
// For else, unconditionally select it.
|
||||
if ((condition.title === 'if' || condition.title === 'else if') && conditionMet) {
|
||||
selectedConnection = connection
|
||||
selectedCondition = condition
|
||||
break
|
||||
} else if (condition.title === 'else') {
|
||||
selectedConnection = connection
|
||||
selectedCondition = condition
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to evaluate condition: ${error.message}`, {
|
||||
condition,
|
||||
error,
|
||||
})
|
||||
throw new Error(`Failed to evaluate condition: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedConnection || !selectedCondition) {
|
||||
throw new Error(`No matching path found for condition block ${block.id}`)
|
||||
}
|
||||
|
||||
// Identify the target block based on the selected connection.
|
||||
const targetBlock = this.workflow.blocks.find((b) => b.id === selectedConnection!.target)
|
||||
if (!targetBlock) {
|
||||
throw new Error(`Target block ${selectedConnection!.target} not found`)
|
||||
}
|
||||
|
||||
// Get the raw output from the source block's state
|
||||
const sourceBlockState = context.blockStates.get(sourceBlockId)
|
||||
if (!sourceBlockState) {
|
||||
throw new Error(`No state found for source block ${sourceBlockId}`)
|
||||
}
|
||||
|
||||
// Create the block output with the source output when condition is met
|
||||
const blockOutput = {
|
||||
response: {
|
||||
result: conditionMet ? sourceBlockState : false,
|
||||
content: `Condition '${selectedCondition.title}' evaluated to ${conditionMet}`,
|
||||
condition: {
|
||||
result: conditionMet,
|
||||
selectedPath: {
|
||||
blockId: targetBlock.id,
|
||||
blockType: targetBlock.metadata?.type || '',
|
||||
blockTitle: targetBlock.metadata?.title || '',
|
||||
},
|
||||
selectedConditionId: selectedCondition.id,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Store the block output in the context
|
||||
context.blockStates.set(block.id, blockOutput)
|
||||
|
||||
return {
|
||||
content: `Condition '${selectedCondition.title}' chosen`,
|
||||
condition: conditionMet,
|
||||
selectedConditionId: selectedCondition.id,
|
||||
sourceOutput: sourceBlockState,
|
||||
selectedPath: {
|
||||
blockId: targetBlock.id,
|
||||
blockType: targetBlock.metadata?.type || '',
|
||||
blockTitle: targetBlock.metadata?.title || '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves block input values from context and environment.
|
||||
* Handles template references and variable substitution.
|
||||
*
|
||||
* Features:
|
||||
* - Resolves block references (<blockId.property>)
|
||||
* - Resolves environment variables ({{ENV_VAR}})
|
||||
* - Handles special formatting for function blocks
|
||||
* - Validates references and paths
|
||||
*
|
||||
* @param block - Block whose inputs need resolution
|
||||
* @param context - Current execution context
|
||||
* @returns Record<string, any> - Resolved input values
|
||||
*/
|
||||
private resolveInputs(block: SerializedBlock, context: ExecutionContext): Record<string, any> {
|
||||
const inputs = { ...block.config.params }
|
||||
@@ -688,377 +1097,4 @@ export class Executor {
|
||||
|
||||
return resolvedInputs
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a router block which calculates branching decisions based on a prompt.
|
||||
*/
|
||||
private async executeRouterBlock(
|
||||
block: SerializedBlock,
|
||||
context: ExecutionContext
|
||||
): Promise<{
|
||||
content: string
|
||||
model: string
|
||||
tokens: {
|
||||
prompt: number
|
||||
completion: number
|
||||
total: number
|
||||
}
|
||||
selectedPath: {
|
||||
blockId: string
|
||||
blockType: string
|
||||
blockTitle: string
|
||||
}
|
||||
}> {
|
||||
// Resolve inputs for the router block.
|
||||
const resolvedInputs = this.resolveInputs(block, context)
|
||||
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,
|
||||
title: targetBlock.metadata?.title,
|
||||
description: targetBlock.metadata?.description,
|
||||
subBlocks: targetBlock.config.params,
|
||||
currentState: context.blockStates.get(targetBlock.id),
|
||||
}
|
||||
})
|
||||
|
||||
const routerConfig = {
|
||||
prompt: resolvedInputs.prompt,
|
||||
model: resolvedInputs.model,
|
||||
apiKey: resolvedInputs.apiKey,
|
||||
temperature: resolvedInputs.temperature || 0,
|
||||
}
|
||||
|
||||
const model = routerConfig.model || 'gpt-4o'
|
||||
const providerId = getProviderFromModel(model)
|
||||
|
||||
// Generate and send the router prompt.
|
||||
const response = await executeProviderRequest(providerId, {
|
||||
model: routerConfig.model,
|
||||
systemPrompt: generateRouterPrompt(routerConfig.prompt, targetBlocks),
|
||||
messages: [{ role: 'user', content: routerConfig.prompt }],
|
||||
temperature: routerConfig.temperature,
|
||||
apiKey: routerConfig.apiKey,
|
||||
})
|
||||
|
||||
const chosenBlockId = response.content.trim().toLowerCase()
|
||||
const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId)
|
||||
if (!chosenBlock) {
|
||||
throw new Error(`Invalid routing decision: ${chosenBlockId}`)
|
||||
}
|
||||
|
||||
const tokens = response.tokens || { prompt: 0, completion: 0, total: 0 }
|
||||
return {
|
||||
content: resolvedInputs.prompt,
|
||||
model: response.model,
|
||||
tokens: {
|
||||
prompt: tokens.prompt || 0,
|
||||
completion: tokens.completion || 0,
|
||||
total: tokens.total || 0,
|
||||
},
|
||||
selectedPath: {
|
||||
blockId: chosenBlock.id,
|
||||
blockType: chosenBlock.type || 'unknown',
|
||||
blockTitle: chosenBlock.title || 'Untitled Block',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a router block which calculates branching decisions based on a prompt.
|
||||
*/
|
||||
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
|
||||
}
|
||||
}> {
|
||||
// Resolve inputs for the evaluator block.
|
||||
console.log('Evaluator: Resolving inputs for the evaluator block.')
|
||||
const resolvedInputs = this.resolveInputs(block, context)
|
||||
console.log('Evaluator: Resolved inputs:', resolvedInputs)
|
||||
|
||||
// Get all possible target blocks from outgoing connections
|
||||
const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id)
|
||||
console.log('Evaluator: Outgoing connections:', outgoingConnections)
|
||||
|
||||
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`)
|
||||
}
|
||||
console.log('Evaluator: Found target block:', targetBlock)
|
||||
return {
|
||||
id: targetBlock.id,
|
||||
type: targetBlock.metadata?.type,
|
||||
title: targetBlock.metadata?.title,
|
||||
description: targetBlock.metadata?.description,
|
||||
subBlocks: targetBlock.config.params,
|
||||
currentState: context.blockStates.get(targetBlock.id),
|
||||
}
|
||||
})
|
||||
console.log('Evaluator: Mapped target blocks:', targetBlocks)
|
||||
|
||||
const evaluatorConfig = {
|
||||
prompt: resolvedInputs.prompt,
|
||||
content: resolvedInputs.content,
|
||||
model: resolvedInputs.model,
|
||||
apiKey: resolvedInputs.apiKey,
|
||||
temperature: resolvedInputs.temperature || 0,
|
||||
}
|
||||
|
||||
const model = evaluatorConfig.model || 'gpt-4o'
|
||||
const providerId = getProviderFromModel(model)
|
||||
|
||||
// Generate and execute the evaluator prompt
|
||||
console.log('Evaluator: Sending request with config:', evaluatorConfig)
|
||||
const response = await executeProviderRequest(providerId, {
|
||||
model: evaluatorConfig.model,
|
||||
systemPrompt: generateEvaluatorPrompt(
|
||||
evaluatorConfig.prompt,
|
||||
evaluatorConfig.content,
|
||||
targetBlocks
|
||||
),
|
||||
messages: [{ role: 'user', content: evaluatorConfig.prompt }],
|
||||
temperature: evaluatorConfig.temperature,
|
||||
apiKey: evaluatorConfig.apiKey,
|
||||
})
|
||||
|
||||
console.log('Evaluator: Raw response:', response)
|
||||
const chosenBlockId = response.content.trim().toLowerCase()
|
||||
console.log('Evaluator: Chosen block ID:', chosenBlockId)
|
||||
|
||||
const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId)
|
||||
if (!chosenBlock) {
|
||||
throw new Error(`Invalid evaluation decision: ${chosenBlockId}`)
|
||||
}
|
||||
|
||||
// Store the evaluation result in the context
|
||||
const tokens = response.tokens || { prompt: 0, completion: 0, total: 0 }
|
||||
const result = {
|
||||
content: evaluatorConfig.prompt,
|
||||
model: response.model,
|
||||
tokens: {
|
||||
prompt: tokens.prompt || 0,
|
||||
completion: tokens.completion || 0,
|
||||
total: tokens.total || 0,
|
||||
},
|
||||
selectedPath: {
|
||||
blockId: chosenBlock.id,
|
||||
blockType: chosenBlock.type || 'unknown',
|
||||
blockTitle: chosenBlock.title || 'Untitled Block',
|
||||
},
|
||||
}
|
||||
|
||||
// ADDED: Explicitly store the evaluation decision in the context
|
||||
context.blockStates.set(block.id, {
|
||||
response: result,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a block is reachable along the chosen router path.
|
||||
*
|
||||
* This uses a breadth-first search starting from the chosen block id.
|
||||
*/
|
||||
private isInChosenPath(blockId: string, chosenBlockId: string, decisionBlockId: string): boolean {
|
||||
const visited = new Set<string>()
|
||||
const queue = [chosenBlockId]
|
||||
|
||||
// Add the decision block (router/evaluator) itself as valid
|
||||
if (blockId === decisionBlockId) {
|
||||
return true
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift()!
|
||||
if (visited.has(currentId)) continue
|
||||
visited.add(currentId)
|
||||
|
||||
// If we found the block we're looking for
|
||||
if (currentId === blockId) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source)
|
||||
if (
|
||||
sourceBlock?.metadata?.type !== 'router' &&
|
||||
sourceBlock?.metadata?.type !== 'evaluator'
|
||||
) {
|
||||
queue.push(conn.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a condition block that evaluates a set of conditions (if/else-if/else).
|
||||
*
|
||||
* The block:
|
||||
* - Parses its conditions.
|
||||
* - Uses the source block's output to evaluate each condition.
|
||||
* - Selects the branch matching the evaluation (via sourceHandle in the connection).
|
||||
* - Returns an output that includes the boolean result and the selected condition's ID.
|
||||
*/
|
||||
private async executeConditionalBlock(
|
||||
block: SerializedBlock,
|
||||
context: ExecutionContext
|
||||
): Promise<{
|
||||
content: string
|
||||
condition: boolean
|
||||
selectedConditionId: string
|
||||
sourceOutput: BlockOutput
|
||||
selectedPath: {
|
||||
blockId: string
|
||||
blockType: string
|
||||
blockTitle: string
|
||||
}
|
||||
}> {
|
||||
const conditions = JSON.parse(block.config.params.conditions)
|
||||
console.log('Parsed conditions:', conditions)
|
||||
|
||||
// Identify the source block that feeds into this condition block.
|
||||
const sourceBlockId = this.workflow.connections.find((conn) => conn.target === block.id)?.source
|
||||
|
||||
if (!sourceBlockId) {
|
||||
throw new Error(`No source block found for condition block ${block.id}`)
|
||||
}
|
||||
|
||||
const sourceOutput = context.blockStates.get(sourceBlockId)
|
||||
if (!sourceOutput) {
|
||||
throw new Error(`No output found for source block ${sourceBlockId}`)
|
||||
}
|
||||
console.log('Source block output:', sourceOutput)
|
||||
|
||||
const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id)
|
||||
console.log('Outgoing connections:', outgoingConnections)
|
||||
|
||||
let conditionMet = false
|
||||
let selectedConnection: { target: string; sourceHandle?: string } | null = null
|
||||
let selectedCondition: { id: string; title: string; value: string } | null = null
|
||||
|
||||
// Evaluate conditions one by one.
|
||||
for (const condition of conditions) {
|
||||
try {
|
||||
// Resolve the condition expression using the current context.
|
||||
const resolvedCondition = this.resolveInputs(
|
||||
{
|
||||
id: block.id,
|
||||
config: { params: { condition: condition.value }, tool: block.config.tool },
|
||||
metadata: block.metadata,
|
||||
position: block.position,
|
||||
inputs: block.inputs,
|
||||
outputs: block.outputs,
|
||||
enabled: block.enabled,
|
||||
},
|
||||
context
|
||||
)
|
||||
const evalContext = {
|
||||
...(typeof sourceOutput === 'object' && sourceOutput !== null ? sourceOutput : {}),
|
||||
agent1: sourceOutput,
|
||||
}
|
||||
conditionMet = new Function(
|
||||
'context',
|
||||
`with(context) { return ${resolvedCondition.condition} }`
|
||||
)(evalContext)
|
||||
|
||||
// Cast the connection so that TypeScript knows it has a target property.
|
||||
const connection = outgoingConnections.find(
|
||||
(conn) => conn.sourceHandle === `condition-${condition.id}`
|
||||
) as { target: string; sourceHandle?: string } | undefined
|
||||
|
||||
if (connection) {
|
||||
// For if/else-if, require conditionMet to be true.
|
||||
// For else, unconditionally select it.
|
||||
if ((condition.title === 'if' || condition.title === 'else if') && conditionMet) {
|
||||
selectedConnection = connection
|
||||
selectedCondition = condition
|
||||
break
|
||||
} else if (condition.title === 'else') {
|
||||
selectedConnection = connection
|
||||
selectedCondition = condition
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to evaluate condition: ${error.message}`, {
|
||||
condition,
|
||||
error,
|
||||
})
|
||||
throw new Error(`Failed to evaluate condition: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedConnection || !selectedCondition) {
|
||||
throw new Error(`No matching path found for condition block ${block.id}`)
|
||||
}
|
||||
|
||||
// Identify the target block based on the selected connection.
|
||||
const targetBlock = this.workflow.blocks.find((b) => b.id === selectedConnection!.target)
|
||||
if (!targetBlock) {
|
||||
throw new Error(`Target block ${selectedConnection!.target} not found`)
|
||||
}
|
||||
|
||||
// Get the raw output from the source block's state
|
||||
const sourceBlockState = context.blockStates.get(sourceBlockId)
|
||||
if (!sourceBlockState) {
|
||||
throw new Error(`No state found for source block ${sourceBlockId}`)
|
||||
}
|
||||
|
||||
// Create the block output with the source output when condition is met
|
||||
const blockOutput = {
|
||||
response: {
|
||||
result: conditionMet ? sourceBlockState : false,
|
||||
content: `Condition '${selectedCondition.title}' evaluated to ${conditionMet}`,
|
||||
condition: {
|
||||
result: conditionMet,
|
||||
selectedPath: {
|
||||
blockId: targetBlock.id,
|
||||
blockType: targetBlock.metadata?.type || '',
|
||||
blockTitle: targetBlock.metadata?.title || '',
|
||||
},
|
||||
selectedConditionId: selectedCondition.id,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Store the block output in the context
|
||||
context.blockStates.set(block.id, blockOutput)
|
||||
|
||||
return {
|
||||
content: `Condition '${selectedCondition.title}' chosen`,
|
||||
condition: conditionMet,
|
||||
selectedConditionId: selectedCondition.id,
|
||||
sourceOutput: sourceBlockState,
|
||||
selectedPath: {
|
||||
blockId: targetBlock.id,
|
||||
blockType: targetBlock.metadata?.type || '',
|
||||
blockTitle: targetBlock.metadata?.title || '',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user