mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 15:38:00 -05:00
Compare commits
1 Commits
fix/google
...
improvemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b8b67179a |
@@ -15,6 +15,7 @@ export const ResponseBlock: BlockConfig<ResponseBlockOutput> = {
|
||||
- This is usually used as the last block in the workflow.
|
||||
`,
|
||||
category: 'blocks',
|
||||
hideFromToolbar: true,
|
||||
bgColor: '#2F55FF',
|
||||
icon: ResponseIcon,
|
||||
subBlocks: [
|
||||
|
||||
121
apps/sim/blocks/blocks/workflow_response.ts
Normal file
121
apps/sim/blocks/blocks/workflow_response.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ResponseIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
|
||||
/**
|
||||
* WorkflowResponseBlock - A simplified response block with flat output structure.
|
||||
* Output is directly accessible via <Response.fieldName> instead of nested paths.
|
||||
*/
|
||||
export const WorkflowResponseBlock: BlockConfig = {
|
||||
type: 'workflow_response',
|
||||
name: 'Response',
|
||||
description: 'Send structured API response',
|
||||
longDescription:
|
||||
'A Response block with direct field access. Use <Response.fieldName> to reference output fields directly.',
|
||||
docsLink: 'https://docs.sim.ai/blocks/response',
|
||||
bestPractices: `
|
||||
- Only use this if the trigger block is the API Trigger.
|
||||
- Prefer the builder mode over the editor mode.
|
||||
- This is usually used as the last block in the workflow.
|
||||
- Output fields are directly accessible: <Response.fieldName>
|
||||
`,
|
||||
category: 'blocks',
|
||||
bgColor: '#2F55FF',
|
||||
icon: ResponseIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'dataMode',
|
||||
title: 'Response Data Mode',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Builder', id: 'structured' },
|
||||
{ label: 'Editor', id: 'json' },
|
||||
],
|
||||
value: () => 'structured',
|
||||
description: 'Choose how to define your response data structure',
|
||||
},
|
||||
{
|
||||
id: 'builderData',
|
||||
title: 'Response Structure',
|
||||
type: 'response-format',
|
||||
condition: { field: 'dataMode', value: 'structured' },
|
||||
description:
|
||||
'Define the structure of your response data. Use <variable.name> in field names to reference workflow variables.',
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
title: 'Response Data',
|
||||
type: 'code',
|
||||
placeholder: '{\n "message": "Hello world",\n "userId": "<variable.userId>"\n}',
|
||||
language: 'json',
|
||||
condition: { field: 'dataMode', value: 'json' },
|
||||
description:
|
||||
'Data that will be sent as the response body on API calls. Use <variable.name> to reference workflow variables.',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert JSON programmer.
|
||||
Generate ONLY the raw JSON object based on the user's request.
|
||||
The output MUST be a single, valid JSON object, starting with { and ending with }.
|
||||
|
||||
Current response: {context}
|
||||
|
||||
Do not include any explanations, markdown formatting, or other text outside the JSON object.
|
||||
|
||||
You have access to the following variables you can use to generate the JSON body:
|
||||
- 'params' (object): Contains input parameters derived from the JSON schema. Access these directly using the parameter name wrapped in angle brackets, e.g., '<paramName>'. Do NOT use 'params.paramName'.
|
||||
- 'environmentVariables' (object): Contains environment variables. Reference these using the double curly brace syntax: '{{ENV_VAR_NAME}}'. Do NOT use 'environmentVariables.VAR_NAME' or env.
|
||||
|
||||
Example:
|
||||
{
|
||||
"name": "<block.agent.response.content>",
|
||||
"age": <block.function.output.age>,
|
||||
"success": true
|
||||
}`,
|
||||
placeholder: 'Describe the API response structure you need...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Status Code',
|
||||
type: 'short-input',
|
||||
placeholder: '200',
|
||||
description: 'HTTP status code (default: 200)',
|
||||
},
|
||||
{
|
||||
id: 'headers',
|
||||
title: 'Response Headers',
|
||||
type: 'table',
|
||||
columns: ['Key', 'Value'],
|
||||
description: 'Additional HTTP headers to include in the response',
|
||||
},
|
||||
],
|
||||
tools: { access: [] },
|
||||
inputs: {
|
||||
dataMode: {
|
||||
type: 'string',
|
||||
description: 'Response data definition mode',
|
||||
},
|
||||
builderData: {
|
||||
type: 'json',
|
||||
description: 'Structured response data',
|
||||
},
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'JSON response body',
|
||||
},
|
||||
status: {
|
||||
type: 'number',
|
||||
description: 'HTTP status code',
|
||||
},
|
||||
headers: {
|
||||
type: 'json',
|
||||
description: 'Response headers',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
// User's data fields are spread directly at root level
|
||||
status: { type: 'number', description: 'HTTP status code' },
|
||||
headers: { type: 'json', description: 'Response headers' },
|
||||
},
|
||||
}
|
||||
@@ -131,6 +131,7 @@ import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
|
||||
import { WordPressBlock } from '@/blocks/blocks/wordpress'
|
||||
import { WorkflowBlock } from '@/blocks/blocks/workflow'
|
||||
import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input'
|
||||
import { WorkflowResponseBlock } from '@/blocks/blocks/workflow_response'
|
||||
import { XBlock } from '@/blocks/blocks/x'
|
||||
import { YouTubeBlock } from '@/blocks/blocks/youtube'
|
||||
import { ZendeskBlock } from '@/blocks/blocks/zendesk'
|
||||
@@ -231,6 +232,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
reddit: RedditBlock,
|
||||
resend: ResendBlock,
|
||||
response: ResponseBlock,
|
||||
workflow_response: WorkflowResponseBlock,
|
||||
rss: RssBlock,
|
||||
router: RouterBlock,
|
||||
s3: S3Block,
|
||||
|
||||
@@ -15,6 +15,7 @@ export enum BlockType {
|
||||
VARIABLES = 'variables',
|
||||
|
||||
RESPONSE = 'response',
|
||||
WORKFLOW_RESPONSE = 'workflow_response',
|
||||
HUMAN_IN_THE_LOOP = 'human_in_the_loop',
|
||||
WORKFLOW = 'workflow',
|
||||
WORKFLOW_INPUT = 'workflow_input',
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler
|
||||
import { VariablesBlockHandler } from '@/executor/handlers/variables/variables-handler'
|
||||
import { WaitBlockHandler } from '@/executor/handlers/wait/wait-handler'
|
||||
import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler'
|
||||
import { WorkflowResponseBlockHandler } from '@/executor/handlers/workflow-response/workflow-response-handler'
|
||||
|
||||
export {
|
||||
AgentBlockHandler,
|
||||
@@ -19,6 +20,7 @@ export {
|
||||
EvaluatorBlockHandler,
|
||||
FunctionBlockHandler,
|
||||
GenericBlockHandler,
|
||||
WorkflowResponseBlockHandler,
|
||||
ResponseBlockHandler,
|
||||
HumanInTheLoopBlockHandler,
|
||||
RouterBlockHandler,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler
|
||||
import { VariablesBlockHandler } from '@/executor/handlers/variables/variables-handler'
|
||||
import { WaitBlockHandler } from '@/executor/handlers/wait/wait-handler'
|
||||
import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler'
|
||||
import { WorkflowResponseBlockHandler } from '@/executor/handlers/workflow-response/workflow-response-handler'
|
||||
import type { BlockHandler } from '@/executor/types'
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,7 @@ export function createBlockHandlers(): BlockHandler[] {
|
||||
new ConditionBlockHandler(),
|
||||
new RouterBlockHandler(),
|
||||
new ResponseBlockHandler(),
|
||||
new WorkflowResponseBlockHandler(),
|
||||
new HumanInTheLoopBlockHandler(),
|
||||
new AgentBlockHandler(),
|
||||
new VariablesBlockHandler(),
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType, HTTP } from '@/executor/constants'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
const logger = createLogger('WorkflowResponseBlockHandler')
|
||||
|
||||
interface JSONProperty {
|
||||
id: string
|
||||
name: string
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files'
|
||||
value: any
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the WorkflowResponse block.
|
||||
* Returns a flat output structure where data fields are directly accessible.
|
||||
* Reference: <Response.fieldName> directly (no wrapper like old Response block)
|
||||
* API output is exactly what user defines - no metadata, no wrappers.
|
||||
*/
|
||||
export class WorkflowResponseBlockHandler implements BlockHandler {
|
||||
canHandle(block: SerializedBlock): boolean {
|
||||
return block.metadata?.id === BlockType.WORKFLOW_RESPONSE
|
||||
}
|
||||
|
||||
async execute(
|
||||
ctx: ExecutionContext,
|
||||
block: SerializedBlock,
|
||||
inputs: Record<string, any>
|
||||
): Promise<BlockOutput> {
|
||||
logger.info(`Executing workflow response block: ${block.id}`)
|
||||
|
||||
try {
|
||||
const responseData = this.parseResponseData(inputs)
|
||||
const statusCode = this.parseStatus(inputs.status)
|
||||
const responseHeaders = this.parseHeaders(inputs.headers)
|
||||
|
||||
logger.info('Workflow Response prepared', {
|
||||
status: statusCode,
|
||||
dataKeys: typeof responseData === 'object' ? Object.keys(responseData) : [],
|
||||
headerKeys: Object.keys(responseHeaders),
|
||||
})
|
||||
|
||||
// Flat output structure - no response or data wrapper
|
||||
// Access as <Response.fieldName>, <Response.status>, <Response.headers>
|
||||
return {
|
||||
...responseData,
|
||||
status: statusCode,
|
||||
headers: responseHeaders,
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Workflow Response block execution failed:', error)
|
||||
return {
|
||||
error: {
|
||||
type: 'Workflow Response block execution failed',
|
||||
message: error.message || 'Unknown error',
|
||||
},
|
||||
status: HTTP.STATUS.SERVER_ERROR,
|
||||
headers: { 'Content-Type': HTTP.CONTENT_TYPE.JSON },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseResponseData(inputs: Record<string, any>): any {
|
||||
const dataMode = inputs.dataMode || 'structured'
|
||||
|
||||
if (dataMode === 'json' && inputs.data) {
|
||||
if (typeof inputs.data === 'string') {
|
||||
try {
|
||||
return JSON.parse(inputs.data)
|
||||
} catch (error) {
|
||||
logger.warn('Failed to parse JSON data, returning as string:', error)
|
||||
return inputs.data
|
||||
}
|
||||
} else if (typeof inputs.data === 'object' && inputs.data !== null) {
|
||||
return inputs.data
|
||||
}
|
||||
return inputs.data
|
||||
}
|
||||
|
||||
if (dataMode === 'structured' && inputs.builderData) {
|
||||
const convertedData = this.convertBuilderDataToJson(inputs.builderData)
|
||||
return this.parseObjectStrings(convertedData)
|
||||
}
|
||||
|
||||
return inputs.data || {}
|
||||
}
|
||||
|
||||
private convertBuilderDataToJson(builderData: JSONProperty[]): any {
|
||||
if (!Array.isArray(builderData)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const result: any = {}
|
||||
|
||||
for (const prop of builderData) {
|
||||
if (!prop.name || !prop.name.trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const value = this.convertPropertyValue(prop)
|
||||
result[prop.name] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private convertPropertyValue(prop: JSONProperty): any {
|
||||
switch (prop.type) {
|
||||
case 'object':
|
||||
return this.convertObjectValue(prop.value)
|
||||
case 'array':
|
||||
return this.convertArrayValue(prop.value)
|
||||
case 'number':
|
||||
return this.convertNumberValue(prop.value)
|
||||
case 'boolean':
|
||||
return this.convertBooleanValue(prop.value)
|
||||
case 'files':
|
||||
return prop.value
|
||||
default:
|
||||
return prop.value
|
||||
}
|
||||
}
|
||||
|
||||
private convertObjectValue(value: any): any {
|
||||
if (Array.isArray(value)) {
|
||||
return this.convertBuilderDataToJson(value)
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && !this.isVariableReference(value)) {
|
||||
return this.tryParseJson(value, value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private convertArrayValue(value: any): any {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item: any) => this.convertArrayItem(item))
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && !this.isVariableReference(value)) {
|
||||
const parsed = this.tryParseJson(value, value)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private convertArrayItem(item: any): any {
|
||||
if (typeof item !== 'object' || !item.type) {
|
||||
return item
|
||||
}
|
||||
|
||||
if (item.type === 'object' && Array.isArray(item.value)) {
|
||||
return this.convertBuilderDataToJson(item.value)
|
||||
}
|
||||
|
||||
if (item.type === 'array' && Array.isArray(item.value)) {
|
||||
return item.value.map((subItem: any) => {
|
||||
if (typeof subItem === 'object' && subItem.type) {
|
||||
return subItem.value
|
||||
}
|
||||
return subItem
|
||||
})
|
||||
}
|
||||
|
||||
return item.value
|
||||
}
|
||||
|
||||
private convertNumberValue(value: any): any {
|
||||
if (this.isVariableReference(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
const numValue = Number(value)
|
||||
if (Number.isNaN(numValue)) {
|
||||
return value
|
||||
}
|
||||
return numValue
|
||||
}
|
||||
|
||||
private convertBooleanValue(value: any): any {
|
||||
if (this.isVariableReference(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
return value === 'true' || value === true
|
||||
}
|
||||
|
||||
private tryParseJson(jsonString: string, fallback: any): any {
|
||||
try {
|
||||
return JSON.parse(jsonString)
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
private isVariableReference(value: any): boolean {
|
||||
return typeof value === 'string' && value.trim().startsWith('<') && value.trim().includes('>')
|
||||
}
|
||||
|
||||
private parseObjectStrings(data: any): any {
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
return this.parseObjectStrings(parsed)
|
||||
}
|
||||
return parsed
|
||||
} catch {
|
||||
return data
|
||||
}
|
||||
} else if (Array.isArray(data)) {
|
||||
return data.map((item) => this.parseObjectStrings(item))
|
||||
} else if (typeof data === 'object' && data !== null) {
|
||||
const result: any = {}
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
result[key] = this.parseObjectStrings(value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private parseStatus(status?: string): number {
|
||||
if (!status) return HTTP.STATUS.OK
|
||||
const parsed = Number(status)
|
||||
if (Number.isNaN(parsed) || parsed < 100 || parsed > 599) {
|
||||
return HTTP.STATUS.OK
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
private parseHeaders(
|
||||
headers: {
|
||||
id: string
|
||||
cells: { Key: string; Value: string }
|
||||
}[]
|
||||
): Record<string, string> {
|
||||
const defaultHeaders = { 'Content-Type': HTTP.CONTENT_TYPE.JSON }
|
||||
if (!headers) return defaultHeaders
|
||||
|
||||
const headerObj = headers.reduce((acc: Record<string, string>, header) => {
|
||||
if (header?.cells?.Key && header?.cells?.Value) {
|
||||
acc[header.cells.Key] = header.cells.Value
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return { ...defaultHeaders, ...headerObj }
|
||||
}
|
||||
}
|
||||
@@ -439,26 +439,46 @@ export function stripCustomToolPrefix(name: string) {
|
||||
}
|
||||
|
||||
export const workflowHasResponseBlock = (executionResult: ExecutionResult): boolean => {
|
||||
if (
|
||||
!executionResult?.logs ||
|
||||
!Array.isArray(executionResult.logs) ||
|
||||
!executionResult.success ||
|
||||
!executionResult.output.response
|
||||
) {
|
||||
if (!executionResult?.logs || !Array.isArray(executionResult.logs) || !executionResult.success) {
|
||||
return false
|
||||
}
|
||||
|
||||
const responseBlock = executionResult.logs.find(
|
||||
(log) => log?.blockType === 'response' && log?.success
|
||||
// Check for old response block format (has output.response)
|
||||
if (executionResult.output.response) {
|
||||
const responseBlock = executionResult.logs.find(
|
||||
(log) => log?.blockType === 'response' && log?.success
|
||||
)
|
||||
if (responseBlock) return true
|
||||
}
|
||||
|
||||
// Check for new workflow_response block format (has status/headers at root)
|
||||
const workflowResponseBlock = executionResult.logs.find(
|
||||
(log) => log?.blockType === 'workflow_response' && log?.success
|
||||
)
|
||||
|
||||
return responseBlock !== undefined
|
||||
return workflowResponseBlock !== undefined
|
||||
}
|
||||
|
||||
// Create a HTTP response from response block
|
||||
export const createHttpResponseFromBlock = (executionResult: ExecutionResult): NextResponse => {
|
||||
const output = executionResult.output.response
|
||||
const { data = {}, status = 200, headers = {} } = output
|
||||
// Check if it's the old response block format
|
||||
if (executionResult.output.response) {
|
||||
const output = executionResult.output.response
|
||||
const { data = {}, status = 200, headers = {} } = output
|
||||
|
||||
const responseHeaders = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
})
|
||||
|
||||
return NextResponse.json(data, {
|
||||
status: status,
|
||||
headers: responseHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
// New workflow_response format - status/headers at root, data spread at root
|
||||
const { status = 200, headers = {}, ...data } = executionResult.output
|
||||
|
||||
const responseHeaders = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user