mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(autoconnect): fixed autoconnect error for conditional block (#215)
* fix(autoconnect): fix bug where autoconnect didn't work for conditional block * improvement(ui): file upload * fix twilio block error * run format --------- Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
This commit is contained in:
co-authored by
Emir Karabeg
parent
da7c7e3687
commit
42936c89d0
@@ -320,7 +320,7 @@ export async function GET(req: NextRequest) {
|
||||
)
|
||||
|
||||
logger.info(`[${requestId}] Executing workflow ${schedule.workflowId}`)
|
||||
|
||||
|
||||
// Get workflow variables
|
||||
let workflowVariables = {}
|
||||
if (workflowRecord.variables) {
|
||||
@@ -332,15 +332,20 @@ export async function GET(req: NextRequest) {
|
||||
// Otherwise use as is (already parsed JSON)
|
||||
workflowVariables = workflowRecord.variables
|
||||
}
|
||||
logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${schedule.workflowId}`)
|
||||
logger.debug(
|
||||
`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${schedule.workflowId}`
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to parse workflow variables: ${schedule.workflowId}`, error)
|
||||
logger.error(
|
||||
`[${requestId}] Failed to parse workflow variables: ${schedule.workflowId}`,
|
||||
error
|
||||
)
|
||||
// Continue execution even if variables can't be parsed
|
||||
}
|
||||
} else {
|
||||
logger.debug(`[${requestId}] No workflow variables found for: ${schedule.workflowId}`)
|
||||
}
|
||||
|
||||
|
||||
const executor = new Executor(
|
||||
serializedWorkflow,
|
||||
processedBlockStates, // Use the processed block states
|
||||
|
||||
@@ -631,9 +631,14 @@ async function processWebhook(
|
||||
// Otherwise use as is (already parsed JSON)
|
||||
workflowVariables = foundWorkflow.variables
|
||||
}
|
||||
logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${foundWorkflow.id}`)
|
||||
logger.debug(
|
||||
`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${foundWorkflow.id}`
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to parse workflow variables: ${foundWorkflow.id}`, error)
|
||||
logger.error(
|
||||
`[${requestId}] Failed to parse workflow variables: ${foundWorkflow.id}`,
|
||||
error
|
||||
)
|
||||
// Continue execution even if variables can't be parsed
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -260,15 +260,16 @@ describe('Workflow Execution API Route', () => {
|
||||
|
||||
// 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
|
||||
expect.anything(), // decryptedEnvVars
|
||||
expect.objectContaining({ // processedInput
|
||||
input: requestBody
|
||||
expect.objectContaining({
|
||||
// processedInput
|
||||
input: requestBody,
|
||||
}),
|
||||
expect.anything() // workflowVariables
|
||||
)
|
||||
@@ -313,8 +314,9 @@ describe('Workflow Execution API Route', () => {
|
||||
expect.anything(), // serializedWorkflow
|
||||
expect.anything(), // processedBlockStates
|
||||
expect.anything(), // decryptedEnvVars
|
||||
expect.objectContaining({ // processedInput
|
||||
input: structuredInput
|
||||
expect.objectContaining({
|
||||
// processedInput
|
||||
input: structuredInput,
|
||||
}),
|
||||
expect.anything() // workflowVariables
|
||||
)
|
||||
@@ -463,8 +465,8 @@ describe('Workflow Execution API Route', () => {
|
||||
it('should pass workflow variables to the Executor', async () => {
|
||||
// Create mock variables for the workflow
|
||||
const workflowVariables = {
|
||||
'variable1': { id: 'var1', name: 'variable1', type: 'string', value: '"test value"' },
|
||||
'variable2': { id: 'var2', name: 'variable2', type: 'boolean', value: 'true' }
|
||||
variable1: { id: 'var1', name: 'variable1', type: 'string', value: '"test value"' },
|
||||
variable2: { id: 'var2', name: 'variable2', type: 'boolean', value: 'true' },
|
||||
}
|
||||
|
||||
// Mock workflow with variables
|
||||
@@ -539,15 +541,15 @@ describe('Workflow Execution API Route', () => {
|
||||
|
||||
// Verify the Executor was constructed with workflow variables
|
||||
expect(executorConstructorMock).toHaveBeenCalled()
|
||||
|
||||
|
||||
// Check that the 5th parameter (workflow variables) was passed
|
||||
const executorCalls = executorConstructorMock.mock.calls
|
||||
expect(executorCalls.length).toBeGreaterThan(0)
|
||||
|
||||
|
||||
// Each call to the constructor should have at least 5 parameters
|
||||
const lastCall = executorCalls[executorCalls.length - 1]
|
||||
expect(lastCall.length).toBeGreaterThanOrEqual(5)
|
||||
|
||||
|
||||
// The 5th parameter should be the workflow variables
|
||||
expect(lastCall[4]).toEqual(workflowVariables)
|
||||
})
|
||||
|
||||
@@ -38,17 +38,22 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
|
||||
}
|
||||
|
||||
// Log input to help debug
|
||||
logger.info(`[${requestId}] Executing workflow with input:`,
|
||||
input ? JSON.stringify(input, null, 2) : 'No input provided');
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Executing workflow with input:`,
|
||||
input ? JSON.stringify(input, null, 2) : 'No input provided'
|
||||
)
|
||||
|
||||
// Validate and structure input for maximum compatibility
|
||||
let processedInput = input;
|
||||
let processedInput = input
|
||||
if (input && typeof input === 'object') {
|
||||
// Ensure input is properly structured for the starter block
|
||||
if (input.input === undefined) {
|
||||
// If input is not already nested, structure it properly
|
||||
processedInput = { input: input };
|
||||
logger.info(`[${requestId}] Restructured input for workflow:`, JSON.stringify(processedInput, null, 2));
|
||||
processedInput = { input: input }
|
||||
logger.info(
|
||||
`[${requestId}] Restructured input for workflow:`,
|
||||
JSON.stringify(processedInput, null, 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +179,9 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
|
||||
// Otherwise use as is (already parsed JSON)
|
||||
workflowVariables = workflow.variables
|
||||
}
|
||||
logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${workflowId}`)
|
||||
logger.debug(
|
||||
`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${workflowId}`
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to parse workflow variables: ${workflowId}`, error)
|
||||
// Continue execution even if variables can't be parsed
|
||||
@@ -188,13 +195,13 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
|
||||
const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
|
||||
|
||||
const executor = new Executor(
|
||||
serializedWorkflow,
|
||||
processedBlockStates,
|
||||
decryptedEnvVars,
|
||||
processedInput,
|
||||
serializedWorkflow,
|
||||
processedBlockStates,
|
||||
decryptedEnvVars,
|
||||
processedInput,
|
||||
workflowVariables
|
||||
)
|
||||
|
||||
|
||||
const result = await executor.execute(workflowId)
|
||||
|
||||
logger.info(`[${requestId}] Workflow execution completed: ${workflowId}`, {
|
||||
@@ -278,7 +285,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const bodyText = await request.text()
|
||||
logger.info(`[${requestId}] Raw request body:`, bodyText)
|
||||
|
||||
|
||||
let body = {}
|
||||
if (bodyText && bodyText.trim()) {
|
||||
try {
|
||||
@@ -293,9 +300,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
|
||||
// Don't double-nest the input if it's already structured
|
||||
const hasContent = Object.keys(body).length > 0;
|
||||
const input = hasContent ? { input: body } : {};
|
||||
|
||||
const hasContent = Object.keys(body).length > 0
|
||||
const input = hasContent ? { input: body } : {}
|
||||
|
||||
logger.info(`[${requestId}] Input passed to workflow:`, JSON.stringify(input, null, 2))
|
||||
|
||||
// Execute workflow with the structured input
|
||||
|
||||
+12
-12
@@ -1,12 +1,12 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { isEqual } from 'lodash'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
/**
|
||||
* Custom hook to get and set values for a sub-block in a workflow.
|
||||
* Handles complex object values properly by using deep equality comparison.
|
||||
*
|
||||
*
|
||||
* @param blockId The ID of the block containing the sub-block
|
||||
* @param subBlockId The ID of the sub-block
|
||||
* @param triggerWorkflowUpdate Whether to trigger a workflow update when the value changes
|
||||
@@ -30,10 +30,7 @@ export function useSubBlockValue<T = any>(
|
||||
|
||||
// Get value from subblock store
|
||||
const storeValue = useSubBlockStore(
|
||||
useCallback(
|
||||
(state) => state.getValue(blockId, subBlockId),
|
||||
[blockId, subBlockId]
|
||||
)
|
||||
useCallback((state) => state.getValue(blockId, subBlockId), [blockId, subBlockId])
|
||||
)
|
||||
|
||||
// Update the ref if the store value changes
|
||||
@@ -51,16 +48,19 @@ export function useSubBlockValue<T = any>(
|
||||
// Use deep comparison to avoid unnecessary updates for complex objects
|
||||
if (!isEqual(valueRef.current, newValue)) {
|
||||
valueRef.current = newValue
|
||||
|
||||
|
||||
// Ensure we're passing the actual value, not a reference that might change
|
||||
const valueCopy = newValue === null
|
||||
? null
|
||||
: (typeof newValue === 'object' ? JSON.parse(JSON.stringify(newValue)) : newValue)
|
||||
|
||||
const valueCopy =
|
||||
newValue === null
|
||||
? null
|
||||
: typeof newValue === 'object'
|
||||
? JSON.parse(JSON.stringify(newValue))
|
||||
: newValue
|
||||
|
||||
// Update the subblock store with the new value
|
||||
// The store's setValue method will now trigger the debounced sync automatically
|
||||
useSubBlockStore.getState().setValue(blockId, subBlockId, valueCopy)
|
||||
|
||||
|
||||
if (triggerWorkflowUpdate) {
|
||||
useWorkflowStore.getState().triggerUpdate()
|
||||
}
|
||||
|
||||
+105
-75
@@ -80,6 +80,50 @@ function WorkflowContent() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Handle drops
|
||||
const findClosestOutput = useCallback(
|
||||
(newNodePosition: { x: number; y: number }) => {
|
||||
const existingBlocks = Object.entries(blocks)
|
||||
.filter(([_, block]) => block.enabled)
|
||||
.map(([id, block]) => ({
|
||||
id,
|
||||
type: block.type,
|
||||
position: block.position,
|
||||
distance: Math.sqrt(
|
||||
Math.pow(block.position.x - newNodePosition.x, 2) +
|
||||
Math.pow(block.position.y - newNodePosition.y, 2)
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
|
||||
return existingBlocks[0] ? existingBlocks[0] : null
|
||||
},
|
||||
[blocks]
|
||||
)
|
||||
|
||||
// Determine the appropriate source handle based on block type
|
||||
const determineSourceHandle = useCallback((block: { id: string; type: string }) => {
|
||||
// Default source handle
|
||||
let sourceHandle = 'source'
|
||||
|
||||
// For condition blocks, use the first condition handle
|
||||
if (block.type === 'condition') {
|
||||
// Get just the first condition handle from the DOM
|
||||
const conditionHandles = document.querySelectorAll(
|
||||
`[data-nodeid^="${block.id}"][data-handleid^="condition-"]`
|
||||
)
|
||||
if (conditionHandles.length > 0) {
|
||||
// Extract the full handle ID from the first condition handle
|
||||
const handleId = conditionHandles[0].getAttribute('data-handleid')
|
||||
if (handleId) {
|
||||
sourceHandle = handleId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sourceHandle
|
||||
}, [])
|
||||
|
||||
// Listen for toolbar block click events
|
||||
useEffect(() => {
|
||||
const handleAddBlockFromToolbar = (event: CustomEvent) => {
|
||||
@@ -112,13 +156,16 @@ function WorkflowContent() {
|
||||
// Auto-connect logic
|
||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
||||
if (isAutoConnectEnabled && type !== 'starter') {
|
||||
const closestBlockId = findClosestOutput(centerPosition)
|
||||
if (closestBlockId) {
|
||||
const closestBlock = findClosestOutput(centerPosition)
|
||||
if (closestBlock) {
|
||||
// Get appropriate source handle
|
||||
const sourceHandle = determineSourceHandle(closestBlock)
|
||||
|
||||
addEdge({
|
||||
id: crypto.randomUUID(),
|
||||
source: closestBlockId,
|
||||
source: closestBlock.id,
|
||||
target: id,
|
||||
sourceHandle: 'source',
|
||||
sourceHandle,
|
||||
targetHandle: 'target',
|
||||
type: 'custom',
|
||||
})
|
||||
@@ -134,7 +181,60 @@ function WorkflowContent() {
|
||||
handleAddBlockFromToolbar as EventListener
|
||||
)
|
||||
}
|
||||
}, [project, blocks, addBlock, addEdge])
|
||||
}, [project, blocks, addBlock, addEdge, findClosestOutput, determineSourceHandle])
|
||||
|
||||
// Update the onDrop handler
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.dataTransfer.getData('application/json'))
|
||||
if (data.type === 'connectionBlock') return
|
||||
|
||||
const reactFlowBounds = event.currentTarget.getBoundingClientRect()
|
||||
const position = project({
|
||||
x: event.clientX - reactFlowBounds.left,
|
||||
y: event.clientY - reactFlowBounds.top,
|
||||
})
|
||||
|
||||
const blockConfig = getBlock(data.type)
|
||||
if (!blockConfig) {
|
||||
logger.error('Invalid block type:', { data })
|
||||
return
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID()
|
||||
const name = `${blockConfig.name} ${
|
||||
Object.values(blocks).filter((b) => b.type === data.type).length + 1
|
||||
}`
|
||||
|
||||
addBlock(id, data.type, name, position)
|
||||
|
||||
// Auto-connect logic
|
||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
||||
if (isAutoConnectEnabled && data.type !== 'starter') {
|
||||
const closestBlock = findClosestOutput(position)
|
||||
if (closestBlock) {
|
||||
// Get appropriate source handle
|
||||
const sourceHandle = determineSourceHandle(closestBlock)
|
||||
|
||||
addEdge({
|
||||
id: crypto.randomUUID(),
|
||||
source: closestBlock.id,
|
||||
target: id,
|
||||
sourceHandle,
|
||||
targetHandle: 'target',
|
||||
type: 'workflowEdge',
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error dropping block:', { err })
|
||||
}
|
||||
},
|
||||
[project, blocks, addBlock, addEdge, findClosestOutput, determineSourceHandle]
|
||||
)
|
||||
|
||||
// Init workflow
|
||||
useEffect(() => {
|
||||
@@ -305,76 +405,6 @@ function WorkflowContent() {
|
||||
[addEdge]
|
||||
)
|
||||
|
||||
// Handle drops
|
||||
const findClosestOutput = useCallback(
|
||||
(newNodePosition: { x: number; y: number }) => {
|
||||
const existingBlocks = Object.entries(blocks)
|
||||
.filter(([_, block]) => block.enabled && block.type !== 'condition')
|
||||
.map(([id, block]) => ({
|
||||
id,
|
||||
position: block.position,
|
||||
distance: Math.sqrt(
|
||||
Math.pow(block.position.x - newNodePosition.x, 2) +
|
||||
Math.pow(block.position.y - newNodePosition.y, 2)
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
|
||||
return existingBlocks[0]?.id
|
||||
},
|
||||
[blocks]
|
||||
)
|
||||
|
||||
// Update the onDrop handler
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.dataTransfer.getData('application/json'))
|
||||
if (data.type === 'connectionBlock') return
|
||||
|
||||
const reactFlowBounds = event.currentTarget.getBoundingClientRect()
|
||||
const position = project({
|
||||
x: event.clientX - reactFlowBounds.left,
|
||||
y: event.clientY - reactFlowBounds.top,
|
||||
})
|
||||
|
||||
const blockConfig = getBlock(data.type)
|
||||
if (!blockConfig) {
|
||||
logger.error('Invalid block type:', { data })
|
||||
return
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID()
|
||||
const name = `${blockConfig.name} ${
|
||||
Object.values(blocks).filter((b) => b.type === data.type).length + 1
|
||||
}`
|
||||
|
||||
addBlock(id, data.type, name, position)
|
||||
|
||||
// Auto-connect logic
|
||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
||||
if (isAutoConnectEnabled && data.type !== 'starter') {
|
||||
const closestBlockId = findClosestOutput(position)
|
||||
if (closestBlockId) {
|
||||
addEdge({
|
||||
id: crypto.randomUUID(),
|
||||
source: closestBlockId,
|
||||
target: id,
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target',
|
||||
type: 'custom',
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Error dropping block:', { err })
|
||||
}
|
||||
},
|
||||
[project, blocks, addBlock, addEdge, findClosestOutput]
|
||||
)
|
||||
|
||||
// Update onPaneClick to only handle edge selection
|
||||
const onPaneClick = useCallback(() => {
|
||||
setSelectedEdgeId(null)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { TwilioIcon } from '@/components/icons'
|
||||
import { BlockCategory, BlockConfig, BlockIcon } from '../types'
|
||||
import { TwilioSMSBlockOutput } from '@/tools/twilio/types'
|
||||
import { BlockCategory, BlockConfig, BlockIcon } from '../types'
|
||||
|
||||
export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
||||
type: 'twilio_sms',
|
||||
name: 'Twilio SMS',
|
||||
description: 'Send SMS messages via Twilio',
|
||||
longDescription:
|
||||
'Send text messages to single or multiple recipients using the Twilio API.',
|
||||
longDescription: 'Send text messages to single or multiple recipients using the Twilio API.',
|
||||
category: 'tools',
|
||||
bgColor: '#F22F46', // Twilio brand color
|
||||
icon: TwilioIcon,
|
||||
@@ -47,12 +46,12 @@ export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'e.g. +1234567890',
|
||||
}
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['twilio_send_sms'],
|
||||
config: {
|
||||
tool: () => 'twilio_send_sms'
|
||||
tool: () => 'twilio_send_sms',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
@@ -60,16 +59,16 @@ export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
|
||||
message: { type: 'string', required: true },
|
||||
accountSid: { type: 'string', required: true },
|
||||
authToken: { type: 'string', required: true },
|
||||
fromNumber: { type: 'string', required: true }
|
||||
fromNumber: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
success: 'boolean',
|
||||
messageId: 'string',
|
||||
status: 'string',
|
||||
error: 'string'
|
||||
messageId: 'any',
|
||||
status: 'any',
|
||||
error: 'any',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,72 +3,75 @@ import { ToolResponse } from '@/tools/types'
|
||||
import { BlockConfig } from '../types'
|
||||
|
||||
interface TypeformResponse extends ToolResponse {
|
||||
output: {
|
||||
total_items: number
|
||||
page_count: number
|
||||
items: Array<{
|
||||
landing_id: string
|
||||
token: string
|
||||
landed_at: string
|
||||
submitted_at: string
|
||||
metadata: {
|
||||
user_agent: string
|
||||
platform: string
|
||||
referer: string
|
||||
network_id: string
|
||||
browser: string
|
||||
output:
|
||||
| {
|
||||
total_items: number
|
||||
page_count: number
|
||||
items: Array<{
|
||||
landing_id: string
|
||||
token: string
|
||||
landed_at: string
|
||||
submitted_at: string
|
||||
metadata: {
|
||||
user_agent: string
|
||||
platform: string
|
||||
referer: string
|
||||
network_id: string
|
||||
browser: string
|
||||
}
|
||||
answers: Array<{
|
||||
field: {
|
||||
id: string
|
||||
type: string
|
||||
ref: string
|
||||
}
|
||||
type: string
|
||||
[key: string]: any // For different answer types (text, boolean, number, etc.)
|
||||
}>
|
||||
hidden: Record<string, any>
|
||||
calculated: {
|
||||
score: number
|
||||
}
|
||||
variables: Array<{
|
||||
key: string
|
||||
type: string
|
||||
[key: string]: any // For different variable types
|
||||
}>
|
||||
}>
|
||||
}
|
||||
answers: Array<{
|
||||
field: {
|
||||
| {
|
||||
fileUrl: string
|
||||
contentType: string
|
||||
filename: string
|
||||
}
|
||||
| {
|
||||
fields: Array<{
|
||||
dropoffs: number
|
||||
id: string
|
||||
type: string
|
||||
label: string
|
||||
ref: string
|
||||
title: string
|
||||
type: string
|
||||
views: number
|
||||
}>
|
||||
form: {
|
||||
platforms: Array<{
|
||||
average_time: number
|
||||
completion_rate: number
|
||||
platform: string
|
||||
responses_count: number
|
||||
total_visits: number
|
||||
unique_visits: number
|
||||
}>
|
||||
summary: {
|
||||
average_time: number
|
||||
completion_rate: number
|
||||
responses_count: number
|
||||
total_visits: number
|
||||
unique_visits: number
|
||||
}
|
||||
}
|
||||
type: string
|
||||
[key: string]: any // For different answer types (text, boolean, number, etc.)
|
||||
}>
|
||||
hidden: Record<string, any>
|
||||
calculated: {
|
||||
score: number
|
||||
}
|
||||
variables: Array<{
|
||||
key: string
|
||||
type: string
|
||||
[key: string]: any // For different variable types
|
||||
}>
|
||||
}>
|
||||
} | {
|
||||
fileUrl: string
|
||||
contentType: string
|
||||
filename: string
|
||||
} | {
|
||||
fields: Array<{
|
||||
dropoffs: number
|
||||
id: string
|
||||
label: string
|
||||
ref: string
|
||||
title: string
|
||||
type: string
|
||||
views: number
|
||||
}>
|
||||
form: {
|
||||
platforms: Array<{
|
||||
average_time: number
|
||||
completion_rate: number
|
||||
platform: string
|
||||
responses_count: number
|
||||
total_visits: number
|
||||
unique_visits: number
|
||||
}>
|
||||
summary: {
|
||||
average_time: number
|
||||
completion_rate: number
|
||||
responses_count: number
|
||||
total_visits: number
|
||||
unique_visits: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const TypeformBlock: BlockConfig<TypeformResponse> = {
|
||||
|
||||
+2
-2
@@ -1,5 +1,6 @@
|
||||
// Import blocks
|
||||
import { AgentBlock } from './blocks/agent'
|
||||
import { AirtableBlock } from './blocks/airtable'
|
||||
import { ApiBlock } from './blocks/api'
|
||||
import { ConditionBlock } from './blocks/condition'
|
||||
import { ConfluenceBlock } from './blocks/confluence'
|
||||
@@ -28,14 +29,13 @@ import { StarterBlock } from './blocks/starter'
|
||||
import { SupabaseBlock } from './blocks/supabase'
|
||||
import { TavilyBlock } from './blocks/tavily'
|
||||
import { TranslateBlock } from './blocks/translate'
|
||||
import { TwilioSMSBlock } from './blocks/twilio'
|
||||
import { TypeformBlock } from './blocks/typeform'
|
||||
import { VisionBlock } from './blocks/vision'
|
||||
import { WhatsAppBlock } from './blocks/whatsapp'
|
||||
import { XBlock } from './blocks/x'
|
||||
import { YouTubeBlock } from './blocks/youtube'
|
||||
import { AirtableBlock } from './blocks/airtable'
|
||||
import { BlockConfig } from './types'
|
||||
import { TwilioSMSBlock } from './blocks/twilio'
|
||||
|
||||
// Export blocks for ease of use
|
||||
export {
|
||||
|
||||
@@ -1745,11 +1745,11 @@ export function TwilioIcon(props: SVGProps<SVGSVGElement>) {
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="128" cy="128" r="128" fill="none" stroke="white" strokeWidth="21"/>
|
||||
<circle cx="85" cy="85" r="21" fill="white"/>
|
||||
<circle cx="171" cy="85" r="21" fill="white"/>
|
||||
<circle cx="85" cy="171" r="21" fill="white"/>
|
||||
<circle cx="171" cy="171" r="21" fill="white"/>
|
||||
<circle cx="128" cy="128" r="128" fill="none" stroke="white" strokeWidth="21" />
|
||||
<circle cx="85" cy="85" r="21" fill="white" />
|
||||
<circle cx="171" cy="85" r="21" fill="white" />
|
||||
<circle cx="85" cy="171" r="21" fill="white" />
|
||||
<circle cx="171" cy="171" r="21" fill="white" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,12 @@ import { cn } from '@/lib/utils'
|
||||
// This file is not typed correctly from shadcn, so we're disabling the type checker
|
||||
// @ts-nocheck
|
||||
|
||||
// This file is not typed correctly from shadcn, so we're disabling the type checker
|
||||
// @ts-nocheck
|
||||
|
||||
// This file is not typed correctly from shadcn, so we're disabling the type checker
|
||||
// @ts-nocheck
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive> & {
|
||||
|
||||
+16
-10
@@ -819,32 +819,38 @@ export class ApiBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
let processedInputs = { ...inputs };
|
||||
|
||||
let processedInputs = { ...inputs }
|
||||
|
||||
// Handle body specifically to ensure it's properly processed for API requests
|
||||
if (processedInputs.body !== undefined) {
|
||||
// If body is a string that looks like JSON, parse it
|
||||
if (typeof processedInputs.body === 'string') {
|
||||
try {
|
||||
// Trim whitespace before checking for JSON pattern
|
||||
const trimmedBody = processedInputs.body.trim();
|
||||
const trimmedBody = processedInputs.body.trim()
|
||||
if (trimmedBody.startsWith('{') || trimmedBody.startsWith('[')) {
|
||||
processedInputs.body = JSON.parse(trimmedBody);
|
||||
logger.info('[ApiBlockHandler] Parsed JSON body:', JSON.stringify(processedInputs.body, null, 2));
|
||||
processedInputs.body = JSON.parse(trimmedBody)
|
||||
logger.info(
|
||||
'[ApiBlockHandler] Parsed JSON body:',
|
||||
JSON.stringify(processedInputs.body, null, 2)
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.info('[ApiBlockHandler] Failed to parse body as JSON, using as string:', e);
|
||||
logger.info('[ApiBlockHandler] Failed to parse body as JSON, using as string:', e)
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
} else if (processedInputs.body === null) {
|
||||
// Convert null to undefined for consistency with API expectations
|
||||
processedInputs.body = undefined;
|
||||
processedInputs.body = undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ensure the final processed body is logged
|
||||
logger.info('[ApiBlockHandler] Final processed request body:', JSON.stringify(processedInputs.body, null, 2));
|
||||
|
||||
logger.info(
|
||||
'[ApiBlockHandler] Final processed request body:',
|
||||
JSON.stringify(processedInputs.body, null, 2)
|
||||
)
|
||||
|
||||
const result = await executeTool(block.config.tool, {
|
||||
...processedInputs,
|
||||
_context: { workflowId: context.workflowId },
|
||||
|
||||
+74
-63
@@ -45,12 +45,12 @@ export class Executor {
|
||||
this.validateWorkflow()
|
||||
|
||||
if (workflowInput) {
|
||||
this.workflowInput = workflowInput;
|
||||
logger.info('[Executor] Using workflow input:', JSON.stringify(this.workflowInput, null, 2));
|
||||
this.workflowInput = workflowInput
|
||||
logger.info('[Executor] Using workflow input:', JSON.stringify(this.workflowInput, null, 2))
|
||||
} else {
|
||||
this.workflowInput = {};
|
||||
this.workflowInput = {}
|
||||
}
|
||||
|
||||
|
||||
this.loopManager = new LoopManager(workflow.loops || {})
|
||||
this.resolver = new InputResolver(
|
||||
workflow,
|
||||
@@ -421,70 +421,75 @@ export class Executor {
|
||||
})
|
||||
} else {
|
||||
*/
|
||||
// No input format defined or not an array,
|
||||
// Handle API call - prioritize using the input as-is
|
||||
if (this.workflowInput && typeof this.workflowInput === 'object') {
|
||||
// For API calls, extract input from the nested structure if it exists
|
||||
const inputData = this.workflowInput.input !== undefined
|
||||
? this.workflowInput.input // Use the nested input data
|
||||
: this.workflowInput; // Fallback to direct input
|
||||
|
||||
// Create starter output with both formats for maximum compatibility
|
||||
const starterOutput = {
|
||||
response: {
|
||||
input: inputData,
|
||||
...inputData, // Make fields directly accessible at response level
|
||||
},
|
||||
}
|
||||
// No input format defined or not an array,
|
||||
// Handle API call - prioritize using the input as-is
|
||||
if (this.workflowInput && typeof this.workflowInput === 'object') {
|
||||
// For API calls, extract input from the nested structure if it exists
|
||||
const inputData =
|
||||
this.workflowInput.input !== undefined
|
||||
? this.workflowInput.input // Use the nested input data
|
||||
: this.workflowInput // Fallback to direct input
|
||||
|
||||
logger.info(`[Executor] API input for starter block:`, {
|
||||
type: typeof inputData,
|
||||
isArray: Array.isArray(inputData),
|
||||
keys: Object.keys(inputData),
|
||||
inputData: JSON.stringify(inputData, null, 2),
|
||||
});
|
||||
|
||||
logger.info(`[Executor] Final starter block output:`, JSON.stringify(starterOutput, null, 2));
|
||||
|
||||
context.blockStates.set(starterBlock.id, {
|
||||
output: starterOutput,
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
} else {
|
||||
// Fallback for other cases
|
||||
const starterOutput = {
|
||||
response: {
|
||||
input: this.workflowInput,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[Executor] Simple starter output:`, JSON.stringify(starterOutput, null, 2));
|
||||
|
||||
context.blockStates.set(starterBlock.id, {
|
||||
output: starterOutput,
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
// Create starter output with both formats for maximum compatibility
|
||||
const starterOutput = {
|
||||
response: {
|
||||
input: inputData,
|
||||
...inputData, // Make fields directly accessible at response level
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[Executor] API input for starter block:`, {
|
||||
type: typeof inputData,
|
||||
isArray: Array.isArray(inputData),
|
||||
keys: Object.keys(inputData),
|
||||
inputData: JSON.stringify(inputData, null, 2),
|
||||
})
|
||||
|
||||
logger.info(
|
||||
`[Executor] Final starter block output:`,
|
||||
JSON.stringify(starterOutput, null, 2)
|
||||
)
|
||||
|
||||
context.blockStates.set(starterBlock.id, {
|
||||
output: starterOutput,
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
} else {
|
||||
// Fallback for other cases
|
||||
const starterOutput = {
|
||||
response: {
|
||||
input: this.workflowInput,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[Executor] Simple starter output:`, JSON.stringify(starterOutput, null, 2))
|
||||
|
||||
context.blockStates.set(starterBlock.id, {
|
||||
output: starterOutput,
|
||||
executed: true,
|
||||
executionTime: 0,
|
||||
})
|
||||
}
|
||||
//} // End of inputFormat conditional
|
||||
} catch (e) {
|
||||
logger.warn('Error processing starter block input format:', e)
|
||||
|
||||
|
||||
// Fallback to raw input with both paths accessible
|
||||
// Ensure we handle both input formats
|
||||
const inputData = this.workflowInput.input !== undefined
|
||||
? this.workflowInput.input // Use nested input if available
|
||||
: this.workflowInput; // Fallback to direct input
|
||||
|
||||
const inputData =
|
||||
this.workflowInput.input !== undefined
|
||||
? this.workflowInput.input // Use nested input if available
|
||||
: this.workflowInput // Fallback to direct input
|
||||
|
||||
const starterOutput = {
|
||||
response: {
|
||||
input: inputData,
|
||||
...inputData, // Add input fields directly at response level too
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[Executor] Fallback starter output:`, JSON.stringify(starterOutput, null, 2));
|
||||
|
||||
logger.info(`[Executor] Fallback starter output:`, JSON.stringify(starterOutput, null, 2))
|
||||
|
||||
context.blockStates.set(starterBlock.id, {
|
||||
output: starterOutput,
|
||||
@@ -493,8 +498,8 @@ export class Executor {
|
||||
})
|
||||
}
|
||||
// Ensure the starter block is in the active execution path
|
||||
context.activeExecutionPath.add(starterBlock.id);
|
||||
// Mark the starter block as executed
|
||||
context.activeExecutionPath.add(starterBlock.id)
|
||||
// Mark the starter block as executed
|
||||
context.executedBlocks.add(starterBlock.id)
|
||||
|
||||
// Add all blocks connected to the starter to the active execution path
|
||||
@@ -688,18 +693,24 @@ export class Executor {
|
||||
if (starterBlock) {
|
||||
const starterState = context.blockStates.get(starterBlock.id)
|
||||
if (!starterState) {
|
||||
logger.warn(`Starter block state not found when executing ${block.metadata?.name || blockId}. This may cause reference errors.`)
|
||||
logger.warn(
|
||||
`Starter block state not found when executing ${block.metadata?.name || blockId}. This may cause reference errors.`
|
||||
)
|
||||
} else {
|
||||
logger.debug(`Starter block state available for ${block.metadata?.name || blockId}:`,
|
||||
JSON.stringify(starterState.output || {}, null, 2));
|
||||
logger.debug(
|
||||
`Starter block state available for ${block.metadata?.name || blockId}:`,
|
||||
JSON.stringify(starterState.output || {}, null, 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve inputs (which will look up references to other blocks including starter)
|
||||
const inputs = this.resolver.resolveInputs(block, context)
|
||||
logger.debug(`Resolved inputs for ${block.metadata?.name || blockId}:`,
|
||||
JSON.stringify(inputs, null, 2));
|
||||
|
||||
logger.debug(
|
||||
`Resolved inputs for ${block.metadata?.name || blockId}:`,
|
||||
JSON.stringify(inputs, null, 2)
|
||||
)
|
||||
|
||||
// Find the appropriate handler
|
||||
const handler = this.blockHandlers.find((h) => h.canHandle(block))
|
||||
if (!handler) {
|
||||
|
||||
+114
-87
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import { LoopManager } from './loops'
|
||||
import { ExecutionContext } from './types'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
const logger = createLogger('InputResolver')
|
||||
|
||||
@@ -82,28 +82,28 @@ export class InputResolver {
|
||||
// Special handling for different block types
|
||||
const isFunctionBlock = block.metadata?.id === 'function'
|
||||
const isApiBlock = block.metadata?.id === 'api'
|
||||
|
||||
|
||||
// For function blocks, we need special handling for code input
|
||||
if (isFunctionBlock && key === 'code') {
|
||||
// For code input in function blocks, we don't want to parse JSON
|
||||
result[key] = resolvedValue
|
||||
logger.debug(`[resolveInputs] Function block code input preserved as string`);
|
||||
}
|
||||
logger.debug(`[resolveInputs] Function block code input preserved as string`)
|
||||
}
|
||||
// For API blocks, handle body input specially
|
||||
else if (isApiBlock && key === 'body') {
|
||||
try {
|
||||
// If it's JSON-looking, preserve its structure
|
||||
if (resolvedValue.trim().startsWith('{') || resolvedValue.trim().startsWith('[')) {
|
||||
result[key] = JSON.parse(resolvedValue)
|
||||
logger.debug(`[resolveInputs] API block body parsed as JSON object`);
|
||||
logger.debug(`[resolveInputs] API block body parsed as JSON object`)
|
||||
} else {
|
||||
result[key] = resolvedValue
|
||||
logger.debug(`[resolveInputs] API block body preserved as string`);
|
||||
logger.debug(`[resolveInputs] API block body preserved as string`)
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, keep as string
|
||||
result[key] = resolvedValue
|
||||
logger.debug(`[resolveInputs] API block body JSON parsing failed, keeping as string`);
|
||||
logger.debug(`[resolveInputs] API block body JSON parsing failed, keeping as string`)
|
||||
}
|
||||
}
|
||||
// For other inputs, try to convert JSON strings to objects
|
||||
@@ -111,7 +111,7 @@ export class InputResolver {
|
||||
try {
|
||||
if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) {
|
||||
result[key] = JSON.parse(resolvedValue)
|
||||
logger.debug(`[resolveInputs] Parsed JSON value for ${key}`);
|
||||
logger.debug(`[resolveInputs] Parsed JSON value for ${key}`)
|
||||
} else {
|
||||
result[key] = resolvedValue
|
||||
}
|
||||
@@ -158,16 +158,18 @@ export class InputResolver {
|
||||
|
||||
if (foundVariable) {
|
||||
const [_, variable] = foundVariable
|
||||
|
||||
|
||||
// Process variable value based on its type
|
||||
let processedValue = variable.value
|
||||
|
||||
|
||||
// Handle string values that could be stored with quotes
|
||||
if (variable.type === 'string' && typeof processedValue === 'string') {
|
||||
// If the string value starts and ends with quotes, remove them
|
||||
const trimmed = processedValue.trim()
|
||||
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
||||
if (
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||
) {
|
||||
// Remove the quotes and unescape any escaped quotes
|
||||
processedValue = trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'")
|
||||
}
|
||||
@@ -184,30 +186,32 @@ export class InputResolver {
|
||||
}
|
||||
}
|
||||
// Handle object/array values that might be stored as JSON strings
|
||||
else if ((variable.type === 'object' || variable.type === 'array') &&
|
||||
typeof processedValue === 'string') {
|
||||
else if (
|
||||
(variable.type === 'object' || variable.type === 'array') &&
|
||||
typeof processedValue === 'string'
|
||||
) {
|
||||
try {
|
||||
processedValue = JSON.parse(processedValue)
|
||||
} catch (e) {
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Determine if this needs to be a code-compatible string literal
|
||||
const needsCodeStringLiteral = this.needsCodeStringLiteral(currentBlock, value);
|
||||
|
||||
const needsCodeStringLiteral = this.needsCodeStringLiteral(currentBlock, value)
|
||||
|
||||
// Format the processed value for insertion into the string based on context
|
||||
let formattedValue: string;
|
||||
|
||||
let formattedValue: string
|
||||
|
||||
if (variable.type === 'string' && needsCodeStringLiteral) {
|
||||
// For code contexts like function and condition blocks, properly quote strings
|
||||
formattedValue = JSON.stringify(processedValue);
|
||||
formattedValue = JSON.stringify(processedValue)
|
||||
} else if (typeof processedValue === 'object' && processedValue !== null) {
|
||||
// For objects, always stringify
|
||||
formattedValue = JSON.stringify(processedValue);
|
||||
formattedValue = JSON.stringify(processedValue)
|
||||
} else {
|
||||
// For other types in normal contexts, use simple string conversion
|
||||
formattedValue = String(processedValue);
|
||||
formattedValue = String(processedValue)
|
||||
}
|
||||
|
||||
resolvedValue = resolvedValue.replace(match, formattedValue)
|
||||
@@ -220,51 +224,51 @@ export class InputResolver {
|
||||
/**
|
||||
* Determines if a value needs to be formatted as a code-compatible string literal
|
||||
* based on the block type and context. Handles JavaScript and other code contexts.
|
||||
*
|
||||
*
|
||||
* @param block - The block where the value is being used
|
||||
* @param expression - The expression containing the value
|
||||
* @returns Whether the value should be formatted as a string literal
|
||||
*/
|
||||
private needsCodeStringLiteral(block?: SerializedBlock, expression?: string): boolean {
|
||||
if (!block) return false;
|
||||
|
||||
if (!block) return false
|
||||
|
||||
// These block types execute code and need properly formatted string literals
|
||||
const codeExecutionBlocks = ['function', 'condition'];
|
||||
|
||||
const codeExecutionBlocks = ['function', 'condition']
|
||||
|
||||
// Check if this is a block that executes code
|
||||
if (block.metadata?.id && codeExecutionBlocks.includes(block.metadata.id)) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// Check if the expression is likely part of code
|
||||
if (expression) {
|
||||
const codeIndicators = [
|
||||
// Function/method calls
|
||||
/\(\s*$/, // Function call
|
||||
/\.\w+\s*\(/, // Method call
|
||||
|
||||
/\(\s*$/, // Function call
|
||||
/\.\w+\s*\(/, // Method call
|
||||
|
||||
// JavaScript/Python operators
|
||||
/[=<>!+\-*/%](?:==?)?/, // Common operators
|
||||
/\+=|-=|\*=|\/=|%=|\*\*=?/, // Assignment operators
|
||||
|
||||
/[=<>!+\-*/%](?:==?)?/, // Common operators
|
||||
/\+=|-=|\*=|\/=|%=|\*\*=?/, // Assignment operators
|
||||
|
||||
// JavaScript keywords
|
||||
/\b(if|else|for|while|return|var|let|const|function)\b/,
|
||||
|
||||
|
||||
// Python keywords
|
||||
/\b(if|else|elif|for|while|def|return|import|from|as|class|with|try|except)\b/,
|
||||
|
||||
|
||||
// Common code patterns
|
||||
/^['"]use strict['"];?$/, // JS strict mode
|
||||
/\$\{.+?\}/, // JS template literals
|
||||
/f['"].*?['"]/, // Python f-strings
|
||||
/\bprint\s*\(/, // Python print
|
||||
/\bconsole\.\w+\(/ // JS console methods
|
||||
];
|
||||
|
||||
return codeIndicators.some(pattern => pattern.test(expression));
|
||||
/^['"]use strict['"];?$/, // JS strict mode
|
||||
/\$\{.+?\}/, // JS template literals
|
||||
/f['"].*?['"]/, // Python f-strings
|
||||
/\bprint\s*\(/, // Python print
|
||||
/\bconsole\.\w+\(/, // JS console methods
|
||||
]
|
||||
|
||||
return codeIndicators.some((pattern) => pattern.test(expression))
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,8 +305,8 @@ export class InputResolver {
|
||||
blockRef,
|
||||
pathParts,
|
||||
currentBlock: currentBlock.id,
|
||||
currentBlockType: currentBlock.metadata?.id
|
||||
});
|
||||
currentBlockType: currentBlock.metadata?.id,
|
||||
})
|
||||
|
||||
// Special case for "start" references
|
||||
// This allows users to reference the starter block using <start.response.type.input>
|
||||
@@ -311,80 +315,100 @@ export class InputResolver {
|
||||
// Find the starter block
|
||||
const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter')
|
||||
if (starterBlock) {
|
||||
logger.debug(`[resolveBlockReferences] Found starter block with ID: ${starterBlock.id}`);
|
||||
|
||||
logger.debug(`[resolveBlockReferences] Found starter block with ID: ${starterBlock.id}`)
|
||||
|
||||
const blockState = context.blockStates.get(starterBlock.id)
|
||||
if (blockState) {
|
||||
logger.debug(`[resolveBlockReferences] Starter block state:`, JSON.stringify(blockState, null, 2));
|
||||
|
||||
logger.debug(
|
||||
`[resolveBlockReferences] Starter block state:`,
|
||||
JSON.stringify(blockState, null, 2)
|
||||
)
|
||||
|
||||
// Navigate through the path parts
|
||||
let replacementValue: any = blockState.output
|
||||
|
||||
|
||||
// Log the initial output value from the starter block
|
||||
logger.debug(`[resolveBlockReferences] Initial starter output:`, JSON.stringify(replacementValue, null, 2));
|
||||
|
||||
logger.debug(
|
||||
`[resolveBlockReferences] Initial starter output:`,
|
||||
JSON.stringify(replacementValue, null, 2)
|
||||
)
|
||||
|
||||
for (const part of pathParts) {
|
||||
logger.debug(`[resolveBlockReferences] Navigating path part: ${part}`, {
|
||||
currentValue: typeof replacementValue === 'object' ?
|
||||
JSON.stringify(replacementValue) : replacementValue
|
||||
});
|
||||
|
||||
logger.debug(`[resolveBlockReferences] Navigating path part: ${part}`, {
|
||||
currentValue:
|
||||
typeof replacementValue === 'object'
|
||||
? JSON.stringify(replacementValue)
|
||||
: replacementValue,
|
||||
})
|
||||
|
||||
if (!replacementValue || typeof replacementValue !== 'object') {
|
||||
logger.warn(`[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`, replacementValue);
|
||||
logger.warn(
|
||||
`[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`,
|
||||
replacementValue
|
||||
)
|
||||
throw new Error(`Invalid path "${part}" in "${path}" for starter block.`)
|
||||
}
|
||||
|
||||
|
||||
replacementValue = replacementValue[part]
|
||||
|
||||
|
||||
if (replacementValue === undefined) {
|
||||
logger.warn(`[resolveBlockReferences] No value found at path "${part}" in starter block.`);
|
||||
logger.warn(
|
||||
`[resolveBlockReferences] No value found at path "${part}" in starter block.`
|
||||
)
|
||||
throw new Error(`No value found at path "${path}" in starter block.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Format the value based on block type and path
|
||||
let formattedValue: string;
|
||||
|
||||
let formattedValue: string
|
||||
|
||||
// Special handling for all blocks referencing starter input
|
||||
if (blockRef.toLowerCase() === 'start' && pathParts.join('.').includes('input')) {
|
||||
const blockType = currentBlock.metadata?.id;
|
||||
|
||||
const blockType = currentBlock.metadata?.id
|
||||
|
||||
// Format based on which block is consuming this value
|
||||
if (typeof replacementValue === 'object' && replacementValue !== null) {
|
||||
// For function blocks, preserve the object structure for code usage
|
||||
if (blockType === 'function') {
|
||||
logger.debug(`[resolveBlockReferences] Special handling for function input:`,
|
||||
JSON.stringify(replacementValue, null, 2));
|
||||
formattedValue = JSON.stringify(replacementValue);
|
||||
logger.debug(
|
||||
`[resolveBlockReferences] Special handling for function input:`,
|
||||
JSON.stringify(replacementValue, null, 2)
|
||||
)
|
||||
formattedValue = JSON.stringify(replacementValue)
|
||||
}
|
||||
// For API blocks, handle body special case
|
||||
else if (blockType === 'api') {
|
||||
logger.debug(`[resolveBlockReferences] Special handling for API input:`,
|
||||
JSON.stringify(replacementValue, null, 2));
|
||||
formattedValue = JSON.stringify(replacementValue);
|
||||
}
|
||||
logger.debug(
|
||||
`[resolveBlockReferences] Special handling for API input:`,
|
||||
JSON.stringify(replacementValue, null, 2)
|
||||
)
|
||||
formattedValue = JSON.stringify(replacementValue)
|
||||
}
|
||||
// For condition blocks, ensure proper formatting
|
||||
else if (blockType === 'condition') {
|
||||
logger.debug(`[resolveBlockReferences] Special handling for condition input:`,
|
||||
JSON.stringify(replacementValue, null, 2));
|
||||
formattedValue = this.stringifyForCondition(replacementValue);
|
||||
logger.debug(
|
||||
`[resolveBlockReferences] Special handling for condition input:`,
|
||||
JSON.stringify(replacementValue, null, 2)
|
||||
)
|
||||
formattedValue = this.stringifyForCondition(replacementValue)
|
||||
}
|
||||
// For all other blocks, stringify objects
|
||||
else {
|
||||
formattedValue = JSON.stringify(replacementValue);
|
||||
formattedValue = JSON.stringify(replacementValue)
|
||||
}
|
||||
} else {
|
||||
// For primitive values
|
||||
formattedValue = String(replacementValue);
|
||||
formattedValue = String(replacementValue)
|
||||
}
|
||||
} else {
|
||||
// Standard handling for non-input references
|
||||
formattedValue = typeof replacementValue === 'object'
|
||||
? JSON.stringify(replacementValue)
|
||||
: String(replacementValue);
|
||||
formattedValue =
|
||||
typeof replacementValue === 'object'
|
||||
? JSON.stringify(replacementValue)
|
||||
: String(replacementValue)
|
||||
}
|
||||
|
||||
logger.debug(`[resolveBlockReferences] Resolved value:`, formattedValue);
|
||||
|
||||
logger.debug(`[resolveBlockReferences] Resolved value:`, formattedValue)
|
||||
resolvedValue = resolvedValue.replace(match, formattedValue)
|
||||
continue
|
||||
}
|
||||
@@ -578,7 +602,10 @@ export class InputResolver {
|
||||
|
||||
if (currentBlock.metadata?.id === 'condition') {
|
||||
formattedValue = this.stringifyForCondition(replacementValue)
|
||||
} else if (typeof replacementValue === 'string' && this.needsCodeStringLiteral(currentBlock, value)) {
|
||||
} else if (
|
||||
typeof replacementValue === 'string' &&
|
||||
this.needsCodeStringLiteral(currentBlock, value)
|
||||
) {
|
||||
// For code blocks, quote string values properly for the given language
|
||||
formattedValue = JSON.stringify(replacementValue)
|
||||
} else {
|
||||
|
||||
+9
-2
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from 'react'
|
||||
import {
|
||||
AirtableIcon,
|
||||
ConfluenceIcon,
|
||||
GithubIcon,
|
||||
GmailIcon,
|
||||
@@ -10,14 +11,20 @@ import {
|
||||
GoogleSheetsIcon,
|
||||
SupabaseIcon,
|
||||
xIcon,
|
||||
AirtableIcon,
|
||||
} from '@/components/icons'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
const logger = createLogger('OAuth')
|
||||
|
||||
// Define the base OAuth provider type
|
||||
export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | 'confluence' | 'airtable' | string
|
||||
export type OAuthProvider =
|
||||
| 'google'
|
||||
| 'github'
|
||||
| 'x'
|
||||
| 'supabase'
|
||||
| 'confluence'
|
||||
| 'airtable'
|
||||
| string
|
||||
export type OAuthService =
|
||||
| 'google'
|
||||
| 'google-email'
|
||||
|
||||
@@ -59,7 +59,7 @@ export const useSubBlockStore = create<SubBlockStore>()(
|
||||
// Persist to localStorage for backup
|
||||
const currentValues = get().workflowValues[activeWorkflowId] || {}
|
||||
saveSubblockValues(activeWorkflowId, currentValues)
|
||||
|
||||
|
||||
// Trigger debounced sync to DB
|
||||
get().syncWithDB()
|
||||
},
|
||||
@@ -83,7 +83,7 @@ export const useSubBlockStore = create<SubBlockStore>()(
|
||||
}))
|
||||
|
||||
saveSubblockValues(activeWorkflowId, {})
|
||||
|
||||
|
||||
// Trigger sync to DB immediately on clear
|
||||
workflowSync.sync()
|
||||
},
|
||||
@@ -121,14 +121,14 @@ export const useSubBlockStore = create<SubBlockStore>()(
|
||||
// Save to localStorage
|
||||
saveSubblockValues(workflowId, values)
|
||||
},
|
||||
|
||||
|
||||
// Debounced sync function to trigger DB sync
|
||||
syncWithDB: () => {
|
||||
// Clear any existing timeout
|
||||
if (syncDebounceTimer) {
|
||||
clearTimeout(syncDebounceTimer)
|
||||
}
|
||||
|
||||
|
||||
// Set new timeout
|
||||
syncDebounceTimer = setTimeout(() => {
|
||||
// Trigger workflow sync to DB
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { ToolTester } from '../__test-utils__/test-tools'
|
||||
import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './index'
|
||||
import { airtableReadTool, airtableUpdateTool, airtableWriteTool } from './index'
|
||||
|
||||
describe('Airtable Tools Integration', () => {
|
||||
let tester: ToolTester
|
||||
@@ -30,7 +30,7 @@ describe('Airtable Tools Integration', () => {
|
||||
tableId: 'table456',
|
||||
accessToken: 'token789',
|
||||
maxRecords: 100,
|
||||
filterFormula: "Status='Active'"
|
||||
filterFormula: "Status='Active'",
|
||||
}
|
||||
|
||||
const url = tester.getRequestUrl(params)
|
||||
@@ -46,9 +46,9 @@ describe('Airtable Tools Integration', () => {
|
||||
const mockData = {
|
||||
records: [
|
||||
{ id: 'rec1', fields: { Name: 'Test 1' } },
|
||||
{ id: 'rec2', fields: { Name: 'Test 2' } }
|
||||
{ id: 'rec2', fields: { Name: 'Test 2' } },
|
||||
],
|
||||
offset: 'next_page_token'
|
||||
offset: 'next_page_token',
|
||||
}
|
||||
|
||||
tester.setup(mockData)
|
||||
@@ -56,7 +56,7 @@ describe('Airtable Tools Integration', () => {
|
||||
const result = await tester.execute({
|
||||
baseId: 'base123',
|
||||
tableId: 'table456',
|
||||
accessToken: 'token789'
|
||||
accessToken: 'token789',
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
@@ -76,7 +76,7 @@ describe('Airtable Tools Integration', () => {
|
||||
baseId: 'base123',
|
||||
tableId: 'table456',
|
||||
accessToken: 'token789',
|
||||
records: [{ fields: { Name: 'New Record' } }]
|
||||
records: [{ fields: { Name: 'New Record' } }],
|
||||
}
|
||||
|
||||
const url = tester.getRequestUrl(params)
|
||||
@@ -90,9 +90,7 @@ describe('Airtable Tools Integration', () => {
|
||||
|
||||
test('should handle successful write response', async () => {
|
||||
const mockData = {
|
||||
records: [
|
||||
{ id: 'rec1', fields: { Name: 'New Record' } }
|
||||
]
|
||||
records: [{ id: 'rec1', fields: { Name: 'New Record' } }],
|
||||
}
|
||||
|
||||
tester.setup(mockData)
|
||||
@@ -101,7 +99,7 @@ describe('Airtable Tools Integration', () => {
|
||||
baseId: 'base123',
|
||||
tableId: 'table456',
|
||||
accessToken: 'token789',
|
||||
records: [{ fields: { Name: 'New Record' } }]
|
||||
records: [{ fields: { Name: 'New Record' } }],
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
@@ -121,7 +119,7 @@ describe('Airtable Tools Integration', () => {
|
||||
tableId: 'table456',
|
||||
recordId: 'rec789',
|
||||
accessToken: 'token789',
|
||||
fields: { Name: 'Updated Record' }
|
||||
fields: { Name: 'Updated Record' },
|
||||
}
|
||||
|
||||
const url = tester.getRequestUrl(params)
|
||||
@@ -136,7 +134,7 @@ describe('Airtable Tools Integration', () => {
|
||||
test('should handle successful update response', async () => {
|
||||
const mockData = {
|
||||
id: 'rec789',
|
||||
fields: { Name: 'Updated Record' }
|
||||
fields: { Name: 'Updated Record' },
|
||||
}
|
||||
|
||||
tester.setup(mockData)
|
||||
@@ -146,7 +144,7 @@ describe('Airtable Tools Integration', () => {
|
||||
tableId: 'table456',
|
||||
recordId: 'rec789',
|
||||
accessToken: 'token789',
|
||||
fields: { Name: 'Updated Record' }
|
||||
fields: { Name: 'Updated Record' },
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
@@ -158,17 +156,17 @@ describe('Airtable Tools Integration', () => {
|
||||
|
||||
test('should handle error responses', async () => {
|
||||
tester = new ToolTester(airtableReadTool)
|
||||
|
||||
|
||||
const errorMessage = 'Invalid API key'
|
||||
tester.setup({ error: errorMessage }, { ok: false, status: 401 })
|
||||
|
||||
const result = await tester.execute({
|
||||
baseId: 'base123',
|
||||
tableId: 'table456',
|
||||
accessToken: 'invalid_token'
|
||||
accessToken: 'invalid_token',
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('Failed to read Airtable records')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readTool } from './read'
|
||||
import { writeTool } from './write'
|
||||
import { updateTool } from './update'
|
||||
import { writeTool } from './write'
|
||||
|
||||
export const airtableReadTool = readTool
|
||||
export const airtableWriteTool = writeTool
|
||||
export const airtableUpdateTool = updateTool
|
||||
export const airtableUpdateTool = updateTool
|
||||
|
||||
@@ -6,12 +6,12 @@ export const readTool: ToolConfig<AirtableReadParams, AirtableReadResponse> = {
|
||||
name: 'Airtable Read Records',
|
||||
description: 'Read records from an Airtable table',
|
||||
version: '1.0.0',
|
||||
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -39,7 +39,7 @@ export const readTool: ToolConfig<AirtableReadParams, AirtableReadResponse> = {
|
||||
description: 'Formula to filter records',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = `https://api.airtable.com/v0/${params.baseId}/${params.tableId}`
|
||||
@@ -54,11 +54,11 @@ export const readTool: ToolConfig<AirtableReadParams, AirtableReadResponse> = {
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'Authorization': `Bearer ${params.accessToken}`,
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
@@ -72,8 +72,8 @@ export const readTool: ToolConfig<AirtableReadParams, AirtableReadResponse> = {
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
transformError: (error) => {
|
||||
return `Failed to read Airtable records: ${error.message}`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,4 +66,4 @@ export interface AirtableUpdateParams {
|
||||
tableId: string
|
||||
recordId: string
|
||||
fields: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ export const writeTool: ToolConfig<AirtableWriteParams, AirtableWriteResponse> =
|
||||
name: 'Airtable Write Records',
|
||||
description: 'Write new records to an Airtable table',
|
||||
version: '1.0.0',
|
||||
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -34,18 +34,17 @@ export const writeTool: ToolConfig<AirtableWriteParams, AirtableWriteResponse> =
|
||||
description: 'Array of records to create',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.airtable.com/v0/${params.baseId}/${params.tableId}`,
|
||||
url: (params) => `https://api.airtable.com/v0/${params.baseId}/${params.tableId}`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Authorization': `Bearer ${params.accessToken}`,
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({ records: params.records }),
|
||||
},
|
||||
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
@@ -58,8 +57,8 @@ export const writeTool: ToolConfig<AirtableWriteParams, AirtableWriteResponse> =
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
transformError: (error) => {
|
||||
return `Failed to write Airtable records: ${error.message}`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { fileParserTool } from './parser'
|
||||
|
||||
export const fileParseTool = fileParserTool
|
||||
export const fileParseTool = fileParserTool
|
||||
|
||||
+2
-2
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment/store'
|
||||
import { airtableReadTool, airtableUpdateTool, airtableWriteTool } from './airtable'
|
||||
import { confluenceListTool, confluenceRetrieveTool, confluenceUpdateTool } from './confluence'
|
||||
import { docsCreateTool, docsReadTool, docsWriteTool } from './docs'
|
||||
import { driveDownloadTool, driveListTool, driveUploadTool } from './drive'
|
||||
@@ -37,6 +38,7 @@ import { sheetsReadTool, sheetsUpdateTool, sheetsWriteTool } from './sheets'
|
||||
import { slackMessageTool } from './slack/message'
|
||||
import { supabaseInsertTool, supabaseQueryTool, supabaseUpdateTool } from './supabase'
|
||||
import { tavilyExtractTool, tavilySearchTool } from './tavily'
|
||||
import { sendSMSTool } from './twilio/send'
|
||||
import { typeformFilesTool, typeformInsightsTool, typeformResponsesTool } from './typeform'
|
||||
import { OAuthTokenPayload, ToolConfig, ToolResponse } from './types'
|
||||
import { formatRequestParams, validateToolRequest } from './utils'
|
||||
@@ -44,8 +46,6 @@ import { visionTool } from './vision/vision'
|
||||
import { whatsappSendMessageTool } from './whatsapp'
|
||||
import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
|
||||
import { youtubeSearchTool } from './youtube/search'
|
||||
import { sendSMSTool } from './twilio/sendSMS'
|
||||
import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './airtable'
|
||||
|
||||
const logger = createLogger('Tools')
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { sendSMSTool } from './sendSMS'
|
||||
import { sendSMSTool } from './send'
|
||||
|
||||
export const twilioSendSMSTool = sendSMSTool
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { ToolConfig } from '../types'
|
||||
import { TwilioSMSBlockOutput, TwilioSendSMSParams } from './types'
|
||||
import { TwilioSendSMSParams, TwilioSMSBlockOutput } from './types'
|
||||
|
||||
const logger = createLogger('Twilio Send SMS Tool')
|
||||
const logger = createLogger('Twilio Send SMS Tool')
|
||||
|
||||
export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput> = {
|
||||
id: 'twilio_send_sms',
|
||||
@@ -14,30 +14,30 @@ export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput>
|
||||
phoneNumbers: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Phone numbers to send the message to, separated by newlines'
|
||||
description: 'Phone numbers to send the message to, separated by newlines',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Message to send'
|
||||
description: 'Message to send',
|
||||
},
|
||||
accountSid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Twilio Account SID',
|
||||
requiredForToolCall: true
|
||||
requiredForToolCall: true,
|
||||
},
|
||||
authToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Twilio Auth Token',
|
||||
requiredForToolCall: true
|
||||
requiredForToolCall: true,
|
||||
},
|
||||
fromNumber: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Twilio phone number to send the message from'
|
||||
}
|
||||
description: 'Twilio phone number to send the message from',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -45,7 +45,7 @@ export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput>
|
||||
if (!params.accountSid) {
|
||||
throw new Error('Twilio Account SID is required')
|
||||
}
|
||||
const url = `https://api.twilio.com/2010-04-01/Accounts/${params.accountSid}/Messages.json`;
|
||||
const url = `https://api.twilio.com/2010-04-01/Accounts/${params.accountSid}/Messages.json`
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
@@ -57,7 +57,7 @@ export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput>
|
||||
const authToken = Buffer.from(`${params.accountSid}:${params.authToken}`).toString('base64')
|
||||
const headers = {
|
||||
Authorization: `Basic ${authToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
return headers
|
||||
},
|
||||
@@ -74,27 +74,28 @@ export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput>
|
||||
|
||||
// Get first phone number if multiple are provided
|
||||
const toNumber = params.phoneNumbers.split('\n')[0].trim()
|
||||
|
||||
|
||||
// Create a URLSearchParams object and convert to string
|
||||
const formData = new URLSearchParams()
|
||||
formData.append('To', toNumber)
|
||||
formData.append('From', params.fromNumber)
|
||||
formData.append('Body', params.message)
|
||||
|
||||
|
||||
const formDataString = formData.toString()
|
||||
return { body: formDataString }
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data.error?.message || data.message || `Failed to send SMS (HTTP ${response.status})`
|
||||
const errorMessage =
|
||||
data.error?.message || data.message || `Failed to send SMS (HTTP ${response.status})`
|
||||
logger.error('Twilio API error:', data)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
|
||||
logger.info('Twilio Response:', data)
|
||||
logger.info('Twilio Response type:', typeof data)
|
||||
return {
|
||||
@@ -102,14 +103,14 @@ export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput>
|
||||
output: {
|
||||
success: true,
|
||||
messageId: data.sid,
|
||||
status: data.status
|
||||
status: data.status,
|
||||
},
|
||||
error: undefined
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
logger.error('Twilio tool error:', { error })
|
||||
return `SMS sending failed: ${error.message || 'Unknown error occurred'}`
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -10,9 +10,9 @@ export interface TwilioSendSMSParams {
|
||||
|
||||
export interface TwilioSMSBlockOutput extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
messageId?: string
|
||||
status?: string
|
||||
error?: string
|
||||
success: boolean
|
||||
messageId?: string
|
||||
status?: string
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('Typeform Files Tool', () => {
|
||||
// Mock file response
|
||||
const mockFileResponseHeaders = {
|
||||
'content-type': 'application/pdf',
|
||||
'content-disposition': 'attachment; filename="test-file.pdf"'
|
||||
'content-disposition': 'attachment; filename="test-file.pdf"',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -35,7 +35,7 @@ describe('Typeform Files Tool', () => {
|
||||
responseId: 'resp456',
|
||||
fieldId: 'field789',
|
||||
filename: 'test-file.pdf',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
}
|
||||
|
||||
expect(tester.getRequestUrl(params)).toBe(
|
||||
@@ -50,7 +50,7 @@ describe('Typeform Files Tool', () => {
|
||||
fieldId: 'field789',
|
||||
filename: 'test-file.pdf',
|
||||
inline: true,
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
}
|
||||
|
||||
const url = tester.getRequestUrl(params)
|
||||
@@ -63,7 +63,7 @@ describe('Typeform Files Tool', () => {
|
||||
responseId: 'resp&with#chars',
|
||||
fieldId: 'field-id',
|
||||
filename: 'file name.pdf',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
}
|
||||
|
||||
const url = tester.getRequestUrl(params)
|
||||
@@ -80,7 +80,7 @@ describe('Typeform Files Tool', () => {
|
||||
responseId: 'resp456',
|
||||
fieldId: 'field789',
|
||||
filename: 'test-file.pdf',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
}
|
||||
|
||||
const headers = tester.getRequestHeaders(params)
|
||||
@@ -93,7 +93,7 @@ describe('Typeform Files Tool', () => {
|
||||
test('should transform file data correctly', async () => {
|
||||
// Setup mock response for binary file data
|
||||
tester.setup('file-content-binary-data', {
|
||||
headers: mockFileResponseHeaders
|
||||
headers: mockFileResponseHeaders,
|
||||
})
|
||||
|
||||
// Execute the tool
|
||||
@@ -102,7 +102,7 @@ describe('Typeform Files Tool', () => {
|
||||
responseId: 'resp456',
|
||||
fieldId: 'field789',
|
||||
filename: 'test-file.pdf',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
})
|
||||
|
||||
// Check the result
|
||||
@@ -115,7 +115,7 @@ describe('Typeform Files Tool', () => {
|
||||
test('should handle missing content-disposition header', async () => {
|
||||
// Setup mock response without content-disposition
|
||||
tester.setup('file-content-binary-data', {
|
||||
headers: { 'content-type': 'application/pdf' }
|
||||
headers: { 'content-type': 'application/pdf' },
|
||||
})
|
||||
|
||||
// Execute the tool
|
||||
@@ -124,7 +124,7 @@ describe('Typeform Files Tool', () => {
|
||||
responseId: 'resp456',
|
||||
fieldId: 'field789',
|
||||
filename: 'test-file.pdf',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
})
|
||||
|
||||
// Check the result
|
||||
@@ -147,7 +147,7 @@ describe('Typeform Files Tool', () => {
|
||||
responseId: 'resp456',
|
||||
fieldId: 'field789',
|
||||
filename: 'nonexistent.pdf',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
})
|
||||
|
||||
// Check error handling
|
||||
@@ -165,7 +165,7 @@ describe('Typeform Files Tool', () => {
|
||||
responseId: 'resp456',
|
||||
fieldId: 'field789',
|
||||
filename: 'test-file.pdf',
|
||||
apiKey: 'invalid-token'
|
||||
apiKey: 'invalid-token',
|
||||
})
|
||||
|
||||
// Check error handling
|
||||
@@ -183,7 +183,7 @@ describe('Typeform Files Tool', () => {
|
||||
responseId: 'resp456',
|
||||
fieldId: 'field789',
|
||||
filename: 'test-file.pdf',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
})
|
||||
|
||||
// Check error handling
|
||||
@@ -191,4 +191,4 @@ describe('Typeform Files Tool', () => {
|
||||
expect(result.error).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+37
-37
@@ -60,88 +60,88 @@ export const filesTool: ToolConfig<TypeformFilesParams, TypeformFilesResponse> =
|
||||
const encodedResponseId = encodeURIComponent(params.responseId)
|
||||
const encodedFieldId = encodeURIComponent(params.fieldId)
|
||||
const encodedFilename = encodeURIComponent(params.filename)
|
||||
|
||||
|
||||
let url = `https://api.typeform.com/forms/${encodedFormId}/responses/${encodedResponseId}/fields/${encodedFieldId}/files/${encodedFilename}`
|
||||
|
||||
|
||||
// Add the inline parameter if provided
|
||||
if (params.inline !== undefined) {
|
||||
url += `?inline=${params.inline}`
|
||||
}
|
||||
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'Authorization': `Bearer ${params.apiKey}`,
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
transformResponse: async (response: Response, params?: TypeformFilesParams) => {
|
||||
if (!response.ok) {
|
||||
let errorMessage = response.statusText || 'Unknown error';
|
||||
|
||||
let errorMessage = response.statusText || 'Unknown error'
|
||||
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
const errorData = await response.json()
|
||||
if (errorData && errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
errorMessage = errorData.message
|
||||
} else if (errorData && errorData.description) {
|
||||
errorMessage = errorData.description;
|
||||
errorMessage = errorData.description
|
||||
} else if (typeof errorData === 'string') {
|
||||
errorMessage = errorData;
|
||||
errorMessage = errorData
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't parse the error as JSON, just use the status text
|
||||
}
|
||||
|
||||
throw new Error(`Typeform API error (${response.status}): ${errorMessage}`);
|
||||
|
||||
throw new Error(`Typeform API error (${response.status}): ${errorMessage}`)
|
||||
}
|
||||
|
||||
|
||||
// For file downloads, we get the file directly
|
||||
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
||||
const contentDisposition = response.headers.get('content-disposition') || '';
|
||||
|
||||
const contentType = response.headers.get('content-type') || 'application/octet-stream'
|
||||
const contentDisposition = response.headers.get('content-disposition') || ''
|
||||
|
||||
// Try to extract filename from content-disposition if possible
|
||||
let filename = '';
|
||||
const filenameMatch = contentDisposition.match(/filename="(.+?)"/);
|
||||
let filename = ''
|
||||
const filenameMatch = contentDisposition.match(/filename="(.+?)"/)
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = filenameMatch[1];
|
||||
filename = filenameMatch[1]
|
||||
}
|
||||
|
||||
|
||||
// Get file URL from the response URL or construct it from parameters if not available
|
||||
let fileUrl = response.url;
|
||||
|
||||
let fileUrl = response.url
|
||||
|
||||
// If the response URL is not available (common in test environments), construct it from params
|
||||
if (!fileUrl && params) {
|
||||
const encodedFormId = encodeURIComponent(params.formId);
|
||||
const encodedResponseId = encodeURIComponent(params.responseId);
|
||||
const encodedFieldId = encodeURIComponent(params.fieldId);
|
||||
const encodedFilename = encodeURIComponent(params.filename);
|
||||
|
||||
fileUrl = `https://api.typeform.com/forms/${encodedFormId}/responses/${encodedResponseId}/fields/${encodedFieldId}/files/${encodedFilename}`;
|
||||
|
||||
const encodedFormId = encodeURIComponent(params.formId)
|
||||
const encodedResponseId = encodeURIComponent(params.responseId)
|
||||
const encodedFieldId = encodeURIComponent(params.fieldId)
|
||||
const encodedFilename = encodeURIComponent(params.filename)
|
||||
|
||||
fileUrl = `https://api.typeform.com/forms/${encodedFormId}/responses/${encodedResponseId}/fields/${encodedFieldId}/files/${encodedFilename}`
|
||||
|
||||
if (params.inline !== undefined) {
|
||||
fileUrl += `?inline=${params.inline}`;
|
||||
fileUrl += `?inline=${params.inline}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
fileUrl: fileUrl || '',
|
||||
contentType,
|
||||
filename
|
||||
}
|
||||
};
|
||||
filename,
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
return `Failed to retrieve Typeform file: ${error.message}`
|
||||
}
|
||||
|
||||
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
return `Failed to retrieve Typeform file: ${JSON.stringify(error)}`
|
||||
}
|
||||
|
||||
|
||||
return `Failed to retrieve Typeform file: An unknown error occurred`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,26 +7,26 @@
|
||||
* work correctly together and can be properly used from the block.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { typeformFilesTool, typeformResponsesTool, typeformInsightsTool } from './index'
|
||||
import { ToolTester } from '../__test-utils__/test-tools'
|
||||
import { typeformFilesTool, typeformInsightsTool, typeformResponsesTool } from './index'
|
||||
|
||||
describe('Typeform Tools Integration', () => {
|
||||
describe('Typeform Responses Tool Export', () => {
|
||||
let tester: ToolTester
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
tester = new ToolTester(typeformResponsesTool)
|
||||
})
|
||||
|
||||
|
||||
afterEach(() => {
|
||||
tester.cleanup()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
|
||||
test('should use the correct tool ID', () => {
|
||||
expect(typeformResponsesTool.id).toBe('typeform_responses')
|
||||
})
|
||||
|
||||
|
||||
test('should handle basic responses request', async () => {
|
||||
// Setup mock response data
|
||||
const mockData = {
|
||||
@@ -41,45 +41,45 @@ describe('Typeform Tools Integration', () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
tester.setup(mockData)
|
||||
|
||||
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
formId: 'test-form',
|
||||
apiKey: 'test-api-key',
|
||||
})
|
||||
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.total_items).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('Typeform Files Tool Export', () => {
|
||||
let tester: ToolTester
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
tester = new ToolTester(typeformFilesTool)
|
||||
})
|
||||
|
||||
|
||||
afterEach(() => {
|
||||
tester.cleanup()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
|
||||
test('should use the correct tool ID', () => {
|
||||
expect(typeformFilesTool.id).toBe('typeform_files')
|
||||
})
|
||||
|
||||
|
||||
test('should handle basic file request', async () => {
|
||||
// Setup mock response with file headers
|
||||
tester.setup('binary-file-content', {
|
||||
headers: {
|
||||
'content-type': 'application/pdf',
|
||||
'content-disposition': 'attachment; filename="test.pdf"'
|
||||
}
|
||||
'content-disposition': 'attachment; filename="test.pdf"',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
formId: 'test-form',
|
||||
@@ -88,29 +88,29 @@ describe('Typeform Tools Integration', () => {
|
||||
filename: 'test.pdf',
|
||||
apiKey: 'test-api-key',
|
||||
})
|
||||
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.contentType).toBe('application/pdf')
|
||||
expect(result.output.filename).toBe('test.pdf')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('Typeform Insights Tool Export', () => {
|
||||
let tester: ToolTester
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
tester = new ToolTester(typeformInsightsTool)
|
||||
})
|
||||
|
||||
|
||||
afterEach(() => {
|
||||
tester.cleanup()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
|
||||
test('should use the correct tool ID', () => {
|
||||
expect(typeformInsightsTool.id).toBe('typeform_insights')
|
||||
})
|
||||
|
||||
|
||||
test('should handle basic insights request', async () => {
|
||||
// Setup mock response data
|
||||
const mockData = {
|
||||
@@ -122,8 +122,8 @@ describe('Typeform Tools Integration', () => {
|
||||
ref: 'ref123',
|
||||
title: 'What is your name?',
|
||||
type: 'short_text',
|
||||
views: 100
|
||||
}
|
||||
views: 100,
|
||||
},
|
||||
],
|
||||
form: {
|
||||
platforms: [
|
||||
@@ -133,40 +133,40 @@ describe('Typeform Tools Integration', () => {
|
||||
platform: 'desktop',
|
||||
responses_count: 80,
|
||||
total_visits: 120,
|
||||
unique_visits: 100
|
||||
}
|
||||
unique_visits: 100,
|
||||
},
|
||||
],
|
||||
summary: {
|
||||
average_time: 140000,
|
||||
completion_rate: 72.3,
|
||||
responses_count: 120,
|
||||
total_visits: 180,
|
||||
unique_visits: 150
|
||||
}
|
||||
}
|
||||
unique_visits: 150,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
tester.setup(mockData)
|
||||
|
||||
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
formId: 'test-form',
|
||||
apiKey: 'test-api-key',
|
||||
})
|
||||
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.form.summary.responses_count).toBe(120)
|
||||
expect(result.output.fields).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('End-to-End Flow', () => {
|
||||
// This test simulates using both tools together in a workflow
|
||||
|
||||
|
||||
test('should be able to get responses and then file', async () => {
|
||||
// First set up responses tester
|
||||
const responsesTester = new ToolTester(typeformResponsesTool)
|
||||
|
||||
|
||||
// Mock responses data with a file upload
|
||||
const mockResponsesData = {
|
||||
total_items: 1,
|
||||
@@ -189,35 +189,35 @@ describe('Typeform Tools Integration', () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
responsesTester.setup(mockResponsesData)
|
||||
|
||||
|
||||
// Get responses
|
||||
const responsesResult = await responsesTester.execute({
|
||||
formId: 'test-form',
|
||||
apiKey: 'test-api-key',
|
||||
})
|
||||
|
||||
|
||||
expect(responsesResult.success).toBe(true)
|
||||
|
||||
|
||||
// Now get the response ID and field ID
|
||||
const responseId = responsesResult.output.items[0].token
|
||||
expect(responseId).toBe('response-id')
|
||||
|
||||
|
||||
const fieldId = responsesResult.output.items[0].answers[0].field.id
|
||||
expect(fieldId).toBe('file-field')
|
||||
|
||||
|
||||
// Now set up files tester
|
||||
const filesTester = new ToolTester(typeformFilesTool)
|
||||
|
||||
|
||||
// Mock file data
|
||||
filesTester.setup('binary-file-data', {
|
||||
headers: {
|
||||
'content-type': 'application/pdf',
|
||||
'content-disposition': 'attachment; filename="uploaded.pdf"'
|
||||
}
|
||||
'content-disposition': 'attachment; filename="uploaded.pdf"',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// Get file using the response ID and field ID from previous request
|
||||
const filesResult = await filesTester.execute({
|
||||
formId: 'test-form',
|
||||
@@ -226,20 +226,20 @@ describe('Typeform Tools Integration', () => {
|
||||
filename: 'uploaded.pdf',
|
||||
apiKey: 'test-api-key',
|
||||
})
|
||||
|
||||
|
||||
expect(filesResult.success).toBe(true)
|
||||
expect(filesResult.output.contentType).toBe('application/pdf')
|
||||
expect(filesResult.output.filename).toBe('uploaded.pdf')
|
||||
|
||||
|
||||
// Clean up
|
||||
responsesTester.cleanup()
|
||||
filesTester.cleanup()
|
||||
})
|
||||
|
||||
|
||||
test('should be able to get responses and then insights', async () => {
|
||||
// First set up responses tester
|
||||
const responsesTester = new ToolTester(typeformResponsesTool)
|
||||
|
||||
|
||||
// Mock responses data
|
||||
const mockResponsesData = {
|
||||
total_items: 10,
|
||||
@@ -253,21 +253,21 @@ describe('Typeform Tools Integration', () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
responsesTester.setup(mockResponsesData)
|
||||
|
||||
|
||||
// Get responses
|
||||
const responsesResult = await responsesTester.execute({
|
||||
formId: 'test-form',
|
||||
apiKey: 'test-api-key',
|
||||
})
|
||||
|
||||
|
||||
expect(responsesResult.success).toBe(true)
|
||||
expect(responsesResult.output.total_items).toBe(10)
|
||||
|
||||
|
||||
// Now set up insights tester
|
||||
const insightsTester = new ToolTester(typeformInsightsTool)
|
||||
|
||||
|
||||
// Mock insights data
|
||||
const mockInsightsData = {
|
||||
fields: [
|
||||
@@ -278,8 +278,8 @@ describe('Typeform Tools Integration', () => {
|
||||
ref: 'ref123',
|
||||
title: 'What is your name?',
|
||||
type: 'short_text',
|
||||
views: 100
|
||||
}
|
||||
views: 100,
|
||||
},
|
||||
],
|
||||
form: {
|
||||
platforms: [
|
||||
@@ -289,37 +289,37 @@ describe('Typeform Tools Integration', () => {
|
||||
platform: 'desktop',
|
||||
responses_count: 80,
|
||||
total_visits: 120,
|
||||
unique_visits: 100
|
||||
}
|
||||
unique_visits: 100,
|
||||
},
|
||||
],
|
||||
summary: {
|
||||
average_time: 140000,
|
||||
completion_rate: 72.3,
|
||||
responses_count: 120,
|
||||
total_visits: 180,
|
||||
unique_visits: 150
|
||||
}
|
||||
}
|
||||
unique_visits: 150,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
insightsTester.setup(mockInsightsData)
|
||||
|
||||
|
||||
// Get insights for the same form
|
||||
const insightsResult = await insightsTester.execute({
|
||||
formId: 'test-form',
|
||||
apiKey: 'test-api-key',
|
||||
})
|
||||
|
||||
|
||||
expect(insightsResult.success).toBe(true)
|
||||
expect(insightsResult.output.form.summary.responses_count).toBe(120)
|
||||
|
||||
|
||||
// Verify we can analyze the data by looking at completion rates
|
||||
expect(insightsResult.output.form.summary.completion_rate).toBe(72.3)
|
||||
expect(insightsResult.output.form.platforms[0].platform).toBe('desktop')
|
||||
|
||||
|
||||
// Clean up
|
||||
responsesTester.cleanup()
|
||||
insightsTester.cleanup()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { responsesTool } from './responses'
|
||||
import { filesTool } from './files'
|
||||
import { insightsTool } from './insights'
|
||||
import { responsesTool } from './responses'
|
||||
|
||||
export const typeformResponsesTool = responsesTool
|
||||
export const typeformFilesTool = filesTool
|
||||
export const typeformInsightsTool = insightsTool
|
||||
export const typeformInsightsTool = insightsTool
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('Typeform Insights Tool', () => {
|
||||
ref: 'ref123',
|
||||
title: 'What is your name?',
|
||||
type: 'short_text',
|
||||
views: 100
|
||||
views: 100,
|
||||
},
|
||||
{
|
||||
dropoffs: 10,
|
||||
@@ -32,8 +32,8 @@ describe('Typeform Insights Tool', () => {
|
||||
ref: 'ref456',
|
||||
title: 'How did you hear about us?',
|
||||
type: 'multiple_choice',
|
||||
views: 95
|
||||
}
|
||||
views: 95,
|
||||
},
|
||||
],
|
||||
form: {
|
||||
platforms: [
|
||||
@@ -43,7 +43,7 @@ describe('Typeform Insights Tool', () => {
|
||||
platform: 'desktop',
|
||||
responses_count: 80,
|
||||
total_visits: 120,
|
||||
unique_visits: 100
|
||||
unique_visits: 100,
|
||||
},
|
||||
{
|
||||
average_time: 180000,
|
||||
@@ -51,17 +51,17 @@ describe('Typeform Insights Tool', () => {
|
||||
platform: 'mobile',
|
||||
responses_count: 40,
|
||||
total_visits: 60,
|
||||
unique_visits: 50
|
||||
}
|
||||
unique_visits: 50,
|
||||
},
|
||||
],
|
||||
summary: {
|
||||
average_time: 140000,
|
||||
completion_rate: 72.3,
|
||||
responses_count: 120,
|
||||
total_visits: 180,
|
||||
unique_visits: 150
|
||||
}
|
||||
}
|
||||
unique_visits: 150,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -77,18 +77,16 @@ describe('Typeform Insights Tool', () => {
|
||||
test('should construct correct URL for insights endpoint', () => {
|
||||
const params = {
|
||||
formId: 'form123',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
}
|
||||
|
||||
expect(tester.getRequestUrl(params)).toBe(
|
||||
'https://api.typeform.com/insights/form123/summary'
|
||||
)
|
||||
expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/insights/form123/summary')
|
||||
})
|
||||
|
||||
test('should handle special characters in form ID', () => {
|
||||
const params = {
|
||||
formId: 'form/with/special?chars',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
}
|
||||
|
||||
const url = tester.getRequestUrl(params)
|
||||
@@ -102,7 +100,7 @@ describe('Typeform Insights Tool', () => {
|
||||
test('should include correct authorization header', () => {
|
||||
const params = {
|
||||
formId: 'form123',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
}
|
||||
|
||||
const headers = tester.getRequestHeaders(params)
|
||||
@@ -119,21 +117,21 @@ describe('Typeform Insights Tool', () => {
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
formId: 'form123',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
})
|
||||
|
||||
// Check the result
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
|
||||
// Verify form summary data
|
||||
expect(result.output.form.summary.responses_count).toBe(120)
|
||||
expect(result.output.form.summary.completion_rate).toBe(72.3)
|
||||
|
||||
|
||||
// Verify platforms data
|
||||
expect(result.output.form.platforms).toHaveLength(2)
|
||||
expect(result.output.form.platforms[0].platform).toBe('desktop')
|
||||
expect(result.output.form.platforms[1].platform).toBe('mobile')
|
||||
|
||||
|
||||
// Verify fields data
|
||||
expect(result.output.fields).toHaveLength(2)
|
||||
expect(result.output.fields[0].title).toBe('What is your name?')
|
||||
@@ -149,7 +147,7 @@ describe('Typeform Insights Tool', () => {
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
formId: 'nonexistent',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
})
|
||||
|
||||
// Check error handling
|
||||
@@ -164,7 +162,7 @@ describe('Typeform Insights Tool', () => {
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
formId: 'form123',
|
||||
apiKey: 'invalid-token'
|
||||
apiKey: 'invalid-token',
|
||||
})
|
||||
|
||||
// Check error handling
|
||||
@@ -179,7 +177,7 @@ describe('Typeform Insights Tool', () => {
|
||||
// Execute the tool
|
||||
const result = await tester.execute({
|
||||
formId: 'form123',
|
||||
apiKey: 'test-token'
|
||||
apiKey: 'test-token',
|
||||
})
|
||||
|
||||
// Check error handling
|
||||
@@ -187,4 +185,4 @@ describe('Typeform Insights Tool', () => {
|
||||
expect(result.error).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,32 +64,32 @@ export const insightsTool: ToolConfig<TypeformInsightsParams, TypeformInsightsRe
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'Authorization': `Bearer ${params.apiKey}`,
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
let errorMessage = response.statusText || 'Unknown error';
|
||||
let errorDetails = '';
|
||||
|
||||
let errorMessage = response.statusText || 'Unknown error'
|
||||
let errorDetails = ''
|
||||
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
console.log('Typeform API error response:', JSON.stringify(errorData, null, 2));
|
||||
|
||||
const errorData = await response.json()
|
||||
console.log('Typeform API error response:', JSON.stringify(errorData, null, 2))
|
||||
|
||||
if (errorData && errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
errorMessage = errorData.message
|
||||
} else if (errorData && errorData.description) {
|
||||
errorMessage = errorData.description;
|
||||
errorMessage = errorData.description
|
||||
} else if (typeof errorData === 'string') {
|
||||
errorMessage = errorData;
|
||||
errorMessage = errorData
|
||||
}
|
||||
|
||||
|
||||
// Extract more details if available
|
||||
if (errorData && errorData.details) {
|
||||
errorDetails = ` Details: ${JSON.stringify(errorData.details)}`;
|
||||
errorDetails = ` Details: ${JSON.stringify(errorData.details)}`
|
||||
}
|
||||
|
||||
|
||||
// Special handling for 403 errors
|
||||
if (response.status === 403) {
|
||||
return {
|
||||
@@ -100,33 +100,33 @@ export const insightsTool: ToolConfig<TypeformInsightsParams, TypeformInsightsRe
|
||||
2. Insufficient plan subscription (insights may require a higher plan)
|
||||
3. No access rights to the specified form
|
||||
4. API token is invalid or expired
|
||||
Details from API: ${errorMessage}${errorDetails}`
|
||||
};
|
||||
Details from API: ${errorMessage}${errorDetails}`,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't parse the error as JSON, just use the status text
|
||||
console.log('Error parsing Typeform API error:', e);
|
||||
console.log('Error parsing Typeform API error:', e)
|
||||
}
|
||||
|
||||
throw new Error(`Typeform API error (${response.status}): ${errorMessage}${errorDetails}`);
|
||||
|
||||
throw new Error(`Typeform API error (${response.status}): ${errorMessage}${errorDetails}`)
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data
|
||||
};
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
return `Failed to retrieve Typeform insights: ${error.message}`
|
||||
}
|
||||
|
||||
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
return `Failed to retrieve Typeform insights: ${JSON.stringify(error)}`
|
||||
}
|
||||
|
||||
|
||||
return `Failed to retrieve Typeform insights: An unknown error occurred`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,7 @@ describe('Typeform Responses Tool', () => {
|
||||
apiKey: 'test-token',
|
||||
}
|
||||
|
||||
expect(tester.getRequestUrl(params)).toBe(
|
||||
'https://api.typeform.com/forms/form123/responses'
|
||||
)
|
||||
expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/forms/form123/responses')
|
||||
})
|
||||
|
||||
test('should add pageSize parameter to URL when provided', () => {
|
||||
@@ -155,9 +153,7 @@ describe('Typeform Responses Tool', () => {
|
||||
completed: 'all',
|
||||
}
|
||||
|
||||
expect(tester.getRequestUrl(params)).toBe(
|
||||
'https://api.typeform.com/forms/form123/responses'
|
||||
)
|
||||
expect(tester.getRequestUrl(params)).toBe('https://api.typeform.com/forms/form123/responses')
|
||||
})
|
||||
|
||||
test('should combine multiple parameters correctly', () => {
|
||||
@@ -207,13 +203,13 @@ describe('Typeform Responses Tool', () => {
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.total_items).toBe(2)
|
||||
expect(result.output.items).toHaveLength(2)
|
||||
|
||||
|
||||
// Check first response
|
||||
const firstResponse = result.output.items[0]
|
||||
expect(firstResponse.token).toBe('response-id-1')
|
||||
expect(firstResponse.answers).toHaveLength(1)
|
||||
expect(firstResponse.answers[0].text).toBe('Sample answer')
|
||||
|
||||
|
||||
// Check second response
|
||||
const secondResponse = result.output.items[1]
|
||||
expect(secondResponse.token).toBe('response-id-2')
|
||||
@@ -266,4 +262,4 @@ describe('Typeform Responses Tool', () => {
|
||||
expect(result.error).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -87,73 +87,75 @@ export const responsesTool: ToolConfig<TypeformResponsesParams, TypeformResponse
|
||||
request: {
|
||||
url: (params: TypeformResponsesParams) => {
|
||||
const url = `https://api.typeform.com/forms/${params.formId}/responses`
|
||||
|
||||
|
||||
const queryParams = []
|
||||
|
||||
|
||||
if (params.pageSize) {
|
||||
queryParams.push(`page_size=${params.pageSize}`)
|
||||
}
|
||||
|
||||
|
||||
if (params.since) {
|
||||
queryParams.push(`since=${encodeURIComponent(params.since)}`)
|
||||
}
|
||||
|
||||
|
||||
if (params.until) {
|
||||
queryParams.push(`until=${encodeURIComponent(params.until)}`)
|
||||
}
|
||||
|
||||
|
||||
if (params.completed && params.completed !== 'all') {
|
||||
queryParams.push(`completed=${params.completed}`)
|
||||
}
|
||||
|
||||
|
||||
return queryParams.length > 0 ? `${url}?${queryParams.join('&')}` : url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
'Authorization': `Bearer ${params.apiKey}`,
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
let errorMessage = response.statusText || 'Unknown error';
|
||||
|
||||
let errorMessage = response.statusText || 'Unknown error'
|
||||
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
const errorData = await response.json()
|
||||
if (errorData && errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
errorMessage = errorData.message
|
||||
} else if (errorData && errorData.description) {
|
||||
errorMessage = errorData.description;
|
||||
errorMessage = errorData.description
|
||||
} else if (typeof errorData === 'string') {
|
||||
errorMessage = errorData;
|
||||
errorMessage = errorData
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't parse the error as JSON, just use the status text
|
||||
}
|
||||
|
||||
throw new Error(`Typeform API error (${response.status}): ${errorMessage}`);
|
||||
|
||||
throw new Error(`Typeform API error (${response.status}): ${errorMessage}`)
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const data = await response.json();
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse Typeform response: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw new Error(
|
||||
`Failed to parse Typeform response: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
return `Failed to retrieve Typeform responses: ${error.message}`
|
||||
}
|
||||
|
||||
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
return `Failed to retrieve Typeform responses: ${JSON.stringify(error)}`
|
||||
}
|
||||
|
||||
|
||||
return `Failed to retrieve Typeform responses: An unknown error occurred`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -48,7 +48,9 @@ export function formatRequestParams(tool: ToolConfig, params: Record<string, any
|
||||
const bodyResult = tool.request.body ? tool.request.body(params) : undefined
|
||||
|
||||
// Special handling for NDJSON content type or 'application/x-www-form-urlencoded'
|
||||
const isPreformattedContent = headers['Content-Type'] === 'application/x-ndjson' || headers['Content-Type'] === 'application/x-www-form-urlencoded'
|
||||
const isPreformattedContent =
|
||||
headers['Content-Type'] === 'application/x-ndjson' ||
|
||||
headers['Content-Type'] === 'application/x-www-form-urlencoded'
|
||||
const body = hasBody
|
||||
? isPreformattedContent && bodyResult
|
||||
? bodyResult.body
|
||||
|
||||
Reference in New Issue
Block a user