chore: run format

This commit is contained in:
Waleed Latif
2025-03-30 03:31:46 -07:00
parent 476bccd253
commit 9cbff55a6f
20 changed files with 448 additions and 413 deletions
+6 -8
View File
@@ -67,17 +67,15 @@ export async function POST(req: NextRequest) {
environmentVariables: envVars, // Make environment variables available in the context
console: {
log: (...args: any[]) => {
const logMessage =
args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
const logMessage = args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
stdout += logMessage
},
error: (...args: any[]) => {
const errorMessage =
args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
const errorMessage = args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
logger.error(`[${requestId}] Code Console Error:`, errorMessage)
stdout += 'ERROR: ' + errorMessage
},
+8 -8
View File
@@ -15,7 +15,7 @@ export async function GET(request: Request) {
// Add appropriate headers for fetching images
const response = await fetch(imageUrl, {
headers: {
'Accept': 'image/*, */*',
Accept: 'image/*, */*',
'User-Agent': 'Mozilla/5.0 (compatible; ImageProxyBot/1.0)',
},
// Set a reasonable timeout
@@ -24,17 +24,17 @@ export async function GET(request: Request) {
if (!response.ok) {
console.error(`Failed to fetch image from ${imageUrl}:`, response.status, response.statusText)
return new NextResponse(`Failed to fetch image: ${response.status} ${response.statusText}`, {
status: response.status
return new NextResponse(`Failed to fetch image: ${response.status} ${response.statusText}`, {
status: response.status,
})
}
const contentType = response.headers.get('content-type')
console.log('Image content-type:', contentType)
const blob = await response.blob()
console.log('Image size:', blob.size, 'bytes')
if (blob.size === 0) {
console.error('Empty image received from source URL')
return new NextResponse('Empty image received from source', { status: 422 })
@@ -52,11 +52,11 @@ export async function GET(request: Request) {
} catch (error) {
// Log the full error for debugging
console.error('Error proxying image:', error)
// Return a helpful error response
return new NextResponse(
`Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
`Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
{ status: 500 }
)
}
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
z-index: 30 !important;
}
.workflow-container .react-flow__edge [data-testid="workflow-edge"] {
.workflow-container .react-flow__edge [data-testid='workflow-edge'] {
z-index: 0 !important;
}
@@ -9,7 +9,9 @@ import {
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface DropdownProps {
options: Array<string | { label: string; id: string }> | (() => Array<string | { label: string; id: string }>)
options:
| Array<string | { label: string; id: string }>
| (() => Array<string | { label: string; id: string }>)
defaultValue?: string
blockId: string
subBlockId: string
+1 -1
View File
@@ -1,9 +1,9 @@
import { AgentIcon } from '@/components/icons'
import { useOllamaStore } from '@/stores/ollama/store'
import { MODELS_TEMP_RANGE_0_1, MODELS_TEMP_RANGE_0_2 } from '@/providers/model-capabilities'
import { getAllModelProviders, getBaseModelProviders } from '@/providers/utils'
import { ToolResponse } from '@/tools/types'
import { BlockConfig } from '../types'
import { useOllamaStore } from '@/stores/ollama/store'
interface AgentResponse extends ToolResponse {
output: {
+2 -2
View File
@@ -1,10 +1,10 @@
import { ChartBarIcon } from '@/components/icons'
import { createLogger } from '@/lib/logs/console-logger'
import { useOllamaStore } from '@/stores/ollama/store'
import { ProviderId } from '@/providers/types'
import { getBaseModelProviders, getAllModelProviders } from '@/providers/utils'
import { getAllModelProviders, getBaseModelProviders } from '@/providers/utils'
import { ToolResponse } from '@/tools/types'
import { BlockConfig, ParamType } from '../types'
import { useOllamaStore } from '@/stores/ollama/store'
const logger = createLogger('EvaluatorBlock')
+4 -6
View File
@@ -17,9 +17,7 @@ export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
title: 'Provider',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'DALL-E', id: 'dalle' },
],
options: [{ label: 'DALL-E', id: 'dalle' }],
value: () => 'dalle',
},
{
@@ -121,8 +119,8 @@ export const ImageGeneratorBlock: BlockConfig<DalleResponse> = {
type: {
content: 'string', // URL of the generated image
image: 'string', // Base64 image data
metadata: 'json' // Contains only model information
}
metadata: 'json', // Contains only model information
},
},
},
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
import { ConnectIcon } from '@/components/icons'
import { useOllamaStore } from '@/stores/ollama/store'
import { ProviderId } from '@/providers/types'
import { getBaseModelProviders, getAllModelProviders } from '@/providers/utils'
import { getAllModelProviders, getBaseModelProviders } from '@/providers/utils'
import { ToolResponse } from '@/tools/types'
import { BlockConfig } from '../types'
import { useOllamaStore } from '@/stores/ollama/store'
interface RouterResponse extends ToolResponse {
output: {
+1 -1
View File
@@ -1755,4 +1755,4 @@ export function ImageIcon(props: SVGProps<SVGSVGElement>) {
<path d="M3 21L10 14L14 18L21 11L25 15V21" />
</svg>
)
}
}
+6
View File
@@ -48,6 +48,12 @@ import { cn } from '@/lib/utils'
// This file is not typed correctly from shadcn, so we're disabling the type checker
// @ts-nocheck
// This file is not typed correctly from shadcn, so we're disabling the type checker
// @ts-nocheck
// This file is not typed correctly from shadcn, so we're disabling the type checker
// @ts-nocheck
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive> & {
+210 -206
View File
@@ -238,7 +238,7 @@ const createWorkflowWithLoop = (): SerializedWorkflow => ({
nodes: ['block1', 'block2'],
iterations: 5,
loopType: 'forEach',
forEachItems: [1, 2, 3, 4, 5]
forEachItems: [1, 2, 3, 4, 5],
},
},
})
@@ -324,14 +324,14 @@ const createWorkflowWithNestedLoops = (): SerializedWorkflow => ({
id: 'outerLoop',
nodes: ['outer-block1', 'inner-block1', 'inner-block2', 'outer-block2'],
iterations: 3,
loopType: 'for'
loopType: 'for',
},
innerLoop: {
id: 'innerLoop',
nodes: ['inner-block1', 'inner-block2'],
iterations: 2,
loopType: 'forEach',
forEachItems: ['a', 'b']
forEachItems: ['a', 'b'],
},
},
})
@@ -846,9 +846,9 @@ describe('Executor', () => {
*/
describe('loop management', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});
vi.resetModules()
vi.clearAllMocks()
})
test('should increment loop iterations correctly', async () => {
// Mock the LoopManager
@@ -856,23 +856,23 @@ describe('Executor', () => {
LoopManager: vi.fn().mockImplementation(() => ({
processLoopIterations: vi.fn().mockImplementation(async (context) => {
// Simulate incrementing iteration counter
const currentIteration = context.loopIterations.get('loop1') || 0;
context.loopIterations.set('loop1', currentIteration + 1);
return false;
const currentIteration = context.loopIterations.get('loop1') || 0
context.loopIterations.set('loop1', currentIteration + 1)
return false
}),
getLoopIndex: vi.fn().mockImplementation((loopId, blockId, context) => {
return context.loopIterations.get(loopId) || 0;
})
}))
}));
return context.loopIterations.get(loopId) || 0
}),
})),
}))
// Create a minimal workflow with loop
const workflow = createWorkflowWithLoop();
const workflow = createWorkflowWithLoop()
// Import with mocks applied
const { LoopManager } = await import('./loops');
const loopManager = new LoopManager(workflow.loops);
const { LoopManager } = await import('./loops')
const loopManager = new LoopManager(workflow.loops)
// Create a mock context
const context = {
workflowId: 'test-workflow-id',
@@ -885,60 +885,55 @@ describe('Executor', () => {
loopItems: new Map(),
executedBlocks: new Set<string>(['block1', 'block2']),
activeExecutionPath: new Set<string>(['block1', 'block2']),
workflow
};
workflow,
}
// Process loop iterations to increment counter
await loopManager.processLoopIterations(context);
await loopManager.processLoopIterations(context)
// Verify that the loop iteration counter was incremented
expect(context.loopIterations.get('loop1')).toBe(1);
expect(context.loopIterations.get('loop1')).toBe(1)
// Get loop index
const loopIndex = loopManager.getLoopIndex('loop1', 'block1', context);
const loopIndex = loopManager.getLoopIndex('loop1', 'block1', context)
// The loop index should match the iteration counter
expect(loopIndex).toBe(1);
});
expect(loopIndex).toBe(1)
})
test('should handle forEach loop item access correctly', async () => {
// Mock the InputResolver
vi.doMock('./resolver', () => ({
InputResolver: vi.fn().mockImplementation(() => ({
resolveBlockReferences: vi.fn().mockImplementation((value, context, block) => {
if (value === '<loop.index>') {
const loopId = 'loop1';
return String(context.loopIterations.get(loopId) || 0);
const loopId = 'loop1'
return String(context.loopIterations.get(loopId) || 0)
}
return value;
})
}))
}));
return value
}),
})),
}))
// Mock the LoopManager
vi.doMock('./loops', () => ({
LoopManager: vi.fn().mockImplementation(() => ({
getLoopIndex: vi.fn().mockImplementation((loopId, blockId, context) => {
return context.loopIterations.get(loopId) || 0;
})
}))
}));
return context.loopIterations.get(loopId) || 0
}),
})),
}))
// Create a minimal workflow with forEach loop
const workflow = createWorkflowWithLoop();
const workflow = createWorkflowWithLoop()
// Import with mocks applied
const { Executor } = await import('./index');
const executor = new Executor(workflow);
const { InputResolver } = await import('./resolver');
const resolver = new InputResolver(
workflow,
{},
{},
(executor as any).loopManager
);
const { Executor } = await import('./index')
const executor = new Executor(workflow)
const { InputResolver } = await import('./resolver')
const resolver = new InputResolver(workflow, {}, {}, (executor as any).loopManager)
// Create a mock context
const context = {
workflowId: 'test-workflow-id',
@@ -951,171 +946,180 @@ describe('Executor', () => {
loopItems: new Map([['loop1', 3]]), // Current item is 3
executedBlocks: new Set<string>(['block1']),
activeExecutionPath: new Set<string>(['block1', 'block2']),
workflow
};
workflow,
}
// Resolve a loop index reference
const resolvedIndex = resolver.resolveBlockReferences('<loop.index>', context, workflow.blocks[1]);
const resolvedIndex = resolver.resolveBlockReferences(
'<loop.index>',
context,
workflow.blocks[1]
)
// The resolved index should be 2 (current iteration)
expect(resolvedIndex).toBe('2');
expect(resolvedIndex).toBe('2')
// Set up a different iteration and test again
context.loopIterations.set('loop1', 4);
const resolvedIndexAgain = resolver.resolveBlockReferences('<loop.index>', context, workflow.blocks[1]);
expect(resolvedIndexAgain).toBe('4');
});
context.loopIterations.set('loop1', 4)
const resolvedIndexAgain = resolver.resolveBlockReferences(
'<loop.index>',
context,
workflow.blocks[1]
)
expect(resolvedIndexAgain).toBe('4')
})
test('should update loop indices correctly between iterations', async () => {
// Reset modules to ensure clean state
vi.resetModules();
vi.resetModules()
// Create array to capture indices
const capturedIndices: number[] = [];
const capturedIndices: number[] = []
// Mock the LoopManager implementation
vi.doMock('./loops', () => ({
LoopManager: vi.fn().mockImplementation(() => ({
processLoopIterations: vi.fn().mockImplementation(async (context) => {
// Simulate 3 loop iterations
if (context.executedBlocks.has('block1') && context.executedBlocks.has('block2')) {
const currentIteration = context.loopIterations.get('loop1') || 0;
const currentIteration = context.loopIterations.get('loop1') || 0
if (currentIteration < 2) {
// Increment iteration and reset blocks
context.loopIterations.set('loop1', currentIteration + 1);
context.executedBlocks.delete('block1');
context.executedBlocks.delete('block2');
return false;
context.loopIterations.set('loop1', currentIteration + 1)
context.executedBlocks.delete('block1')
context.executedBlocks.delete('block2')
return false
}
}
return true;
return true
}),
getLoopIndex: vi.fn().mockImplementation((loopId, blockId, context) => {
// Return the current iteration counter
return context.loopIterations.get(loopId) || 0;
})
}))
}));
return context.loopIterations.get(loopId) || 0
}),
})),
}))
// Mock the handlers to capture loop indices
vi.doMock('./handlers', () => ({
AgentBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
RouterBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
ConditionBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
EvaluatorBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
FunctionBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: (block: any) => block.metadata?.id === 'function' || block.id === 'block1' || block.id === 'block2',
canHandle: (block: any) =>
block.metadata?.id === 'function' || block.id === 'block1' || block.id === 'block2',
execute: vi.fn().mockImplementation(async (block, inputs, context) => {
// Capture the loop index during execution
const loopIndex = context.loopIterations.get('loop1') || 0;
capturedIndices.push(loopIndex);
return { response: { result: `Index: ${loopIndex}` } };
})
const loopIndex = context.loopIterations.get('loop1') || 0
capturedIndices.push(loopIndex)
return { response: { result: `Index: ${loopIndex}` } }
}),
})),
ApiBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
GenericBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => true,
execute: vi.fn().mockResolvedValue({ response: { result: 'Executed' } })
}))
}));
execute: vi.fn().mockResolvedValue({ response: { result: 'Executed' } }),
})),
}))
// Mock PathTracker
vi.doMock('./path', () => ({
PathTracker: vi.fn().mockImplementation(() => ({
updateExecutionPaths: vi.fn(),
isInActivePath: vi.fn().mockReturnValue(true)
}))
}));
isInActivePath: vi.fn().mockReturnValue(true),
})),
}))
// Create a workflow with loop
const workflow = createWorkflowWithLoop();
const workflow = createWorkflowWithLoop()
// Import the executor with mocks applied
const { Executor } = await import('./index');
const executor = new Executor(workflow);
const { Executor } = await import('./index')
const executor = new Executor(workflow)
// Manually simulate execution to populate capturedIndices
// First iteration - both blocks with index 0
capturedIndices.push(0, 0);
capturedIndices.push(0, 0)
// Second iteration - both blocks with index 1
capturedIndices.push(1, 1);
capturedIndices.push(1, 1)
// Third iteration - both blocks with index 2
capturedIndices.push(2, 2);
capturedIndices.push(2, 2)
// We should have captured indices 0, 0 (first iteration - both blocks)
// then 1, 1 (second iteration - both blocks)
// then 2, 2 (third iteration - both blocks)
expect(capturedIndices).toEqual([0, 0, 1, 1, 2, 2]);
});
expect(capturedIndices).toEqual([0, 0, 1, 1, 2, 2])
})
test('should handle nested loops correctly', async () => {
// Reset modules to ensure clean state
vi.resetModules();
vi.resetModules()
// Create array to capture indices
const capturedIndices: {loopId: string, blockId: string, index: number}[] = [];
const capturedIndices: { loopId: string; blockId: string; index: number }[] = []
// Mock the LoopManager
vi.doMock('./loops', () => ({
LoopManager: vi.fn().mockImplementation(() => ({
processLoopIterations: vi.fn().mockImplementation(async (context) => {
return true;
return true
}),
getLoopIndex: vi.fn().mockImplementation((loopId, blockId, context) => {
return context.loopIterations.get(loopId) || 0;
})
}))
}));
return context.loopIterations.get(loopId) || 0
}),
})),
}))
// Mock the handlers to capture loop indices
vi.doMock('./handlers', () => ({
AgentBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
RouterBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
ConditionBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
EvaluatorBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
FunctionBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: (block: any) => block.id.includes('block'),
execute: vi.fn().mockImplementation(async (block, inputs, context) => {
return { response: { result: 'Executed' } };
})
return { response: { result: 'Executed' } }
}),
})),
ApiBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => false,
execute: vi.fn()
execute: vi.fn(),
})),
GenericBlockHandler: vi.fn().mockImplementation(() => ({
canHandle: () => true,
execute: vi.fn().mockResolvedValue({ response: { result: 'Executed' } })
}))
}));
execute: vi.fn().mockResolvedValue({ response: { result: 'Executed' } }),
})),
}))
// Manually populate the capturedIndices array for testing
capturedIndices.push(
{ loopId: 'innerLoop', blockId: 'inner-block1', index: 0 },
@@ -1126,60 +1130,60 @@ describe('Executor', () => {
{ loopId: 'outerLoop', blockId: 'outer-block2', index: 0 },
{ loopId: 'outerLoop', blockId: 'outer-block1', index: 1 },
{ loopId: 'outerLoop', blockId: 'outer-block2', index: 1 }
);
)
// Verify that nested loops maintain independent counters
expect(capturedIndices.length).toBeGreaterThan(0);
expect(capturedIndices.length).toBeGreaterThan(0)
// Group captures by loopId
const innerLoopIndices = capturedIndices
.filter(c => c.loopId === 'innerLoop')
.map(c => c.index);
.filter((c) => c.loopId === 'innerLoop')
.map((c) => c.index)
const outerLoopIndices = capturedIndices
.filter(c => c.loopId === 'outerLoop')
.map(c => c.index);
.filter((c) => c.loopId === 'outerLoop')
.map((c) => c.index)
// Verify inner loop indices - should increment on each iteration
expect(innerLoopIndices).toContain(0);
expect(innerLoopIndices).toContain(1);
expect(innerLoopIndices).toContain(0)
expect(innerLoopIndices).toContain(1)
// Verify outer loop indices
expect(outerLoopIndices).toContain(0);
expect(outerLoopIndices).toContain(1);
});
expect(outerLoopIndices).toContain(0)
expect(outerLoopIndices).toContain(1)
})
test('should fix the bug where first two iterations showed same index', async () => {
// Reset modules to ensure clean state
vi.resetModules();
vi.resetModules()
// Mock the LoopManager
vi.doMock('./loops', () => ({
LoopManager: vi.fn().mockImplementation(() => ({
processLoopIterations: vi.fn().mockImplementation(async (context) => {
// Increment iteration when both blocks executed
if (context.executedBlocks.has('block1') && context.executedBlocks.has('block2')) {
const currentIteration = context.loopIterations.get('loop1') || 0;
context.loopIterations.set('loop1', currentIteration + 1);
context.executedBlocks.delete('block1');
context.executedBlocks.delete('block2');
const currentIteration = context.loopIterations.get('loop1') || 0
context.loopIterations.set('loop1', currentIteration + 1)
context.executedBlocks.delete('block1')
context.executedBlocks.delete('block2')
}
return false;
return false
}),
getLoopIndex: vi.fn().mockImplementation((loopId, blockId, context) => {
// Return current iteration counter (not subtracting 1 as in the old buggy version)
return context.loopIterations.get(loopId) || 0;
})
}))
}));
return context.loopIterations.get(loopId) || 0
}),
})),
}))
// Import with mocks applied
const { LoopManager } = await import('./loops');
const { LoopManager } = await import('./loops')
// Create a workflow with a simple loop
const workflow = createWorkflowWithLoop();
const loopManager = new LoopManager(workflow.loops);
const workflow = createWorkflowWithLoop()
const loopManager = new LoopManager(workflow.loops)
// Create a mock context
const context = {
workflowId: 'test-workflow-id',
@@ -1192,54 +1196,54 @@ describe('Executor', () => {
loopItems: new Map(),
executedBlocks: new Set<string>(),
activeExecutionPath: new Set<string>(['block1', 'block2']),
workflow
};
workflow,
}
// First iteration - this should give index 0 for both blocks
const firstIterationIndex1 = loopManager.getLoopIndex('loop1', 'block1', context);
const firstIterationIndex2 = loopManager.getLoopIndex('loop1', 'block2', context);
expect(firstIterationIndex1).toBe(0);
expect(firstIterationIndex2).toBe(0);
const firstIterationIndex1 = loopManager.getLoopIndex('loop1', 'block1', context)
const firstIterationIndex2 = loopManager.getLoopIndex('loop1', 'block2', context)
expect(firstIterationIndex1).toBe(0)
expect(firstIterationIndex2).toBe(0)
// Execute first iteration of both blocks
context.executedBlocks.add('block1');
context.executedBlocks.add('block2');
context.executedBlocks.add('block1')
context.executedBlocks.add('block2')
// Process loop iterations - this should increment the counter to 1
await loopManager.processLoopIterations(context);
await loopManager.processLoopIterations(context)
// Verify counter has been incremented BEFORE resetting blocks
expect(context.loopIterations.get('loop1')).toBe(1);
expect(context.loopIterations.get('loop1')).toBe(1)
// Verify blocks have been reset
expect(context.executedBlocks.has('block1')).toBe(false);
expect(context.executedBlocks.has('block2')).toBe(false);
expect(context.executedBlocks.has('block1')).toBe(false)
expect(context.executedBlocks.has('block2')).toBe(false)
// Now in second iteration - indices should be 1, not 0
const secondIterationIndex1 = loopManager.getLoopIndex('loop1', 'block1', context);
const secondIterationIndex2 = loopManager.getLoopIndex('loop1', 'block2', context);
const secondIterationIndex1 = loopManager.getLoopIndex('loop1', 'block1', context)
const secondIterationIndex2 = loopManager.getLoopIndex('loop1', 'block2', context)
// This is the critical test - indices should be 1 for the second iteration
expect(secondIterationIndex1).toBe(1);
expect(secondIterationIndex2).toBe(1);
expect(secondIterationIndex1).toBe(1)
expect(secondIterationIndex2).toBe(1)
// Execute second iteration of both blocks
context.executedBlocks.add('block1');
context.executedBlocks.add('block2');
context.executedBlocks.add('block1')
context.executedBlocks.add('block2')
// Process loop iterations again - should increment to 2
await loopManager.processLoopIterations(context);
await loopManager.processLoopIterations(context)
// Verify counter has been incremented again
expect(context.loopIterations.get('loop1')).toBe(2);
expect(context.loopIterations.get('loop1')).toBe(2)
// Third iteration indices should be 2
const thirdIterationIndex1 = loopManager.getLoopIndex('loop1', 'block1', context);
const thirdIterationIndex2 = loopManager.getLoopIndex('loop1', 'block2', context);
expect(thirdIterationIndex1).toBe(2);
expect(thirdIterationIndex2).toBe(2);
});
const thirdIterationIndex1 = loopManager.getLoopIndex('loop1', 'block1', context)
const thirdIterationIndex2 = loopManager.getLoopIndex('loop1', 'block2', context)
expect(thirdIterationIndex1).toBe(2)
expect(thirdIterationIndex2).toBe(2)
})
})
})
+6 -1
View File
@@ -46,7 +46,12 @@ export class Executor {
this.workflowInput = workflowInput || {}
this.loopManager = new LoopManager(workflow.loops || {})
this.resolver = new InputResolver(workflow, environmentVariables, workflowVariables, this.loopManager)
this.resolver = new InputResolver(
workflow,
environmentVariables,
workflowVariables,
this.loopManager
)
this.pathTracker = new PathTracker(workflow)
this.blockHandlers = [
+62 -56
View File
@@ -31,17 +31,20 @@ export class LoopManager {
// Handle forEach loop
if (loopType === 'forEach') {
// Get the items to iterate over if we haven't already processed them into an array/object
if (!loop.forEachItems || typeof loop.forEachItems === 'string' ||
!(Array.isArray(loop.forEachItems) || typeof loop.forEachItems === 'object')) {
if (
!loop.forEachItems ||
typeof loop.forEachItems === 'string' ||
!(Array.isArray(loop.forEachItems) || typeof loop.forEachItems === 'object')
) {
// Evaluate the forEach items expression
const items = await this.evalForEachItems(loopId, loop, context);
const items = await this.evalForEachItems(loopId, loop, context)
// Store the evaluated items for future iterations
if (Array.isArray(items) || (typeof items === 'object' && items !== null)) {
loop.forEachItems = items;
loop.forEachItems = items
} else {
// Default to empty array if we couldn't get any valid items
loop.forEachItems = [];
loop.forEachItems = []
}
}
@@ -49,10 +52,10 @@ export class LoopManager {
const currentIteration = context.loopIterations.get(loopId) || 0
// For forEach, convert to array if it's an object
const items = Array.isArray(loop.forEachItems)
? loop.forEachItems
const items = Array.isArray(loop.forEachItems)
? loop.forEachItems
: Object.entries(loop.forEachItems as Record<string, any>)
// If we've processed all items or hit max iterations, skip this loop
if (currentIteration >= items.length || currentIteration >= loop.iterations) {
if (currentIteration >= items.length) {
@@ -63,14 +66,14 @@ export class LoopManager {
// Check if all blocks in the loop have been executed
const allExecuted = this.allBlocksExecuted(loop.nodes, context)
if (allExecuted) {
// Get current item to process in this iteration
const currentItem = items[currentIteration]
// Store the current item in the context for blocks to access via <loop.currentItem>
context.loopItems.set(loopId, currentItem)
// IMPORTANT: We're incrementing the iteration counter AFTER storing the current item
// But BEFORE resetting the blocks for next iteration
// This ensures that when blocks execute in the new iteration, they'll get the correct index
@@ -109,7 +112,7 @@ export class LoopManager {
// Check if all blocks in the loop have been executed
const allExecuted = this.allBlocksExecuted(loop.nodes, context)
if (allExecuted) {
// IMPORTANT: Increment the counter BEFORE resetting blocks for the next iteration
// This ensures the next iteration will show the correct index value
@@ -145,7 +148,7 @@ export class LoopManager {
/**
* Gets the correct loop index based on the current block being executed.
* Accounts for position within the loop cycle to provide accurate index.
*
*
* @param loopId - ID of the loop
* @param blockId - ID of the block requesting the index
* @param context - Current execution context
@@ -154,20 +157,20 @@ export class LoopManager {
getLoopIndex(loopId: string, blockId: string, context: ExecutionContext): number {
const loop = this.loops[loopId]
if (!loop) return 0
// Get the current iteration counter from context
const iterationCounter = context.loopIterations.get(loopId) || 0
// Simply return the current iteration counter
// Since we're updating the iteration counter BEFORE resetting blocks,
// the counter will already be at the correct value for the current iteration
return iterationCounter
}
/**
* Determines the execution order of blocks in a loop based on the connections.
* This is needed to figure out which blocks should be assigned which iteration.
*
*
* @param nodeIds - IDs of nodes in the loop
* @param context - Current execution context
* @returns Array of block IDs in execution order
@@ -176,22 +179,22 @@ export class LoopManager {
// Start with the entry block
const entryBlock = this.findEntryBlock(nodeIds, context)
if (!entryBlock) return nodeIds
const result: string[] = [entryBlock]
const visited = new Set<string>([entryBlock])
// Perform a depth-first traversal to determine execution order
const traverse = (nodeId: string) => {
// Find all outgoing connections from this node
const connections = context.workflow?.connections.filter(
conn => conn.source === nodeId &&
nodeIds.includes(conn.target) &&
conn.sourceHandle !== 'error'
) || []
const connections =
context.workflow?.connections.filter(
(conn) =>
conn.source === nodeId && nodeIds.includes(conn.target) && conn.sourceHandle !== 'error'
) || []
// Sort by target node to ensure deterministic order
connections.sort((a, b) => a.target.localeCompare(b.target))
// Visit each target node
for (const conn of connections) {
if (!visited.has(conn.target)) {
@@ -201,99 +204,102 @@ export class LoopManager {
}
}
}
// Start traversal from the entry block
traverse(entryBlock)
// If there are nodes we didn't visit, add them at the end
for (const nodeId of nodeIds) {
if (!visited.has(nodeId)) {
result.push(nodeId)
}
}
return result
}
/**
* Evaluates the forEach items string or retrieves items for a forEach loop.
*
*
* @param loopId - ID of the loop
* @param loop - Loop configuration
* @param context - Current execution context
* @returns Items to iterate over (array or object)
*/
private async evalForEachItems(
loopId: string,
loop: SerializedLoop,
loopId: string,
loop: SerializedLoop,
context: ExecutionContext
): Promise<any[] | Record<string, any> | undefined> {
// If forEachItems is not set, return empty array
if (!loop.forEachItems) {
return [];
return []
}
// If we already have items as an array or object, return them directly
if (Array.isArray(loop.forEachItems) || (typeof loop.forEachItems === 'object' && loop.forEachItems !== null)) {
return loop.forEachItems as any[] | Record<string, any>;
if (
Array.isArray(loop.forEachItems) ||
(typeof loop.forEachItems === 'object' && loop.forEachItems !== null)
) {
return loop.forEachItems as any[] | Record<string, any>
}
// If we have forEachItems as a string, try to evaluate it as an expression
if (typeof loop.forEachItems === 'string') {
try {
// Skip comments or empty expressions
const trimmedExpression = loop.forEachItems.trim();
const trimmedExpression = loop.forEachItems.trim()
if (trimmedExpression.startsWith('//') || trimmedExpression === '') {
return [];
return []
}
// First check if it's valid JSON (array or object)
if (trimmedExpression.startsWith('[') || trimmedExpression.startsWith('{')) {
try {
// Try to parse as JSON first
return JSON.parse(trimmedExpression);
return JSON.parse(trimmedExpression)
} catch (jsonError) {
console.error(`Error parsing JSON for loop ${loopId}:`, jsonError);
console.error(`Error parsing JSON for loop ${loopId}:`, jsonError)
// If JSON parsing fails, continue with expression evaluation
}
}
// If not valid JSON or JSON parsing failed, try to evaluate as an expression
const result = new Function('context', `return ${loop.forEachItems}`)(context);
const result = new Function('context', `return ${loop.forEachItems}`)(context)
// If the result is an array or object, return it
if (Array.isArray(result) || (typeof result === 'object' && result !== null)) {
return result;
return result
}
// If it's a primitive, wrap it in an array
if (result !== undefined) {
return [result];
return [result]
}
return [];
return []
} catch (e) {
console.error(`Error evaluating forEach items for loop ${loopId}:`, e);
return [];
console.error(`Error evaluating forEach items for loop ${loopId}:`, e)
return []
}
}
// As a fallback, try to find the first non-empty array or object in the context
for (const [blockId, blockState] of context.blockStates.entries()) {
const output = blockState.output?.response;
const output = blockState.output?.response
if (output) {
// Look for arrays or objects in the response that could be iterated over
for (const [key, value] of Object.entries(output)) {
if (Array.isArray(value) && value.length > 0) {
return value;
return value
} else if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) {
return value;
return value
}
}
}
}
return [];
return []
}
/**
+71 -61
View File
@@ -1,6 +1,6 @@
import { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
import { ExecutionContext } from './types'
import { LoopManager } from './loops'
import { ExecutionContext } from './types'
/**
* Resolves input values for blocks by handling references and variable substitution.
@@ -182,91 +182,98 @@ export class InputResolver {
if (containingLoopId) {
const loop = context.workflow?.loops[containingLoopId]
const loopType = loop?.loopType || 'for'
// Handle each loop property
if (pathParts[0] === 'currentItem') {
// Get the items to iterate over
const items = this.getLoopItems(loop, context);
const items = this.getLoopItems(loop, context)
// Get the correct index using the LoopManager
const index = this.loopManager
const index = this.loopManager
? this.loopManager.getLoopIndex(containingLoopId, currentBlock.id, context)
: context.loopIterations.get(containingLoopId) || 0;
: context.loopIterations.get(containingLoopId) || 0
// Get the current item directly from the items array at the current index
if (Array.isArray(items) && index >= 0 && index < items.length) {
const currentItem = items[index];
const currentItem = items[index]
// Format the value based on type
if (currentItem !== undefined) {
if (typeof currentItem !== 'object' || currentItem === null) {
// For primitives, convert to string
resolvedValue = resolvedValue.replace(match, String(currentItem));
} else if (Array.isArray(currentItem) && currentItem.length === 2 && typeof currentItem[0] === 'string') {
resolvedValue = resolvedValue.replace(match, String(currentItem))
} else if (
Array.isArray(currentItem) &&
currentItem.length === 2 &&
typeof currentItem[0] === 'string'
) {
// Handle [key, value] pair from Object.entries()
if (pathParts.length > 1) {
if (pathParts[1] === 'key') {
resolvedValue = resolvedValue.replace(match, String(currentItem[0]));
resolvedValue = resolvedValue.replace(match, String(currentItem[0]))
} else if (pathParts[1] === 'value') {
const itemValue = currentItem[1];
const formattedValue = typeof itemValue === 'object' && itemValue !== null
? JSON.stringify(itemValue)
: String(itemValue);
resolvedValue = resolvedValue.replace(match, formattedValue);
const itemValue = currentItem[1]
const formattedValue =
typeof itemValue === 'object' && itemValue !== null
? JSON.stringify(itemValue)
: String(itemValue)
resolvedValue = resolvedValue.replace(match, formattedValue)
}
} else {
// Default to stringifying the whole item
resolvedValue = resolvedValue.replace(match, JSON.stringify(currentItem));
resolvedValue = resolvedValue.replace(match, JSON.stringify(currentItem))
}
} else {
// Navigate path if provided for objects
if (pathParts.length > 1) {
let itemValue = currentItem;
let itemValue = currentItem
for (let i = 1; i < pathParts.length; i++) {
if (!itemValue || typeof itemValue !== 'object') {
throw new Error(`Invalid path "${pathParts[i]}" in loop item reference "${path}"`);
throw new Error(
`Invalid path "${pathParts[i]}" in loop item reference "${path}"`
)
}
itemValue = itemValue[pathParts[i]];
itemValue = itemValue[pathParts[i]]
if (itemValue === undefined) {
throw new Error(`No value found at path "${path}" in loop item`);
throw new Error(`No value found at path "${path}" in loop item`)
}
}
const formattedValue = typeof itemValue === 'object' && itemValue !== null
? JSON.stringify(itemValue)
: String(itemValue);
resolvedValue = resolvedValue.replace(match, formattedValue);
const formattedValue =
typeof itemValue === 'object' && itemValue !== null
? JSON.stringify(itemValue)
: String(itemValue)
resolvedValue = resolvedValue.replace(match, formattedValue)
} else {
// Return the whole item as JSON
resolvedValue = resolvedValue.replace(match, JSON.stringify(currentItem));
resolvedValue = resolvedValue.replace(match, JSON.stringify(currentItem))
}
}
}
continue;
continue
}
} else if (pathParts[0] === 'items' && loopType === 'forEach') {
// Get all items in the forEach loop
const items = this.getLoopItems(loop, context);
const items = this.getLoopItems(loop, context)
if (items) {
// Format the items based on type
const formattedValue = typeof items === 'object' && items !== null
? JSON.stringify(items)
: String(items);
resolvedValue = resolvedValue.replace(match, formattedValue);
continue;
const formattedValue =
typeof items === 'object' && items !== null ? JSON.stringify(items) : String(items)
resolvedValue = resolvedValue.replace(match, formattedValue)
continue
}
} else if (pathParts[0] === 'index') {
// Use the LoopManager to get the correct index
const index = this.loopManager
? this.loopManager.getLoopIndex(containingLoopId, currentBlock.id, context)
: context.loopIterations.get(containingLoopId) || 0;
resolvedValue = resolvedValue.replace(match, String(index));
continue;
: context.loopIterations.get(containingLoopId) || 0
resolvedValue = resolvedValue.replace(match, String(index))
continue
}
}
}
@@ -614,58 +621,61 @@ export class InputResolver {
* @returns The items to iterate over (array or object)
*/
private getLoopItems(loop: any, context: ExecutionContext): any[] | Record<string, any> | null {
if (!loop) return null;
if (!loop) return null
// If items are already available as an array or object, return them directly
if (loop.forEachItems) {
if (Array.isArray(loop.forEachItems) || (typeof loop.forEachItems === 'object' && loop.forEachItems !== null)) {
return loop.forEachItems;
if (
Array.isArray(loop.forEachItems) ||
(typeof loop.forEachItems === 'object' && loop.forEachItems !== null)
) {
return loop.forEachItems
}
// If it's a string, try to evaluate it (could be an expression or JSON)
if (typeof loop.forEachItems === 'string') {
try {
// Check if it's valid JSON
const trimmedExpression = loop.forEachItems.trim();
const trimmedExpression = loop.forEachItems.trim()
if (trimmedExpression.startsWith('[') || trimmedExpression.startsWith('{')) {
try {
// Try to parse as JSON first
return JSON.parse(trimmedExpression);
return JSON.parse(trimmedExpression)
} catch (jsonError) {
console.error(`Error parsing JSON for loop:`, jsonError);
console.error(`Error parsing JSON for loop:`, jsonError)
// If JSON parsing fails, continue with expression evaluation
}
}
// If not valid JSON or JSON parsing failed, try to evaluate as an expression
if (trimmedExpression && !trimmedExpression.startsWith('//')) {
const result = new Function('context', `return ${loop.forEachItems}`)(context);
const result = new Function('context', `return ${loop.forEachItems}`)(context)
if (Array.isArray(result) || (typeof result === 'object' && result !== null)) {
return result;
return result
}
}
} catch (e) {
console.error(`Error evaluating forEach items:`, e);
console.error(`Error evaluating forEach items:`, e)
}
}
}
// As a fallback, look for the most recent array or object in any block's output
// This is less reliable but might help in some cases
for (const [blockId, blockState] of context.blockStates.entries()) {
const output = blockState.output?.response;
const output = blockState.output?.response
if (output) {
for (const [key, value] of Object.entries(output)) {
if (Array.isArray(value) && value.length > 0) {
return value;
return value
} else if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) {
return value;
return value
}
}
}
}
// Default to empty array if no valid items found
return [];
return []
}
}
+4 -5
View File
@@ -1,9 +1,9 @@
import OpenAI from 'openai'
import { createLogger } from '@/lib/logs/console-logger'
import { useOllamaStore } from '@/stores/ollama/store'
import { executeTool } from '@/tools'
import { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types'
import { ModelsObject } from './types'
import { useOllamaStore } from '@/stores/ollama/store'
const logger = createLogger('Ollama Provider')
const OLLAMA_HOST = 'http://localhost:11434'
@@ -25,12 +25,12 @@ export const ollamaProvider: ProviderConfig = {
logger.warn('Ollama service is not available. The provider will be disabled.')
return
}
const data = await response.json() as ModelsObject
const data = (await response.json()) as ModelsObject
this.models = data.models.map((model) => model.name)
useOllamaStore.getState().setModels(this.models)
} catch (error) {
logger.warn('Ollama model instantiation failed. The provider will be disabled.', {
error: error instanceof Error ? error.message : 'Unknown error'
error: error instanceof Error ? error.message : 'Unknown error',
})
}
},
@@ -52,7 +52,7 @@ export const ollamaProvider: ProviderConfig = {
try {
// Prepare messages array
const ollama = new OpenAI({
apiKey: "empty",
apiKey: 'empty',
baseURL: `${OLLAMA_HOST}/v1`,
dangerouslyAllowBrowser: true,
})
@@ -296,4 +296,3 @@ export const ollamaProvider: ProviderConfig = {
}
},
}
+10 -10
View File
@@ -1,12 +1,12 @@
interface Model {
name: string;
model: string;
modified_at: string;
size: number;
digest: string;
details: object;
}
name: string
model: string
modified_at: string
size: number
digest: string
details: object
}
export interface ModelsObject {
models: Model[];
}
models: Model[]
}
+3 -3
View File
@@ -5,8 +5,8 @@ import { cerebrasProvider } from './cerebras'
import { deepseekProvider } from './deepseek'
import { googleProvider } from './google'
import { groqProvider } from './groq'
import { openaiProvider } from './openai'
import { ollamaProvider } from './ollama'
import { openaiProvider } from './openai'
import { getModelPricing } from './pricing'
import { ProviderConfig, ProviderId, ProviderToolConfig } from './types'
import { xAIProvider } from './xai'
@@ -72,9 +72,9 @@ export const providers: Record<
// Initialize all providers that have initialize method
Object.entries(providers).forEach(([id, provider]) => {
if (provider.initialize) {
provider.initialize().catch(error => {
provider.initialize().catch((error) => {
logger.error(`Failed to initialize ${id} provider`, {
error: error instanceof Error ? error.message : 'Unknown error'
error: error instanceof Error ? error.message : 'Unknown error',
})
})
}
+1 -1
View File
@@ -17,4 +17,4 @@ export const useOllamaStore = create<OllamaState>((set) => ({
// Update the providers when models change
updateOllamaProviderModels(models)
},
}))
}))
+14 -13
View File
@@ -193,7 +193,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
nodes: path,
iterations: 5,
loopType: 'for', // Default to 'for' loop
forEachItems: ''
forEachItems: '',
}
processedPaths.add(canonicalPath)
}
@@ -233,7 +233,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
nodes: path,
iterations: 5,
loopType: 'for', // Default to 'for' loop
forEachItems: ''
forEachItems: '',
}
processedPaths.add(canonicalPath)
}
@@ -563,27 +563,28 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
updateLoopForEachItems: (loopId: string, items: string) => {
let parsedItems: any = items;
let parsedItems: any = items
// Try to parse the string as JSON if it looks like JSON
if (typeof items === 'string' &&
((items.trim().startsWith('[') && items.trim().endsWith(']')) ||
(items.trim().startsWith('{') && items.trim().endsWith('}')))
if (
typeof items === 'string' &&
((items.trim().startsWith('[') && items.trim().endsWith(']')) ||
(items.trim().startsWith('{') && items.trim().endsWith('}')))
) {
try {
// First try to parse to validate it's valid JSON
const parsed = JSON.parse(items);
const parsed = JSON.parse(items)
// If parsing succeeds, store the original string to preserve formatting
// This way we keep the user's exact formatting (spacing, line breaks, etc.)
parsedItems = items;
parsedItems = items
} catch (e) {
// If parsing fails, keep it as a string expression
console.error('Invalid JSON format for forEach items:', e);
parsedItems = items;
console.error('Invalid JSON format for forEach items:', e)
parsedItems = items
}
}
const newState = {
blocks: { ...get().blocks },
edges: [...get().edges],
+33 -27
View File
@@ -13,7 +13,7 @@ export interface DalleResponse extends ToolResponse {
export const dalleTool: ToolConfig = {
id: 'dalle_generate',
name: 'DALL-E Generate',
description: 'Generate images using OpenAI\'s DALL-E model',
description: "Generate images using OpenAI's DALL-E model",
version: '1.0.0',
params: {
prompt: {
@@ -71,9 +71,9 @@ export const dalleTool: ToolConfig = {
transformResponse: async (response, params) => {
try {
const data = await response.json()
console.log('DALL-E API response:', JSON.stringify(data, null, 2))
if (!data.data?.[0]?.url) {
console.error('No image URL in DALL-E response:', data)
throw new Error('No image URL in response')
@@ -81,37 +81,40 @@ export const dalleTool: ToolConfig = {
const imageUrl = data.data[0].url
const modelName = data.model || params?.model || 'dall-e'
console.log('Got image URL:', imageUrl)
console.log('Using model:', modelName)
try {
// Fetch the image using the proxy-image endpoint instead of direct fetch
console.log('Fetching image from URL via proxy...')
const proxyUrl = `/api/proxy-image?url=${encodeURIComponent(imageUrl)}`
const imageResponse = await fetch(proxyUrl, {
headers: {
'Accept': 'image/*, */*',
Accept: 'image/*, */*',
},
cache: 'no-store', // Don't use cache
cache: 'no-store', // Don't use cache
})
if (!imageResponse.ok) {
console.error('Failed to fetch image:', imageResponse.status, imageResponse.statusText)
throw new Error(`Failed to fetch image: ${imageResponse.statusText}`)
}
console.log('Image fetch successful, content-type:', imageResponse.headers.get('content-type'))
console.log(
'Image fetch successful, content-type:',
imageResponse.headers.get('content-type')
)
const imageBlob = await imageResponse.blob()
console.log('Image blob size:', imageBlob.size)
if (imageBlob.size === 0) {
console.error('Empty image blob received')
throw new Error('Empty image received')
}
const reader = new FileReader()
const base64Promise = new Promise<string>((resolve, reject) => {
reader.onloadend = () => {
@@ -121,7 +124,7 @@ export const dalleTool: ToolConfig = {
reject(new Error('No data read from image'))
return
}
const base64Content = base64data.split(',')[1] // Remove the data URL prefix
console.log('Successfully converted image to base64, length:', base64Content.length)
resolve(base64Content)
@@ -136,7 +139,7 @@ export const dalleTool: ToolConfig = {
}
reader.readAsDataURL(imageBlob)
})
const base64Image = await base64Promise
console.log('Returning success response with image data')
@@ -153,27 +156,27 @@ export const dalleTool: ToolConfig = {
} catch (error) {
// Log the error but continue with returning the URL
console.error('Error fetching or processing image:', error)
// Try again with a direct browser fetch as fallback
try {
console.log('Attempting fallback with direct browser fetch...')
const directImageResponse = await fetch(imageUrl, {
cache: 'no-store',
headers: {
'Accept': 'image/*, */*',
Accept: 'image/*, */*',
'User-Agent': 'Mozilla/5.0 (compatible; DalleProxy/1.0)',
},
})
if (!directImageResponse.ok) {
throw new Error(`Direct fetch failed: ${directImageResponse.status}`)
}
const imageBlob = await directImageResponse.blob()
if (imageBlob.size === 0) {
throw new Error('Empty blob received from direct fetch')
}
const reader = new FileReader()
const base64Promise = new Promise<string>((resolve, reject) => {
reader.onloadend = () => {
@@ -183,9 +186,12 @@ export const dalleTool: ToolConfig = {
reject(new Error('No data read from image'))
return
}
const base64Content = base64data.split(',')[1]
console.log('Successfully converted image to base64 via direct fetch, length:', base64Content.length)
console.log(
'Successfully converted image to base64 via direct fetch, length:',
base64Content.length
)
resolve(base64Content)
} catch (err) {
reject(err)
@@ -194,9 +200,9 @@ export const dalleTool: ToolConfig = {
reader.onerror = reject
reader.readAsDataURL(imageBlob)
})
const base64Image = await base64Promise
return {
success: true,
output: {
@@ -209,7 +215,7 @@ export const dalleTool: ToolConfig = {
}
} catch (fallbackError) {
console.error('Fallback fetch also failed:', fallbackError)
// Even if both attempts fail, still return the URL and metadata
return {
success: true,
@@ -225,7 +231,7 @@ export const dalleTool: ToolConfig = {
}
} catch (error) {
console.error('Error in DALL-E response handling:', error)
throw error;
throw error
}
},
transformError: (error) => {
@@ -235,4 +241,4 @@ export const dalleTool: ToolConfig = {
}
return error.message || 'Failed to generate image with DALL-E'
},
}
}