mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
chore: run format
This commit is contained in:
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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,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: {
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1755,4 +1755,4 @@ export function ImageIcon(props: SVGProps<SVGSVGElement>) {
|
||||
<path d="M3 21L10 14L14 18L21 11L25 15V21" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
@@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ export const useOllamaStore = create<OllamaState>((set) => ({
|
||||
// Update the providers when models change
|
||||
updateOllamaProviderModels(models)
|
||||
},
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -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
@@ -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'
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user