mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 15:38:00 -05:00
v0.2.4: feat, improvement, fix (#595)
* feat(function): added more granular error logs for function execution for easier debugging (#593) * added more granular error logs for function execution * added tests * fixed syntax error reporting * feat(models): added temp controls for gpt-4.1 family of models (#594) * improvement(knowledge-upload): create and upload document to KB (#579) * improvement: added knowledge upload * improvement: added greptile comments (#579) * improvement: changed to text to doc (#579) * improvement: removed comment (#579) * added input validation, tested persistence of KB selector * update docs --------- Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net> Co-authored-by: Waleed Latif <walif6@gmail.com> * fix(remove workflow.state usage): no more usage of deprecated state column in any routes (#586) * fix(remove workflow.state usage): no more usage of deprecated state col in routes * fix lint * fix chat route to only use deployed state * fix lint * better typing * remove useless logs * fix lint * restore workflow handler file * removed all other usages of deprecated 'state' column from workflows table, updated tests --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local> Co-authored-by: Waleed Latif <walif6@gmail.com> --------- Co-authored-by: Waleed Latif <walif6@gmail.com> Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com> Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>
This commit is contained in:
committed by
GitHub
parent
86168f1a87
commit
50d6ea6c9d
@@ -49,7 +49,7 @@ In Sim Studio, the Knowledge Base block enables your agents to perform intellige
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Perform semantic vector search across one or more knowledge bases or upload new chunks to documents. Uses advanced AI embeddings to understand meaning and context for search operations.
|
||||
Perform semantic vector search across knowledge bases, upload individual chunks to existing documents, or create new documents from text content. Uses advanced AI embeddings to understand meaning and context for search operations.
|
||||
|
||||
|
||||
|
||||
@@ -100,6 +100,25 @@ Upload a new chunk to a document in a knowledge base
|
||||
| `createdAt` | string |
|
||||
| `updatedAt` | string |
|
||||
|
||||
### `knowledge_create_document`
|
||||
|
||||
Create a new document in a knowledge base
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `knowledgeBaseId` | string | Yes | ID of the knowledge base containing the document |
|
||||
| `name` | string | Yes | Name of the document |
|
||||
| `content` | string | Yes | Content of the document |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type |
|
||||
| --------- | ---- |
|
||||
| `data` | string |
|
||||
| `name` | string |
|
||||
|
||||
|
||||
|
||||
## Block Configuration
|
||||
|
||||
@@ -292,12 +292,12 @@ export async function executeWorkflowForChat(
|
||||
|
||||
logger.debug(`[${requestId}] Using ${outputBlockIds.length} output blocks for extraction`)
|
||||
|
||||
// Find the workflow
|
||||
// Find the workflow (deployedState is NOT deprecated - needed for chat execution)
|
||||
const workflowResult = await db
|
||||
.select({
|
||||
state: workflow.state,
|
||||
deployedState: workflow.deployedState,
|
||||
isDeployed: workflow.isDeployed,
|
||||
deployedState: workflow.deployedState,
|
||||
variables: workflow.variables,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
@@ -308,9 +308,14 @@ export async function executeWorkflowForChat(
|
||||
throw new Error('Workflow not available')
|
||||
}
|
||||
|
||||
// Use deployed state for execution
|
||||
const state = workflowResult[0].deployedState || workflowResult[0].state
|
||||
const { blocks, edges, loops, parallels } = state as WorkflowState
|
||||
// For chat execution, use ONLY the deployed state (no fallback)
|
||||
if (!workflowResult[0].deployedState) {
|
||||
throw new Error(`Workflow must be deployed to be available for chat`)
|
||||
}
|
||||
|
||||
// Use deployed state for chat execution (this is the stable, deployed version)
|
||||
const deployedState = workflowResult[0].deployedState as WorkflowState
|
||||
const { blocks, edges, loops, parallels } = deployedState
|
||||
|
||||
// Prepare for execution, similar to use-workflow-execution.ts
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
@@ -344,16 +349,13 @@ export async function executeWorkflowForChat(
|
||||
logger.warn(`[${requestId}] Could not fetch environment variables:`, error)
|
||||
}
|
||||
|
||||
// Get workflow variables
|
||||
let workflowVariables = {}
|
||||
try {
|
||||
// The workflow state may contain variables
|
||||
const workflowState = state as any
|
||||
if (workflowState.variables) {
|
||||
if (workflowResult[0].variables) {
|
||||
workflowVariables =
|
||||
typeof workflowState.variables === 'string'
|
||||
? JSON.parse(workflowState.variables)
|
||||
: workflowState.variables
|
||||
typeof workflowResult[0].variables === 'string'
|
||||
? JSON.parse(workflowResult[0].variables)
|
||||
: workflowResult[0].variables
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Could not parse workflow variables:`, error)
|
||||
|
||||
@@ -391,6 +391,225 @@ describe('Function Execute API Route', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Enhanced Error Handling', () => {
|
||||
it('should provide detailed syntax error with line content', async () => {
|
||||
// Mock VM Script to throw a syntax error
|
||||
const mockScript = vi.fn().mockImplementation(() => {
|
||||
const error = new Error('Invalid or unexpected token')
|
||||
error.name = 'SyntaxError'
|
||||
error.stack = `user-function.js:5
|
||||
description: "This has a missing closing quote
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
SyntaxError: Invalid or unexpected token
|
||||
at new Script (node:vm:117:7)
|
||||
at POST (/path/to/route.ts:123:24)`
|
||||
throw error
|
||||
})
|
||||
|
||||
vi.doMock('vm', () => ({
|
||||
createContext: mockCreateContext,
|
||||
Script: mockScript,
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toContain('Syntax Error')
|
||||
expect(data.error).toContain('Line 3')
|
||||
expect(data.error).toContain('description: "This has a missing closing quote')
|
||||
expect(data.error).toContain('Invalid or unexpected token')
|
||||
expect(data.error).toContain('(Check for missing quotes, brackets, or semicolons)')
|
||||
|
||||
// Check debug information
|
||||
expect(data.debug).toBeDefined()
|
||||
expect(data.debug.line).toBe(3)
|
||||
expect(data.debug.errorType).toBe('SyntaxError')
|
||||
expect(data.debug.lineContent).toBe('description: "This has a missing closing quote')
|
||||
})
|
||||
|
||||
it('should provide detailed runtime error with line and column', async () => {
|
||||
// Create the error object first
|
||||
const runtimeError = new Error("Cannot read properties of null (reading 'someMethod')")
|
||||
runtimeError.name = 'TypeError'
|
||||
runtimeError.stack = `TypeError: Cannot read properties of null (reading 'someMethod')
|
||||
at user-function.js:4:16
|
||||
at user-function.js:9:3
|
||||
at Script.runInContext (node:vm:147:14)`
|
||||
|
||||
// Mock successful script creation but runtime error
|
||||
const mockScript = vi.fn().mockImplementation(() => ({
|
||||
runInContext: vi.fn().mockRejectedValue(runtimeError),
|
||||
}))
|
||||
|
||||
vi.doMock('vm', () => ({
|
||||
createContext: mockCreateContext,
|
||||
Script: mockScript,
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = null;\nreturn obj.someMethod();',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toContain('Type Error')
|
||||
expect(data.error).toContain('Line 2')
|
||||
expect(data.error).toContain('return obj.someMethod();')
|
||||
expect(data.error).toContain('Cannot read properties of null')
|
||||
|
||||
// Check debug information
|
||||
expect(data.debug).toBeDefined()
|
||||
expect(data.debug.line).toBe(2)
|
||||
expect(data.debug.column).toBe(16)
|
||||
expect(data.debug.errorType).toBe('TypeError')
|
||||
expect(data.debug.lineContent).toBe('return obj.someMethod();')
|
||||
})
|
||||
|
||||
it('should handle ReferenceError with enhanced details', async () => {
|
||||
// Create the error object first
|
||||
const referenceError = new Error('undefinedVariable is not defined')
|
||||
referenceError.name = 'ReferenceError'
|
||||
referenceError.stack = `ReferenceError: undefinedVariable is not defined
|
||||
at user-function.js:4:8
|
||||
at Script.runInContext (node:vm:147:14)`
|
||||
|
||||
const mockScript = vi.fn().mockImplementation(() => ({
|
||||
runInContext: vi.fn().mockRejectedValue(referenceError),
|
||||
}))
|
||||
|
||||
vi.doMock('vm', () => ({
|
||||
createContext: mockCreateContext,
|
||||
Script: mockScript,
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const x = 42;\nreturn undefinedVariable + x;',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toContain('Reference Error')
|
||||
expect(data.error).toContain('Line 2')
|
||||
expect(data.error).toContain('return undefinedVariable + x;')
|
||||
expect(data.error).toContain('undefinedVariable is not defined')
|
||||
})
|
||||
|
||||
it('should handle errors without line content gracefully', async () => {
|
||||
const mockScript = vi.fn().mockImplementation(() => {
|
||||
const error = new Error('Generic error without stack trace')
|
||||
error.name = 'Error'
|
||||
// No stack trace
|
||||
throw error
|
||||
})
|
||||
|
||||
vi.doMock('vm', () => ({
|
||||
createContext: mockCreateContext,
|
||||
Script: mockScript,
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test";',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toBe('Generic error without stack trace')
|
||||
|
||||
// Should still have debug info, but without line details
|
||||
expect(data.debug).toBeDefined()
|
||||
expect(data.debug.errorType).toBe('Error')
|
||||
expect(data.debug.line).toBeUndefined()
|
||||
expect(data.debug.lineContent).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should extract line numbers from different stack trace formats', async () => {
|
||||
const mockScript = vi.fn().mockImplementation(() => {
|
||||
const error = new Error('Test error')
|
||||
error.name = 'Error'
|
||||
error.stack = `Error: Test error
|
||||
at user-function.js:7:25
|
||||
at async function
|
||||
at Script.runInContext (node:vm:147:14)`
|
||||
throw error
|
||||
})
|
||||
|
||||
vi.doMock('vm', () => ({
|
||||
createContext: mockCreateContext,
|
||||
Script: mockScript,
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
|
||||
// Line 7 in VM should map to line 5 in user code (7 - 3 + 1 = 5)
|
||||
expect(data.debug.line).toBe(5)
|
||||
expect(data.debug.column).toBe(25)
|
||||
expect(data.debug.lineContent).toBe('return a + b + c + d;')
|
||||
})
|
||||
|
||||
it('should provide helpful suggestions for common syntax errors', async () => {
|
||||
const mockScript = vi.fn().mockImplementation(() => {
|
||||
const error = new Error('Unexpected end of input')
|
||||
error.name = 'SyntaxError'
|
||||
error.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input'
|
||||
throw error
|
||||
})
|
||||
|
||||
vi.doMock('vm', () => ({
|
||||
createContext: mockCreateContext,
|
||||
Script: mockScript,
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = {\n name: "test"\n// Missing closing brace',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toContain('Syntax Error')
|
||||
expect(data.error).toContain('Unexpected end of input')
|
||||
expect(data.error).toContain('(Check for missing closing brackets or braces)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
it('should properly escape regex special characters', async () => {
|
||||
// This tests the escapeRegExp function indirectly
|
||||
|
||||
@@ -8,6 +8,210 @@ export const maxDuration = 60
|
||||
|
||||
const logger = createLogger('FunctionExecuteAPI')
|
||||
|
||||
/**
|
||||
* Enhanced error information interface
|
||||
*/
|
||||
interface EnhancedError {
|
||||
message: string
|
||||
line?: number
|
||||
column?: number
|
||||
stack?: string
|
||||
name: string
|
||||
originalError: any
|
||||
lineContent?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract enhanced error information from VM execution errors
|
||||
*/
|
||||
function extractEnhancedError(
|
||||
error: any,
|
||||
userCodeStartLine: number,
|
||||
userCode?: string
|
||||
): EnhancedError {
|
||||
const enhanced: EnhancedError = {
|
||||
message: error.message || 'Unknown error',
|
||||
name: error.name || 'Error',
|
||||
originalError: error,
|
||||
}
|
||||
|
||||
if (error.stack) {
|
||||
enhanced.stack = error.stack
|
||||
|
||||
// Parse stack trace to extract line and column information
|
||||
// Handle both compilation errors and runtime errors
|
||||
const stackLines: string[] = error.stack.split('\n')
|
||||
|
||||
for (const line of stackLines) {
|
||||
// Pattern 1: Compilation errors - "user-function.js:6"
|
||||
let match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||
|
||||
// Pattern 2: Runtime errors - "at user-function.js:5:12"
|
||||
if (!match) {
|
||||
match = line.match(/at\s+user-function\.js:(\d+):(\d+)/)
|
||||
}
|
||||
|
||||
// Pattern 3: Generic patterns for any line containing our filename
|
||||
if (!match) {
|
||||
match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||
}
|
||||
|
||||
if (match) {
|
||||
const stackLine = Number.parseInt(match[1], 10)
|
||||
const stackColumn = match[2] ? Number.parseInt(match[2], 10) : undefined
|
||||
|
||||
// Adjust line number to account for wrapper code
|
||||
// The user code starts at a specific line in our wrapper
|
||||
const adjustedLine = stackLine - userCodeStartLine + 1
|
||||
|
||||
// Check if this is a syntax error in wrapper code caused by incomplete user code
|
||||
const isWrapperSyntaxError =
|
||||
stackLine > userCodeStartLine &&
|
||||
error.name === 'SyntaxError' &&
|
||||
(error.message.includes('Unexpected token') ||
|
||||
error.message.includes('Unexpected end of input'))
|
||||
|
||||
if (isWrapperSyntaxError && userCode) {
|
||||
// Map wrapper syntax errors to the last line of user code
|
||||
const codeLines = userCode.split('\n')
|
||||
const lastUserLine = codeLines.length
|
||||
enhanced.line = lastUserLine
|
||||
enhanced.column = codeLines[lastUserLine - 1]?.length || 0
|
||||
enhanced.lineContent = codeLines[lastUserLine - 1]?.trim()
|
||||
break
|
||||
}
|
||||
|
||||
if (adjustedLine > 0) {
|
||||
enhanced.line = adjustedLine
|
||||
enhanced.column = stackColumn
|
||||
|
||||
// Extract the actual line content from user code
|
||||
if (userCode) {
|
||||
const codeLines = userCode.split('\n')
|
||||
if (adjustedLine <= codeLines.length) {
|
||||
enhanced.lineContent = codeLines[adjustedLine - 1]?.trim()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (stackLine <= userCodeStartLine) {
|
||||
// Error is in wrapper code itself
|
||||
enhanced.line = stackLine
|
||||
enhanced.column = stackColumn
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up stack trace to show user-relevant information
|
||||
const cleanedStackLines: string[] = stackLines
|
||||
.filter(
|
||||
(line: string) =>
|
||||
line.includes('user-function.js') ||
|
||||
(!line.includes('vm.js') && !line.includes('internal/'))
|
||||
)
|
||||
.map((line: string) => line.replace(/\s+at\s+/, ' at '))
|
||||
|
||||
if (cleanedStackLines.length > 0) {
|
||||
enhanced.stack = cleanedStackLines.join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// Keep original message without adding error type prefix
|
||||
// The error type will be added later in createUserFriendlyErrorMessage
|
||||
|
||||
return enhanced
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detailed error message for users
|
||||
*/
|
||||
function createUserFriendlyErrorMessage(
|
||||
enhanced: EnhancedError,
|
||||
requestId: string,
|
||||
userCode?: string
|
||||
): string {
|
||||
let errorMessage = enhanced.message
|
||||
|
||||
// Add line and column information if available
|
||||
if (enhanced.line !== undefined) {
|
||||
let lineInfo = `Line ${enhanced.line}${enhanced.column !== undefined ? `:${enhanced.column}` : ''}`
|
||||
|
||||
// Add the actual line content if available
|
||||
if (enhanced.lineContent) {
|
||||
lineInfo += `: \`${enhanced.lineContent}\``
|
||||
}
|
||||
|
||||
errorMessage = `${lineInfo} - ${errorMessage}`
|
||||
} else {
|
||||
// If no line number, try to extract it from stack trace for display
|
||||
if (enhanced.stack) {
|
||||
const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||
if (stackMatch) {
|
||||
const line = Number.parseInt(stackMatch[1], 10)
|
||||
const column = stackMatch[2] ? Number.parseInt(stackMatch[2], 10) : undefined
|
||||
let lineInfo = `Line ${line}${column ? `:${column}` : ''}`
|
||||
|
||||
// Try to get line content if we have userCode
|
||||
if (userCode) {
|
||||
const codeLines = userCode.split('\n')
|
||||
// Note: stackMatch gives us VM line number, need to adjust
|
||||
// This is a fallback case, so we might not have perfect line mapping
|
||||
if (line <= codeLines.length) {
|
||||
const lineContent = codeLines[line - 1]?.trim()
|
||||
if (lineContent) {
|
||||
lineInfo += `: \`${lineContent}\``
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorMessage = `${lineInfo} - ${errorMessage}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add error type prefix with consistent naming
|
||||
if (enhanced.name !== 'Error') {
|
||||
const errorTypePrefix =
|
||||
enhanced.name === 'SyntaxError'
|
||||
? 'Syntax Error'
|
||||
: enhanced.name === 'TypeError'
|
||||
? 'Type Error'
|
||||
: enhanced.name === 'ReferenceError'
|
||||
? 'Reference Error'
|
||||
: enhanced.name
|
||||
|
||||
// Only add prefix if not already present
|
||||
if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) {
|
||||
errorMessage = `${errorTypePrefix}: ${errorMessage}`
|
||||
}
|
||||
}
|
||||
|
||||
// For syntax errors, provide additional context
|
||||
if (enhanced.name === 'SyntaxError') {
|
||||
if (errorMessage.includes('Invalid or unexpected token')) {
|
||||
errorMessage += ' (Check for missing quotes, brackets, or semicolons)'
|
||||
} else if (errorMessage.includes('Unexpected end of input')) {
|
||||
errorMessage += ' (Check for missing closing brackets or braces)'
|
||||
} else if (errorMessage.includes('Unexpected token')) {
|
||||
// Check if this might be due to incomplete code
|
||||
if (
|
||||
enhanced.lineContent &&
|
||||
((enhanced.lineContent.includes('(') && !enhanced.lineContent.includes(')')) ||
|
||||
(enhanced.lineContent.includes('[') && !enhanced.lineContent.includes(']')) ||
|
||||
(enhanced.lineContent.includes('{') && !enhanced.lineContent.includes('}')))
|
||||
) {
|
||||
errorMessage += ' (Check for missing closing parentheses, brackets, or braces)'
|
||||
} else {
|
||||
errorMessage += ' (Check your syntax)'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves environment variables and tags in code
|
||||
* @param code - Code with variables
|
||||
@@ -121,6 +325,8 @@ export async function POST(req: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const startTime = Date.now()
|
||||
let stdout = ''
|
||||
let userCodeStartLine = 3 // Default value for error reporting
|
||||
let resolvedCode = '' // Store resolved code for error reporting
|
||||
|
||||
try {
|
||||
const body = await req.json()
|
||||
@@ -149,13 +355,15 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
|
||||
// Resolve variables in the code with workflow environment variables
|
||||
const { resolvedCode, contextVariables } = resolveCodeVariables(
|
||||
const codeResolution = resolveCodeVariables(
|
||||
code,
|
||||
executionParams,
|
||||
envVars,
|
||||
blockData,
|
||||
blockNameMapping
|
||||
)
|
||||
resolvedCode = codeResolution.resolvedCode
|
||||
const contextVariables = codeResolution.contextVariables
|
||||
|
||||
const executionMethod = 'vm' // Default execution method
|
||||
|
||||
@@ -301,16 +509,12 @@ export async function POST(req: NextRequest) {
|
||||
// timeout,
|
||||
// displayErrors: true,
|
||||
// })
|
||||
// logger.info(`[${requestId}] VM execution result`, {
|
||||
// result,
|
||||
// stdout,
|
||||
// })
|
||||
// }
|
||||
// } else {
|
||||
logger.info(`[${requestId}] Using VM for code execution`, {
|
||||
resolvedCode,
|
||||
executionParams,
|
||||
envVars,
|
||||
hasEnvVars: Object.keys(envVars).length > 0,
|
||||
})
|
||||
|
||||
// Create a secure context with console logging
|
||||
@@ -336,28 +540,40 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
const script = new Script(`
|
||||
(async () => {
|
||||
try {
|
||||
${
|
||||
isCustomTool
|
||||
? `// For custom tools, make parameters directly accessible
|
||||
${Object.keys(executionParams)
|
||||
.map((key) => `const ${key} = params.${key};`)
|
||||
.join('\n ')}`
|
||||
: ''
|
||||
}
|
||||
${resolvedCode}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
})()
|
||||
`)
|
||||
// Calculate line offset for user code to provide accurate error reporting
|
||||
const wrapperLines = ['(async () => {', ' try {']
|
||||
|
||||
// Add custom tool parameter declarations if needed
|
||||
if (isCustomTool) {
|
||||
wrapperLines.push(' // For custom tools, make parameters directly accessible')
|
||||
Object.keys(executionParams).forEach((key) => {
|
||||
wrapperLines.push(` const ${key} = params.${key};`)
|
||||
})
|
||||
}
|
||||
|
||||
userCodeStartLine = wrapperLines.length + 1 // +1 because user code starts on next line
|
||||
|
||||
// Build the complete script with proper formatting for line numbers
|
||||
const fullScript = [
|
||||
...wrapperLines,
|
||||
` ${resolvedCode.split('\n').join('\n ')}`, // Indent user code
|
||||
' } catch (error) {',
|
||||
' console.error(error);',
|
||||
' throw error;',
|
||||
' }',
|
||||
'})()',
|
||||
].join('\n')
|
||||
|
||||
const script = new Script(fullScript, {
|
||||
filename: 'user-function.js', // This filename will appear in stack traces
|
||||
lineOffset: 0, // Start line numbering from 0
|
||||
columnOffset: 0, // Start column numbering from 0
|
||||
})
|
||||
|
||||
const result = await script.runInContext(context, {
|
||||
timeout,
|
||||
displayErrors: true,
|
||||
breakOnSigint: true, // Allow breaking on SIGINT for better debugging
|
||||
})
|
||||
// }
|
||||
|
||||
@@ -384,14 +600,40 @@ export async function POST(req: NextRequest) {
|
||||
executionTime,
|
||||
})
|
||||
|
||||
const enhancedError = extractEnhancedError(error, userCodeStartLine, resolvedCode)
|
||||
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
|
||||
enhancedError,
|
||||
requestId,
|
||||
resolvedCode
|
||||
)
|
||||
|
||||
// Log enhanced error details for debugging
|
||||
logger.error(`[${requestId}] Enhanced error details`, {
|
||||
originalMessage: error.message,
|
||||
enhancedMessage: userFriendlyErrorMessage,
|
||||
line: enhancedError.line,
|
||||
column: enhancedError.column,
|
||||
lineContent: enhancedError.lineContent,
|
||||
errorType: enhancedError.name,
|
||||
userCodeStartLine,
|
||||
})
|
||||
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
error: error.message || 'Code execution failed',
|
||||
error: userFriendlyErrorMessage,
|
||||
output: {
|
||||
result: null,
|
||||
stdout,
|
||||
executionTime,
|
||||
},
|
||||
// Include debug information in development or for debugging
|
||||
debug: {
|
||||
line: enhancedError.line,
|
||||
column: enhancedError.column,
|
||||
errorType: enhancedError.name,
|
||||
lineContent: enhancedError.lineContent,
|
||||
stack: enhancedError.stack,
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json(errorResponse, { status: 500 })
|
||||
|
||||
@@ -17,6 +17,17 @@ describe('Scheduled Workflow Execution API Route', () => {
|
||||
|
||||
mockExecutionDependencies()
|
||||
|
||||
// Mock the normalized tables helper
|
||||
vi.doMock('@/lib/workflows/db-helpers', () => ({
|
||||
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue({
|
||||
blocks: sampleWorkflowState.blocks,
|
||||
edges: sampleWorkflowState.edges || [],
|
||||
loops: sampleWorkflowState.loops || {},
|
||||
parallels: sampleWorkflowState.parallels || {},
|
||||
isFromNormalizedTables: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('croner', () => ({
|
||||
Cron: vi.fn().mockImplementation(() => ({
|
||||
nextRun: vi.fn().mockReturnValue(new Date(Date.now() + 60000)), // Next run in 1 minute
|
||||
|
||||
@@ -14,13 +14,13 @@ import {
|
||||
} from '@/lib/schedules/utils'
|
||||
import { checkServerSideUsageLimits } from '@/lib/usage-monitor'
|
||||
import { decryptSecret } from '@/lib/utils'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import { environment, userStats, workflow, workflowSchedule } from '@/db/schema'
|
||||
import { Executor } from '@/executor'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { mergeSubblockState } from '@/stores/workflows/server-utils'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
// Add dynamic export to prevent caching
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -149,8 +149,27 @@ export async function GET(req: NextRequest) {
|
||||
continue
|
||||
}
|
||||
|
||||
const state = workflowRecord.state as WorkflowState
|
||||
const { blocks, edges, loops, parallels } = state
|
||||
// Load workflow data from normalized tables (no fallback to deprecated state column)
|
||||
logger.debug(
|
||||
`[${requestId}] Loading workflow ${schedule.workflowId} from normalized tables`
|
||||
)
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(schedule.workflowId)
|
||||
|
||||
if (!normalizedData) {
|
||||
logger.error(
|
||||
`[${requestId}] No normalized data found for scheduled workflow ${schedule.workflowId}`
|
||||
)
|
||||
throw new Error(`Workflow data not found in normalized tables for ${schedule.workflowId}`)
|
||||
}
|
||||
|
||||
// Use normalized data only
|
||||
const blocks = normalizedData.blocks
|
||||
const edges = normalizedData.edges
|
||||
const loops = normalizedData.loops
|
||||
const parallels = normalizedData.parallels
|
||||
logger.info(
|
||||
`[${requestId}] Loaded scheduled workflow ${schedule.workflowId} from normalized tables`
|
||||
)
|
||||
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
|
||||
@@ -405,9 +424,13 @@ export async function GET(req: NextRequest) {
|
||||
.limit(1)
|
||||
|
||||
if (workflowRecord) {
|
||||
const state = workflowRecord.state as WorkflowState
|
||||
const { blocks } = state
|
||||
nextRunAt = calculateNextRunTime(schedule, blocks)
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(schedule.workflowId)
|
||||
|
||||
if (!normalizedData) {
|
||||
nextRunAt = new Date(now.getTime() + 24 * 60 * 60 * 1000)
|
||||
} else {
|
||||
nextRunAt = calculateNextRunTime(schedule, normalizedData.blocks)
|
||||
}
|
||||
} else {
|
||||
nextRunAt = new Date(now.getTime() + 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import { NextRequest } from 'next/server'
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createMockRequest,
|
||||
mockExecutionDependencies,
|
||||
sampleWorkflowState,
|
||||
} from '@/app/api/__test-utils__/utils'
|
||||
import { createMockRequest, mockExecutionDependencies } from '@/app/api/__test-utils__/utils'
|
||||
|
||||
// Define mock functions at the top level to be used in mocks
|
||||
const hasProcessedMessageMock = vi.fn().mockResolvedValue(false)
|
||||
@@ -148,10 +144,18 @@ describe('Webhook Trigger API Route', () => {
|
||||
vi.resetAllMocks()
|
||||
vi.clearAllTimers()
|
||||
|
||||
// Mock all dependencies
|
||||
mockExecutionDependencies()
|
||||
|
||||
// Reset mock behaviors to default for each test
|
||||
vi.doMock('@/lib/workflows/db-helpers', () => ({
|
||||
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue({
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isFromNormalizedTables: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
hasProcessedMessageMock.mockResolvedValue(false)
|
||||
markMessageAsProcessedMock.mockResolvedValue(true)
|
||||
acquireLockMock.mockResolvedValue(true)
|
||||
@@ -159,12 +163,10 @@ describe('Webhook Trigger API Route', () => {
|
||||
processGenericDeduplicationMock.mockResolvedValue(null)
|
||||
processWebhookMock.mockResolvedValue(new Response('Webhook processed', { status: 200 }))
|
||||
|
||||
// Restore original crypto.randomUUID if it was mocked
|
||||
if ((global as any).crypto?.randomUUID) {
|
||||
vi.spyOn(crypto, 'randomUUID').mockRestore()
|
||||
}
|
||||
|
||||
// Mock crypto.randomUUID to return predictable values
|
||||
vi.spyOn(crypto, 'randomUUID').mockReturnValue('mock-uuid-12345')
|
||||
})
|
||||
|
||||
@@ -263,7 +265,6 @@ describe('Webhook Trigger API Route', () => {
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
userId: 'user-id',
|
||||
state: sampleWorkflowState,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -355,7 +356,6 @@ describe('Webhook Trigger API Route', () => {
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
userId: 'user-id',
|
||||
state: sampleWorkflowState,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -409,7 +409,6 @@ describe('Webhook Trigger API Route', () => {
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
userId: 'user-id',
|
||||
state: sampleWorkflowState,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -482,7 +481,6 @@ describe('Webhook Trigger API Route', () => {
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
userId: 'user-id',
|
||||
state: sampleWorkflowState,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -553,7 +551,6 @@ describe('Webhook Trigger API Route', () => {
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
userId: 'user-id',
|
||||
state: sampleWorkflowState,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
processWebhook,
|
||||
processWhatsAppDeduplication,
|
||||
} from '@/lib/webhooks/utils'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { db } from '@/db'
|
||||
import { webhook, workflow } from '@/db/schema'
|
||||
|
||||
@@ -187,6 +188,24 @@ export async function POST(
|
||||
foundWebhook = webhooks[0].webhook
|
||||
foundWorkflow = webhooks[0].workflow
|
||||
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(foundWorkflow.id)
|
||||
|
||||
if (!normalizedData) {
|
||||
logger.error(`[${requestId}] No normalized data found for webhook workflow ${foundWorkflow.id}`)
|
||||
return new NextResponse('Workflow data not found in normalized tables', { status: 500 })
|
||||
}
|
||||
|
||||
// Construct state from normalized data only (execution-focused, no frontend state fields)
|
||||
foundWorkflow.state = {
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: foundWorkflow.isDeployed || false,
|
||||
deployedAt: foundWorkflow.deployedAt,
|
||||
}
|
||||
|
||||
// Special handling for Telegram webhooks to work around middleware User-Agent checks
|
||||
if (foundWebhook.provider === 'telegram') {
|
||||
// Log detailed information about the request for debugging
|
||||
|
||||
@@ -31,6 +31,27 @@ describe('Workflow Deployment API Route', () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/workflows/db-helpers', () => ({
|
||||
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue({
|
||||
blocks: {
|
||||
'block-1': {
|
||||
id: 'block-1',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: { x: 100, y: 100 },
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isFromNormalizedTables: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('../../middleware', () => ({
|
||||
validateWorkflowAccess: vi.fn().mockResolvedValue({
|
||||
workflow: {
|
||||
@@ -74,6 +95,7 @@ describe('Workflow Deployment API Route', () => {
|
||||
isDeployed: false,
|
||||
deployedAt: null,
|
||||
userId: 'user-id',
|
||||
deployedState: null,
|
||||
},
|
||||
]),
|
||||
}),
|
||||
@@ -129,7 +151,6 @@ describe('Workflow Deployment API Route', () => {
|
||||
}),
|
||||
}),
|
||||
})
|
||||
// Mock normalized table queries (blocks, edges, subflows)
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([
|
||||
@@ -216,7 +237,6 @@ describe('Workflow Deployment API Route', () => {
|
||||
}),
|
||||
}),
|
||||
})
|
||||
// Mock normalized table queries (blocks, edges, subflows)
|
||||
.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([
|
||||
|
||||
@@ -32,7 +32,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
isDeployed: workflow.isDeployed,
|
||||
deployedAt: workflow.deployedAt,
|
||||
userId: workflow.userId,
|
||||
state: workflow.state,
|
||||
deployedState: workflow.deployedState,
|
||||
})
|
||||
.from(workflow)
|
||||
@@ -93,11 +92,25 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
// Check if the workflow has meaningful changes that would require redeployment
|
||||
let needsRedeployment = false
|
||||
if (workflowData.deployedState) {
|
||||
const { hasWorkflowChanged } = await import('@/lib/workflows/utils')
|
||||
needsRedeployment = hasWorkflowChanged(
|
||||
workflowData.state as any,
|
||||
workflowData.deployedState as any
|
||||
)
|
||||
// Load current state from normalized tables for comparison
|
||||
const { loadWorkflowFromNormalizedTables } = await import('@/lib/workflows/db-helpers')
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(id)
|
||||
|
||||
if (normalizedData) {
|
||||
// Convert normalized data to WorkflowState format for comparison
|
||||
const currentState = {
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
}
|
||||
|
||||
const { hasWorkflowChanged } = await import('@/lib/workflows/utils')
|
||||
needsRedeployment = hasWorkflowChanged(
|
||||
currentState as any,
|
||||
workflowData.deployedState as any
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully retrieved deployment info: ${id}`)
|
||||
@@ -126,11 +139,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
// Get the workflow to find the user
|
||||
// Get the workflow to find the user (removed deprecated state column)
|
||||
const workflowData = await db
|
||||
.select({
|
||||
userId: workflow.userId,
|
||||
state: workflow.state,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, id))
|
||||
|
||||
@@ -24,45 +24,54 @@ describe('Workflow Execution API Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
|
||||
// Mock workflow middleware
|
||||
vi.doMock('@/app/api/workflows/middleware', () => ({
|
||||
validateWorkflowAccess: vi.fn().mockResolvedValue({
|
||||
workflow: {
|
||||
id: 'workflow-id',
|
||||
userId: 'user-id',
|
||||
state: {
|
||||
blocks: {
|
||||
'starter-id': {
|
||||
id: 'starter-id',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: { x: 100, y: 100 },
|
||||
enabled: true,
|
||||
},
|
||||
'agent-id': {
|
||||
id: 'agent-id',
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
position: { x: 300, y: 100 },
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'starter-id',
|
||||
target: 'agent-id',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Reset execute mock to track calls
|
||||
vi.doMock('@/lib/workflows/db-helpers', () => ({
|
||||
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue({
|
||||
blocks: {
|
||||
'starter-id': {
|
||||
id: 'starter-id',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: { x: 100, y: 100 },
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
data: {},
|
||||
},
|
||||
'agent-id': {
|
||||
id: 'agent-id',
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
position: { x: 300, y: 100 },
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'starter-id',
|
||||
target: 'agent-id',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isFromNormalizedTables: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
executeMock = vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
output: {
|
||||
@@ -76,14 +85,12 @@ describe('Workflow Execution API Route', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Mock executor
|
||||
vi.doMock('@/executor', () => ({
|
||||
Executor: vi.fn().mockImplementation(() => ({
|
||||
execute: executeMock,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock environment variables
|
||||
vi.doMock('@/lib/utils', () => ({
|
||||
decryptSecret: vi.fn().mockResolvedValue({
|
||||
decrypted: 'decrypted-secret-value',
|
||||
@@ -92,13 +99,11 @@ describe('Workflow Execution API Route', () => {
|
||||
getRotatingApiKey: vi.fn().mockReturnValue('rotated-api-key'),
|
||||
}))
|
||||
|
||||
// Mock logger
|
||||
vi.doMock('@/lib/logs/execution-logger', () => ({
|
||||
persistExecutionLogs: vi.fn().mockResolvedValue(undefined),
|
||||
persistExecutionError: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
// Mock trace spans
|
||||
vi.doMock('@/lib/logs/trace-spans', () => ({
|
||||
buildTraceSpans: vi.fn().mockReturnValue({
|
||||
traceSpans: [],
|
||||
@@ -106,13 +111,11 @@ describe('Workflow Execution API Route', () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow run counts
|
||||
vi.doMock('@/lib/workflows/utils', () => ({
|
||||
updateWorkflowRunCounts: vi.fn().mockResolvedValue(undefined),
|
||||
workflowHasResponseBlock: vi.fn().mockReturnValue(false),
|
||||
}))
|
||||
|
||||
// Mock database
|
||||
vi.doMock('@/db', () => {
|
||||
const mockDb = {
|
||||
select: vi.fn().mockImplementation(() => ({
|
||||
@@ -140,7 +143,6 @@ describe('Workflow Execution API Route', () => {
|
||||
return { db: mockDb }
|
||||
})
|
||||
|
||||
// Mock Serializer
|
||||
vi.doMock('@/serializer', () => ({
|
||||
Serializer: vi.fn().mockImplementation(() => ({
|
||||
serializeWorkflow: vi.fn().mockReturnValue({
|
||||
@@ -162,49 +164,37 @@ describe('Workflow Execution API Route', () => {
|
||||
* Simulates direct execution with URL-based parameters
|
||||
*/
|
||||
it('should execute workflow with GET request successfully', async () => {
|
||||
// Create a mock request with query parameters
|
||||
const req = createMockRequest('GET')
|
||||
|
||||
// Create params similar to what Next.js would provide
|
||||
const params = Promise.resolve({ id: 'workflow-id' })
|
||||
|
||||
// Import the handler after mocks are set up
|
||||
const { GET } = await import('./route')
|
||||
|
||||
// Call the handler
|
||||
const response = await GET(req, { params })
|
||||
|
||||
// Get the actual status code - in some implementations this might not be 200
|
||||
// Based on the current implementation, validate the response exists
|
||||
expect(response).toBeDefined()
|
||||
|
||||
// Try to parse the response body
|
||||
let data
|
||||
try {
|
||||
data = await response.json()
|
||||
} catch (e) {
|
||||
// If we can't parse JSON, the response may not be what we expect
|
||||
console.error('Response could not be parsed as JSON:', await response.text())
|
||||
throw e
|
||||
}
|
||||
|
||||
// If status is 200, verify success structure
|
||||
if (response.status === 200) {
|
||||
expect(data).toHaveProperty('success', true)
|
||||
expect(data).toHaveProperty('output')
|
||||
expect(data.output).toHaveProperty('response')
|
||||
}
|
||||
|
||||
// Verify middleware was called
|
||||
const validateWorkflowAccess = (await import('@/app/api/workflows/middleware'))
|
||||
.validateWorkflowAccess
|
||||
expect(validateWorkflowAccess).toHaveBeenCalledWith(expect.any(Object), 'workflow-id')
|
||||
|
||||
// Verify executor was initialized
|
||||
const Executor = (await import('@/executor')).Executor
|
||||
expect(Executor).toHaveBeenCalled()
|
||||
|
||||
// Verify execute was called with undefined input (GET requests don't have body)
|
||||
expect(executeMock).toHaveBeenCalledWith('workflow-id')
|
||||
})
|
||||
|
||||
@@ -213,59 +203,45 @@ describe('Workflow Execution API Route', () => {
|
||||
* Simulates execution with a JSON body containing parameters
|
||||
*/
|
||||
it('should execute workflow with POST request successfully', async () => {
|
||||
// Create request body with custom inputs
|
||||
const requestBody = {
|
||||
inputs: {
|
||||
message: 'Test input message',
|
||||
},
|
||||
}
|
||||
|
||||
// Create a mock request with the request body
|
||||
const req = createMockRequest('POST', requestBody)
|
||||
|
||||
// Create params similar to what Next.js would provide
|
||||
const params = Promise.resolve({ id: 'workflow-id' })
|
||||
|
||||
// Import the handler after mocks are set up
|
||||
const { POST } = await import('./route')
|
||||
|
||||
// Call the handler
|
||||
const response = await POST(req, { params })
|
||||
|
||||
// Ensure response exists
|
||||
expect(response).toBeDefined()
|
||||
|
||||
// Try to parse the response body
|
||||
let data
|
||||
try {
|
||||
data = await response.json()
|
||||
} catch (e) {
|
||||
// If we can't parse JSON, the response may not be what we expect
|
||||
console.error('Response could not be parsed as JSON:', await response.text())
|
||||
throw e
|
||||
}
|
||||
|
||||
// If status is 200, verify success structure
|
||||
if (response.status === 200) {
|
||||
expect(data).toHaveProperty('success', true)
|
||||
expect(data).toHaveProperty('output')
|
||||
expect(data.output).toHaveProperty('response')
|
||||
}
|
||||
|
||||
// Verify middleware was called
|
||||
const validateWorkflowAccess = (await import('@/app/api/workflows/middleware'))
|
||||
.validateWorkflowAccess
|
||||
expect(validateWorkflowAccess).toHaveBeenCalledWith(expect.any(Object), 'workflow-id')
|
||||
|
||||
// Verify executor was constructed
|
||||
const Executor = (await import('@/executor')).Executor
|
||||
expect(Executor).toHaveBeenCalled()
|
||||
|
||||
// Verify execute was called with the input body
|
||||
expect(executeMock).toHaveBeenCalledWith('workflow-id')
|
||||
|
||||
// Updated expectations to match actual implementation
|
||||
// The structure should match: serializedWorkflow, processedBlockStates, decryptedEnvVars, processedInput, workflowVariables
|
||||
expect(Executor).toHaveBeenCalledWith(
|
||||
expect.anything(), // serializedWorkflow
|
||||
expect.anything(), // processedBlockStates
|
||||
@@ -282,7 +258,6 @@ describe('Workflow Execution API Route', () => {
|
||||
* Test POST execution with structured input matching the input format
|
||||
*/
|
||||
it('should execute workflow with structured input matching the input format', async () => {
|
||||
// Create structured input matching the expected input format
|
||||
const structuredInput = {
|
||||
firstName: 'John',
|
||||
age: 30,
|
||||
@@ -291,27 +266,20 @@ describe('Workflow Execution API Route', () => {
|
||||
tags: ['test', 'api'],
|
||||
}
|
||||
|
||||
// Create a mock request with the structured input
|
||||
const req = createMockRequest('POST', structuredInput)
|
||||
|
||||
// Create params similar to what Next.js would provide
|
||||
const params = Promise.resolve({ id: 'workflow-id' })
|
||||
|
||||
// Import the handler after mocks are set up
|
||||
const { POST } = await import('./route')
|
||||
|
||||
// Call the handler
|
||||
const response = await POST(req, { params })
|
||||
|
||||
// Ensure response exists and is successful
|
||||
expect(response).toBeDefined()
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
// Parse the response body
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('success', true)
|
||||
|
||||
// Verify the executor was constructed with the structured input - updated to match implementation
|
||||
const Executor = (await import('@/executor')).Executor
|
||||
expect(Executor).toHaveBeenCalledWith(
|
||||
expect.anything(), // serializedWorkflow
|
||||
@@ -478,39 +446,51 @@ describe('Workflow Execution API Route', () => {
|
||||
workflow: {
|
||||
id: 'workflow-with-vars-id',
|
||||
userId: 'user-id',
|
||||
state: {
|
||||
blocks: {
|
||||
'starter-id': {
|
||||
id: 'starter-id',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: { x: 100, y: 100 },
|
||||
enabled: true,
|
||||
},
|
||||
'agent-id': {
|
||||
id: 'agent-id',
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
position: { x: 300, y: 100 },
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'starter-id',
|
||||
target: 'agent-id',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
},
|
||||
variables: workflowVariables,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock normalized tables helper for this specific test
|
||||
vi.doMock('@/lib/workflows/db-helpers', () => ({
|
||||
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue({
|
||||
blocks: {
|
||||
'starter-id': {
|
||||
id: 'starter-id',
|
||||
type: 'starter',
|
||||
name: 'Start',
|
||||
position: { x: 100, y: 100 },
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
data: {},
|
||||
},
|
||||
'agent-id': {
|
||||
id: 'agent-id',
|
||||
type: 'agent',
|
||||
name: 'Agent',
|
||||
position: { x: 300, y: 100 },
|
||||
enabled: true,
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'starter-id',
|
||||
target: 'agent-id',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
},
|
||||
],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isFromNormalizedTables: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Create a constructor mock to capture the arguments
|
||||
const executorConstructorMock = vi.fn().mockImplementation(() => ({
|
||||
execute: vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { persistExecutionError, persistExecutionLogs } from '@/lib/logs/executio
|
||||
import { buildTraceSpans } from '@/lib/logs/trace-spans'
|
||||
import { checkServerSideUsageLimits } from '@/lib/usage-monitor'
|
||||
import { decryptSecret } from '@/lib/utils'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import {
|
||||
createHttpResponseFromBlock,
|
||||
updateWorkflowRunCounts,
|
||||
@@ -94,19 +95,34 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
|
||||
runningExecutions.add(executionKey)
|
||||
logger.info(`[${requestId}] Starting workflow execution: ${workflowId}`)
|
||||
|
||||
// Use the deployed state if available, otherwise fall back to current state
|
||||
const workflowState = workflow.deployedState || workflow.state
|
||||
// Load workflow data from normalized tables
|
||||
logger.debug(`[${requestId}] Loading workflow ${workflowId} from normalized tables`)
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
|
||||
if (!workflow.deployedState) {
|
||||
logger.warn(
|
||||
`[${requestId}] No deployed state found for workflow: ${workflowId}, using current state`
|
||||
)
|
||||
let blocks: Record<string, any>
|
||||
let edges: any[]
|
||||
let loops: Record<string, any>
|
||||
let parallels: Record<string, any>
|
||||
|
||||
if (normalizedData) {
|
||||
// Use normalized data as primary source
|
||||
;({ blocks, edges, loops, parallels } = normalizedData)
|
||||
logger.info(`[${requestId}] Using normalized tables for workflow execution: ${workflowId}`)
|
||||
} else {
|
||||
logger.info(`[${requestId}] Using deployed state for workflow execution: ${workflowId}`)
|
||||
}
|
||||
// Fallback to deployed state if available (for legacy workflows)
|
||||
logger.warn(
|
||||
`[${requestId}] No normalized data found, falling back to deployed state for workflow: ${workflowId}`
|
||||
)
|
||||
|
||||
const state = workflowState as WorkflowState
|
||||
const { blocks, edges, loops, parallels } = state
|
||||
if (!workflow.deployedState) {
|
||||
throw new Error(
|
||||
`Workflow ${workflowId} has no deployed state and no normalized data available`
|
||||
)
|
||||
}
|
||||
|
||||
const deployedState = workflow.deployedState as WorkflowState
|
||||
;({ blocks, edges, loops, parallels } = deployedState)
|
||||
}
|
||||
|
||||
// Use the same execution flow as in scheduled executions
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
|
||||
@@ -6,13 +6,13 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
name: 'Knowledge',
|
||||
description: 'Use vector search',
|
||||
longDescription:
|
||||
'Perform semantic vector search across one or more knowledge bases or upload new chunks to documents. Uses advanced AI embeddings to understand meaning and context for search operations.',
|
||||
'Perform semantic vector search across knowledge bases, upload individual chunks to existing documents, or create new documents from text content. Uses advanced AI embeddings to understand meaning and context for search operations.',
|
||||
bgColor: '#00B0B0',
|
||||
icon: PackageSearchIcon,
|
||||
category: 'blocks',
|
||||
docsLink: 'https://docs.simstudio.ai/blocks/knowledge',
|
||||
tools: {
|
||||
access: ['knowledge_search', 'knowledge_upload_chunk'],
|
||||
access: ['knowledge_search', 'knowledge_upload_chunk', 'knowledge_create_document'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
@@ -20,6 +20,8 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
return 'knowledge_search'
|
||||
case 'upload_chunk':
|
||||
return 'knowledge_upload_chunk'
|
||||
case 'create_document':
|
||||
return 'knowledge_create_document'
|
||||
default:
|
||||
return 'knowledge_search'
|
||||
}
|
||||
@@ -53,6 +55,7 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
options: [
|
||||
{ label: 'Search', id: 'search' },
|
||||
{ label: 'Upload Chunk', id: 'upload_chunk' },
|
||||
{ label: 'Create Document', id: 'create_document' },
|
||||
],
|
||||
value: () => 'search',
|
||||
},
|
||||
@@ -72,7 +75,7 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
layout: 'full',
|
||||
placeholder: 'Select knowledge base',
|
||||
multiSelect: false,
|
||||
condition: { field: 'operation', value: 'upload_chunk' },
|
||||
condition: { field: 'operation', value: ['upload_chunk', 'create_document'] },
|
||||
},
|
||||
{
|
||||
id: 'query',
|
||||
@@ -107,5 +110,22 @@ export const KnowledgeBlock: BlockConfig = {
|
||||
rows: 6,
|
||||
condition: { field: 'operation', value: 'upload_chunk' },
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
title: 'Document Name',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter document name',
|
||||
condition: { field: 'operation', value: ['create_document'] },
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
title: 'Document Content',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter the document content',
|
||||
rows: 6,
|
||||
condition: { field: 'operation', value: ['create_document'] },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ export const workflow = pgTable('workflow', {
|
||||
folderId: text('folder_id').references(() => workflowFolder.id, { onDelete: 'set null' }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
// DEPRECATED: Use normalized tables (workflow_blocks, workflow_edges, workflow_subflows) instead
|
||||
state: json('state').notNull(),
|
||||
color: text('color').notNull().default('#3972F6'),
|
||||
lastSynced: timestamp('last_synced').notNull(),
|
||||
|
||||
@@ -145,7 +145,7 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
|
||||
logger.info(`Loaded child workflow: ${workflowData.name} (${workflowId})`)
|
||||
|
||||
// Extract the workflow state
|
||||
// Extract the workflow state (API returns normalized data in state field)
|
||||
const workflowState = workflowData.state
|
||||
|
||||
if (!workflowState || !workflowState.blocks) {
|
||||
@@ -153,7 +153,7 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
return null
|
||||
}
|
||||
|
||||
// Use blocks directly since DB format should match UI format
|
||||
// Use blocks directly since API returns data from normalized tables
|
||||
const serializedWorkflow = this.serializer.serializeWorkflow(
|
||||
workflowState.blocks,
|
||||
workflowState.edges || [],
|
||||
|
||||
@@ -339,8 +339,36 @@ async function parseWithFileParser(
|
||||
try {
|
||||
let content: string
|
||||
|
||||
if (fileUrl.startsWith('http://') || fileUrl.startsWith('https://')) {
|
||||
// Download and parse remote file with timeout
|
||||
if (fileUrl.startsWith('data:')) {
|
||||
logger.info(`Processing data URI for: ${filename}`)
|
||||
|
||||
try {
|
||||
const [header, base64Data] = fileUrl.split(',')
|
||||
if (!base64Data) {
|
||||
throw new Error('Invalid data URI format')
|
||||
}
|
||||
|
||||
if (header.includes('base64')) {
|
||||
const buffer = Buffer.from(base64Data, 'base64')
|
||||
content = buffer.toString('utf8')
|
||||
} else {
|
||||
content = decodeURIComponent(base64Data)
|
||||
}
|
||||
|
||||
if (mimeType === 'text/plain') {
|
||||
logger.info(`Data URI processed successfully for text content: ${filename}`)
|
||||
} else {
|
||||
const extension = filename.split('.').pop()?.toLowerCase() || 'txt'
|
||||
const buffer = Buffer.from(base64Data, 'base64')
|
||||
const result = await parseBuffer(buffer, extension)
|
||||
content = result.content
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to process data URI: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
} else if (fileUrl.startsWith('http://') || fileUrl.startsWith('https://')) {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.FILE_DOWNLOAD)
|
||||
|
||||
@@ -354,7 +382,6 @@ async function parseWithFileParser(
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer())
|
||||
|
||||
// Extract file extension from filename
|
||||
const extension = filename.split('.').pop()?.toLowerCase() || ''
|
||||
if (!extension) {
|
||||
throw new Error(`Could not determine file extension from filename: ${filename}`)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { persistExecutionError, persistExecutionLogs } from '@/lib/logs/executio
|
||||
import { buildTraceSpans } from '@/lib/logs/trace-spans'
|
||||
import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis'
|
||||
import { decryptSecret } from '@/lib/utils'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
|
||||
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
|
||||
import { db } from '@/db'
|
||||
@@ -13,7 +14,6 @@ import { environment, userStats, webhook } from '@/db/schema'
|
||||
import { Executor } from '@/executor'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { mergeSubblockStateAsync } from '@/stores/workflows/server-utils'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('WebhookUtils')
|
||||
|
||||
@@ -475,23 +475,28 @@ export async function executeWorkflowFromPayload(
|
||||
|
||||
// Returns void as errors are handled internally
|
||||
try {
|
||||
// Get the workflow state
|
||||
if (!foundWorkflow.state) {
|
||||
logger.error(`[${requestId}] TRACE: Missing workflow state`, {
|
||||
// Load workflow data from normalized tables
|
||||
logger.debug(`[${requestId}] Loading workflow ${foundWorkflow.id} from normalized tables`)
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(foundWorkflow.id)
|
||||
|
||||
if (!normalizedData) {
|
||||
logger.error(`[${requestId}] TRACE: No normalized data found for workflow`, {
|
||||
workflowId: foundWorkflow.id,
|
||||
hasState: false,
|
||||
hasNormalizedData: false,
|
||||
})
|
||||
throw new Error(`Workflow ${foundWorkflow.id} has no state`)
|
||||
throw new Error(`Workflow ${foundWorkflow.id} data not found in normalized tables`)
|
||||
}
|
||||
const state = foundWorkflow.state as WorkflowState
|
||||
const { blocks, edges, loops, parallels } = state
|
||||
|
||||
// Use normalized data for execution
|
||||
const { blocks, edges, loops, parallels } = normalizedData
|
||||
logger.info(`[${requestId}] Loaded workflow ${foundWorkflow.id} from normalized tables`)
|
||||
|
||||
// DEBUG: Log state information
|
||||
logger.debug(`[${requestId}] TRACE: Retrieved workflow state`, {
|
||||
logger.debug(`[${requestId}] TRACE: Retrieved workflow state from normalized tables`, {
|
||||
workflowId: foundWorkflow.id,
|
||||
blockCount: Object.keys(blocks || {}).length,
|
||||
edgeCount: (edges || []).length,
|
||||
loopCount: (loops || []).length,
|
||||
loopCount: Object.keys(loops || {}).length,
|
||||
})
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -122,6 +122,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-06-17',
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
@@ -134,6 +135,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-06-17',
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
@@ -146,6 +148,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-06-17',
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -110,6 +110,9 @@ describe('Model Capabilities', () => {
|
||||
it.concurrent('should return true for models that support temperature', () => {
|
||||
const supportedModels = [
|
||||
'gpt-4o',
|
||||
'gpt-4.1',
|
||||
'gpt-4.1-mini',
|
||||
'gpt-4.1-nano',
|
||||
'gemini-2.5-flash',
|
||||
'claude-sonnet-4-0',
|
||||
'claude-opus-4-0',
|
||||
@@ -139,10 +142,6 @@ describe('Model Capabilities', () => {
|
||||
'deepseek-r1',
|
||||
// Chat models that don't support temperature
|
||||
'deepseek-chat',
|
||||
// GPT-4.1 family models that don't support temperature
|
||||
'gpt-4.1',
|
||||
'gpt-4.1-nano',
|
||||
'gpt-4.1-mini',
|
||||
'azure/gpt-4.1',
|
||||
'azure/model-router',
|
||||
]
|
||||
|
||||
@@ -432,7 +432,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
let workflowState: any
|
||||
|
||||
if (workflowData?.state) {
|
||||
// Use the state from the database
|
||||
// API returns normalized data in state
|
||||
workflowState = {
|
||||
blocks: workflowData.state.blocks || {},
|
||||
edges: workflowData.state.edges || [],
|
||||
@@ -448,9 +448,18 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: workflowData.state,
|
||||
state: {
|
||||
blocks: workflowData.state.blocks || {},
|
||||
edges: workflowData.state.edges || [],
|
||||
loops: workflowData.state.loops || {},
|
||||
parallels: workflowData.state.parallels || {},
|
||||
isDeployed: workflowData.isDeployed || false,
|
||||
deployedAt: workflowData.deployedAt
|
||||
? new Date(workflowData.deployedAt)
|
||||
: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Loaded from database',
|
||||
action: 'Loaded from database (normalized tables)',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
|
||||
@@ -164,6 +164,197 @@ describe('Function Execute Tool', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Enhanced Error Handling', () => {
|
||||
test('should handle enhanced syntax error with line content', async () => {
|
||||
// Setup enhanced error response with debug information
|
||||
tester.setup(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
'Syntax Error: Line 3: `description: "This has a missing closing quote` - Invalid or unexpected token (Check for missing quotes, brackets, or semicolons)',
|
||||
output: {
|
||||
result: null,
|
||||
stdout: '',
|
||||
executionTime: 5,
|
||||
},
|
||||
debug: {
|
||||
line: 3,
|
||||
column: undefined,
|
||||
errorType: 'SyntaxError',
|
||||
lineContent: 'description: "This has a missing closing quote',
|
||||
stack: 'user-function.js:5\n description: "This has a missing closing quote\n...',
|
||||
},
|
||||
},
|
||||
{ ok: false, status: 500 }
|
||||
)
|
||||
|
||||
// Execute the tool with syntax error
|
||||
const result = await tester.execute({
|
||||
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
|
||||
})
|
||||
|
||||
// Check enhanced error handling
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('Syntax Error')
|
||||
expect(result.error).toContain('Line 3')
|
||||
expect(result.error).toContain('description: "This has a missing closing quote')
|
||||
expect(result.error).toContain('Invalid or unexpected token')
|
||||
expect(result.error).toContain('(Check for missing quotes, brackets, or semicolons)')
|
||||
})
|
||||
|
||||
test('should handle enhanced runtime error with line and column', async () => {
|
||||
// Setup enhanced runtime error response
|
||||
tester.setup(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"Type Error: Line 2:16: `return obj.someMethod();` - Cannot read properties of null (reading 'someMethod')",
|
||||
output: {
|
||||
result: null,
|
||||
stdout: 'ERROR: {}\n',
|
||||
executionTime: 12,
|
||||
},
|
||||
debug: {
|
||||
line: 2,
|
||||
column: 16,
|
||||
errorType: 'TypeError',
|
||||
lineContent: 'return obj.someMethod();',
|
||||
stack: 'TypeError: Cannot read properties of null...',
|
||||
},
|
||||
},
|
||||
{ ok: false, status: 500 }
|
||||
)
|
||||
|
||||
// Execute the tool with runtime error
|
||||
const result = await tester.execute({
|
||||
code: 'const obj = null;\nreturn obj.someMethod();',
|
||||
})
|
||||
|
||||
// Check enhanced error handling
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('Type Error')
|
||||
expect(result.error).toContain('Line 2:16')
|
||||
expect(result.error).toContain('return obj.someMethod();')
|
||||
expect(result.error).toContain('Cannot read properties of null')
|
||||
})
|
||||
|
||||
test('should handle enhanced error information in tool response', async () => {
|
||||
// Setup enhanced error response with full debug info
|
||||
tester.setup(
|
||||
{
|
||||
success: false,
|
||||
error: 'Reference Error: Line 1: `return undefinedVar` - undefinedVar is not defined',
|
||||
output: {
|
||||
result: null,
|
||||
stdout: '',
|
||||
executionTime: 3,
|
||||
},
|
||||
debug: {
|
||||
line: 1,
|
||||
column: 7,
|
||||
errorType: 'ReferenceError',
|
||||
lineContent: 'return undefinedVar',
|
||||
stack: 'ReferenceError: undefinedVar is not defined...',
|
||||
},
|
||||
},
|
||||
{ ok: false, status: 500 }
|
||||
)
|
||||
|
||||
// Execute the tool with reference error
|
||||
const result = await tester.execute({
|
||||
code: 'return undefinedVar',
|
||||
})
|
||||
|
||||
// Check that the tool properly captures enhanced error
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe(
|
||||
'Reference Error: Line 1: `return undefinedVar` - undefinedVar is not defined'
|
||||
)
|
||||
})
|
||||
|
||||
test('should preserve debug information in error object', async () => {
|
||||
// Setup enhanced error response
|
||||
tester.setup(
|
||||
{
|
||||
success: false,
|
||||
error: 'Syntax Error: Line 2 - Invalid syntax',
|
||||
debug: {
|
||||
line: 2,
|
||||
column: 5,
|
||||
errorType: 'SyntaxError',
|
||||
lineContent: 'invalid syntax here',
|
||||
stack: 'SyntaxError: Invalid syntax...',
|
||||
},
|
||||
},
|
||||
{ ok: false, status: 500 }
|
||||
)
|
||||
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
code: 'valid line\ninvalid syntax here',
|
||||
})
|
||||
|
||||
// Check that enhanced error information is available
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe('Syntax Error: Line 2 - Invalid syntax')
|
||||
|
||||
// Note: In this test framework, debug information would be available
|
||||
// in the response object, but the tool transforms it into the error message
|
||||
})
|
||||
|
||||
test('should handle enhanced error without line information', async () => {
|
||||
// Setup error response without line information
|
||||
tester.setup(
|
||||
{
|
||||
success: false,
|
||||
error: 'Generic error message',
|
||||
debug: {
|
||||
errorType: 'Error',
|
||||
stack: 'Error: Generic error message...',
|
||||
},
|
||||
},
|
||||
{ ok: false, status: 500 }
|
||||
)
|
||||
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
code: 'return "test";',
|
||||
})
|
||||
|
||||
// Check error handling without enhanced line info
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe('Generic error message')
|
||||
})
|
||||
|
||||
test('should provide line-specific error message when available', async () => {
|
||||
// Setup enhanced error response with line info
|
||||
tester.setup(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
'Type Error: Line 5:20: `obj.nonExistentMethod()` - obj.nonExistentMethod is not a function',
|
||||
debug: {
|
||||
line: 5,
|
||||
column: 20,
|
||||
errorType: 'TypeError',
|
||||
lineContent: 'obj.nonExistentMethod()',
|
||||
},
|
||||
},
|
||||
{ ok: false, status: 500 }
|
||||
)
|
||||
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
code: 'const obj = {};\nobj.nonExistentMethod();',
|
||||
})
|
||||
|
||||
// Check that enhanced error message is provided
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('Line 5:20')
|
||||
expect(result.error).toContain('obj.nonExistentMethod()')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('should handle empty code input', async () => {
|
||||
// Execute with empty code - this should still pass through to the API
|
||||
|
||||
@@ -70,7 +70,21 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.error || 'Code execution failed')
|
||||
// Create enhanced error with debug information if available
|
||||
const error = new Error(result.error || 'Code execution failed')
|
||||
|
||||
// Add debug information to the error object if available
|
||||
if (result.debug) {
|
||||
Object.assign(error, {
|
||||
line: result.debug.line,
|
||||
column: result.debug.column,
|
||||
errorType: result.debug.errorType,
|
||||
stack: result.debug.stack,
|
||||
enhancedError: true,
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -83,6 +97,10 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
// If we have enhanced error information, create a more detailed message
|
||||
if (error.enhancedError && error.line) {
|
||||
return `Line ${error.line}${error.column ? `:${error.column}` : ''} - ${error.message}`
|
||||
}
|
||||
return error.message || 'Code execution failed'
|
||||
},
|
||||
}
|
||||
|
||||
173
apps/sim/tools/knowledge/create_document.ts
Normal file
173
apps/sim/tools/knowledge/create_document.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import type { ToolConfig } from '../types'
|
||||
import type { KnowledgeCreateDocumentResponse } from './types'
|
||||
|
||||
export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumentResponse> = {
|
||||
id: 'knowledge_create_document',
|
||||
name: 'Knowledge Create Document',
|
||||
description: 'Create a new document in a knowledge base',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'ID of the knowledge base containing the document',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Name of the document',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Content of the document',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const textContent = params.content?.trim()
|
||||
const documentName = params.name?.trim()
|
||||
|
||||
if (!documentName || documentName.length === 0) {
|
||||
throw new Error('Document name is required')
|
||||
}
|
||||
if (documentName.length > 255) {
|
||||
throw new Error('Document name must be 255 characters or less')
|
||||
}
|
||||
if (/[<>:"/\\|?*]/.test(documentName)) {
|
||||
throw new Error('Document name contains invalid characters. Avoid: < > : " / \\ | ? *')
|
||||
}
|
||||
if (!textContent || textContent.length < 10) {
|
||||
throw new Error('Document content must be at least 10 characters long')
|
||||
}
|
||||
if (textContent.length > 1000000) {
|
||||
throw new Error('Document content exceeds maximum size of 1MB')
|
||||
}
|
||||
|
||||
const contentBytes = new TextEncoder().encode(textContent).length
|
||||
|
||||
const utf8Bytes = new TextEncoder().encode(textContent)
|
||||
const base64Content =
|
||||
typeof Buffer !== 'undefined'
|
||||
? Buffer.from(textContent, 'utf8').toString('base64')
|
||||
: btoa(String.fromCharCode(...utf8Bytes))
|
||||
|
||||
const dataUri = `data:text/plain;base64,${base64Content}`
|
||||
|
||||
const documents = [
|
||||
{
|
||||
filename: documentName.endsWith('.txt') ? documentName : `${documentName}.txt`,
|
||||
fileUrl: dataUri,
|
||||
fileSize: contentBytes,
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
documents: documents,
|
||||
processingOptions: {
|
||||
chunkSize: 1024,
|
||||
minCharactersPerChunk: 100,
|
||||
chunkOverlap: 200,
|
||||
recipe: 'default',
|
||||
lang: 'en',
|
||||
},
|
||||
bulk: true,
|
||||
}
|
||||
},
|
||||
isInternalRoute: true,
|
||||
},
|
||||
transformResponse: async (response): Promise<KnowledgeCreateDocumentResponse> => {
|
||||
try {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = result.error?.message || result.message || 'Failed to create document'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = result.data || result
|
||||
const documentsCreated = data.documentsCreated || []
|
||||
|
||||
// Handle multiple documents response
|
||||
const uploadCount = documentsCreated.length
|
||||
const firstDocument = documentsCreated[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: {
|
||||
id: firstDocument?.documentId || firstDocument?.id || '',
|
||||
name:
|
||||
uploadCount > 1 ? `${uploadCount} documents` : firstDocument?.filename || 'Unknown',
|
||||
type: 'document',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
enabled: true,
|
||||
},
|
||||
message:
|
||||
uploadCount > 1
|
||||
? `Successfully created ${uploadCount} documents in knowledge base`
|
||||
: `Successfully created document in knowledge base`,
|
||||
documentId: firstDocument?.documentId || firstDocument?.id || '',
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
data: {
|
||||
id: '',
|
||||
name: '',
|
||||
type: '',
|
||||
enabled: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
message: `Failed to create document: ${error.message || 'Unknown error'}`,
|
||||
documentId: '',
|
||||
},
|
||||
error: `Failed to create document: ${error.message || 'Unknown error'}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
transformError: async (error): Promise<KnowledgeCreateDocumentResponse> => {
|
||||
let errorMessage = 'Failed to create document'
|
||||
|
||||
if (error.message) {
|
||||
if (error.message.includes('Document name')) {
|
||||
errorMessage = `Document name error: ${error.message}`
|
||||
} else if (error.message.includes('Document content')) {
|
||||
errorMessage = `Document content error: ${error.message}`
|
||||
} else if (error.message.includes('invalid characters')) {
|
||||
errorMessage = `${error.message}. Please use a valid filename.`
|
||||
} else if (error.message.includes('maximum size')) {
|
||||
errorMessage = `${error.message}. Consider breaking large content into smaller documents.`
|
||||
} else {
|
||||
errorMessage = `Failed to create document: ${error.message}`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
data: {
|
||||
id: '',
|
||||
name: '',
|
||||
type: '',
|
||||
enabled: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
message: errorMessage,
|
||||
documentId: '',
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { knowledgeCreateDocumentTool } from './create_document'
|
||||
import { knowledgeSearchTool } from './search'
|
||||
import { knowledgeUploadChunkTool } from './upload_chunk'
|
||||
|
||||
export { knowledgeSearchTool, knowledgeUploadChunkTool }
|
||||
export { knowledgeSearchTool, knowledgeUploadChunkTool, knowledgeCreateDocumentTool }
|
||||
|
||||
@@ -49,3 +49,22 @@ export interface KnowledgeUploadChunkParams {
|
||||
content: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface KnowledgeCreateDocumentResult {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
enabled: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface KnowledgeCreateDocumentResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
data: KnowledgeCreateDocumentResult
|
||||
message: string
|
||||
documentId: string
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ import { contactsTool as hubspotContacts } from './hubspot/contacts'
|
||||
import { huggingfaceChatTool } from './huggingface'
|
||||
import { readUrlTool } from './jina'
|
||||
import { jiraBulkRetrieveTool, jiraRetrieveTool, jiraUpdateTool, jiraWriteTool } from './jira'
|
||||
import { knowledgeSearchTool, knowledgeUploadChunkTool } from './knowledge'
|
||||
import {
|
||||
knowledgeCreateDocumentTool,
|
||||
knowledgeSearchTool,
|
||||
knowledgeUploadChunkTool,
|
||||
} from './knowledge'
|
||||
import { linearCreateIssueTool, linearReadIssuesTool } from './linear'
|
||||
import { linkupSearchTool } from './linkup'
|
||||
import { mem0AddMemoriesTool, mem0GetMemoriesTool, mem0SearchMemoriesTool } from './mem0'
|
||||
@@ -191,6 +195,7 @@ export const tools: Record<string, ToolConfig> = {
|
||||
memory_delete: memoryDeleteTool,
|
||||
knowledge_search: knowledgeSearchTool,
|
||||
knowledge_upload_chunk: knowledgeUploadChunkTool,
|
||||
knowledge_create_document: knowledgeCreateDocumentTool,
|
||||
elevenlabs_tts: elevenLabsTtsTool,
|
||||
s3_get_object: s3GetObjectTool,
|
||||
telegram_message: telegramMessageTool,
|
||||
|
||||
Reference in New Issue
Block a user