From 21b857caf5736e8f753d5b20e8a74094f64a2730 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 11:15:13 -0800 Subject: [PATCH] chore(executor): extract shared utils and remove dead code from handlers --- .gitignore | 4 + .../executor/handlers/agent/agent-handler.ts | 97 ++++------- .../handlers/evaluator/evaluator-handler.ts | 45 +---- .../human-in-the-loop-handler.ts | 140 +-------------- .../handlers/response/response-handler.ts | 159 +----------------- .../handlers/router/router-handler.ts | 40 +---- .../handlers/workflow/workflow-handler.ts | 39 ----- apps/sim/executor/utils/builder-data.ts | 149 ++++++++++++++++ apps/sim/executor/utils/vertex-credential.ts | 42 +++++ 9 files changed, 251 insertions(+), 464 deletions(-) create mode 100644 apps/sim/executor/utils/builder-data.ts create mode 100644 apps/sim/executor/utils/vertex-credential.ts diff --git a/.gitignore b/.gitignore index 6617532dd8..2d791fcbf6 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,7 @@ start-collector.sh ## Helm Chart Tests helm/sim/test i18n.cache + +## Claude Code +.claude/launch.json +.claude/worktrees/ diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 33ae22f5ec..c76ab48c0d 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -1,9 +1,8 @@ import { db } from '@sim/db' -import { account, mcpServers } from '@sim/db/schema' +import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull } from 'drizzle-orm' import { createMcpToolId } from '@/lib/mcp/utils' -import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' import { getAllBlocks } from '@/blocks' import type { BlockOutput } from '@/blocks/types' import { @@ -30,6 +29,7 @@ import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/execu import { collectBlockData } from '@/executor/utils/block-data' import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http' import { stringifyJSON } from '@/executor/utils/json' +import { resolveVertexCredential } from '@/executor/utils/vertex-credential' import { executeProviderRequest } from '@/providers' import { getProviderFromModel, transformBlockTool } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -439,24 +439,15 @@ export class AgentBlockHandler implements BlockHandler { tool: ToolInput ): Promise { const { serverId, toolName, serverName, ...userProvidedParams } = tool.params || {} - - const { filterSchemaForLLM } = await import('@/tools/params') - const filteredSchema = filterSchemaForLLM( - tool.schema || { type: 'object', properties: {} }, - userProvidedParams - ) - - const toolId = createMcpToolId(serverId, toolName) - - return { - id: toolId, - name: toolName, + return this.buildMcpTool({ + serverId, + toolName, description: tool.schema?.description || `MCP tool ${toolName} from ${serverName || serverId}`, - parameters: filteredSchema, - params: userProvidedParams, - usageControl: tool.usageControl || 'auto', - } + schema: tool.schema || { type: 'object', properties: {} }, + userProvidedParams, + usageControl: tool.usageControl, + }) } /** @@ -585,22 +576,35 @@ export class AgentBlockHandler implements BlockHandler { serverId: string ): Promise { const { toolName, ...userProvidedParams } = tool.params || {} + return this.buildMcpTool({ + serverId, + toolName, + description: mcpTool.description || `MCP tool ${toolName} from ${mcpTool.serverName}`, + schema: mcpTool.inputSchema || { type: 'object', properties: {} }, + userProvidedParams, + usageControl: tool.usageControl, + }) + } + private async buildMcpTool(config: { + serverId: string + toolName: string + description: string + schema: any + userProvidedParams: Record + usageControl?: string + }): Promise { const { filterSchemaForLLM } = await import('@/tools/params') - const filteredSchema = filterSchemaForLLM( - mcpTool.inputSchema || { type: 'object', properties: {} }, - userProvidedParams - ) - - const toolId = createMcpToolId(serverId, toolName) + const filteredSchema = filterSchemaForLLM(config.schema, config.userProvidedParams) + const toolId = createMcpToolId(config.serverId, config.toolName) return { id: toolId, - name: toolName, - description: mcpTool.description || `MCP tool ${toolName} from ${mcpTool.serverName}`, + name: config.toolName, + description: config.description, parameters: filteredSchema, - params: userProvidedParams, - usageControl: tool.usageControl || 'auto', + params: config.userProvidedParams, + usageControl: config.usageControl || 'auto', } } @@ -924,9 +928,9 @@ export class AgentBlockHandler implements BlockHandler { let finalApiKey: string | undefined = providerRequest.apiKey if (providerId === 'vertex' && providerRequest.vertexCredential) { - finalApiKey = await this.resolveVertexCredential( + finalApiKey = await resolveVertexCredential( providerRequest.vertexCredential, - ctx.workflowId + 'vertex-agent' ) } @@ -973,37 +977,6 @@ export class AgentBlockHandler implements BlockHandler { } } - /** - * Resolves a Vertex AI OAuth credential to an access token - */ - private async resolveVertexCredential(credentialId: string, workflowId: string): Promise { - const requestId = `vertex-${Date.now()}` - - logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) - - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) - } - - const credential = await db.query.account.findFirst({ - where: eq(account.id, resolved.accountId), - }) - - if (!credential) { - throw new Error(`Vertex AI credential not found: ${credentialId}`) - } - - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) - - if (!accessToken) { - throw new Error('Failed to get Vertex AI access token') - } - - logger.info(`[${requestId}] Successfully resolved Vertex AI credential`) - return accessToken - } - private handleExecutionError( error: any, startTime: number, @@ -1187,7 +1160,7 @@ export class AgentBlockHandler implements BlockHandler { }, toolCalls: { list: result.toolCalls?.map(this.formatToolCall.bind(this)) || [], - count: result.toolCalls?.length || DEFAULTS.EXECUTION_TIME, + count: result.toolCalls?.length ?? 0, }, providerTiming: result.timing, cost: result.cost, diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index bd8fe04a24..710db01ee1 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -1,14 +1,11 @@ -import { db } from '@sim/db' -import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' -import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' import type { BlockOutput } from '@/blocks/types' import { validateModelProvider } from '@/ee/access-control/utils/permission-check' import { BlockType, DEFAULTS, EVALUATOR } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http' import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json' +import { resolveVertexCredential } from '@/executor/utils/vertex-credential' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -44,7 +41,10 @@ export class EvaluatorBlockHandler implements BlockHandler { let finalApiKey: string | undefined = evaluatorConfig.apiKey if (providerId === 'vertex' && evaluatorConfig.vertexCredential) { - finalApiKey = await this.resolveVertexCredential(evaluatorConfig.vertexCredential) + finalApiKey = await resolveVertexCredential( + evaluatorConfig.vertexCredential, + 'vertex-evaluator' + ) } const processedContent = this.processContent(inputs.content) @@ -234,7 +234,7 @@ export class EvaluatorBlockHandler implements BlockHandler { if (Object.keys(parsedContent).length === 0) { validMetrics.forEach((metric: any) => { if (metric?.name) { - metricScores[metric.name.toLowerCase()] = DEFAULTS.EXECUTION_TIME + metricScores[metric.name.toLowerCase()] = 0 } }) return metricScores @@ -273,37 +273,6 @@ export class EvaluatorBlockHandler implements BlockHandler { } logger.warn(`Metric "${metricName}" not found in LLM response`) - return DEFAULTS.EXECUTION_TIME - } - - /** - * Resolves a Vertex AI OAuth credential to an access token - */ - private async resolveVertexCredential(credentialId: string): Promise { - const requestId = `vertex-evaluator-${Date.now()}` - - logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) - - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) - } - - const credential = await db.query.account.findFirst({ - where: eq(account.id, resolved.accountId), - }) - - if (!credential) { - throw new Error(`Vertex AI credential not found: ${credentialId}`) - } - - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) - - if (!accessToken) { - throw new Error('Failed to get Vertex AI access token') - } - - logger.info(`[${requestId}] Successfully resolved Vertex AI credential`) - return accessToken + return 0 } } diff --git a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts index 63b331754a..2208e9911d 100644 --- a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts +++ b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts @@ -9,7 +9,6 @@ import { HTTP, normalizeName, PAUSE_RESUME, - REFERENCE, } from '@/executor/constants' import { generatePauseContextId, @@ -17,6 +16,7 @@ import { } from '@/executor/human-in-the-loop/utils' import type { BlockHandler, ExecutionContext, PauseMetadata } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' +import { convertBuilderDataToJson, convertPropertyValue } from '@/executor/utils/builder-data' import { parseObjectStrings } from '@/executor/utils/json' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' @@ -265,7 +265,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler { } if (dataMode === 'structured' && inputs.builderData) { - const convertedData = this.convertBuilderDataToJson(inputs.builderData) + const convertedData = convertBuilderDataToJson(inputs.builderData) return parseObjectStrings(convertedData) } @@ -296,7 +296,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler { } } - const value = this.convertPropertyValue(prop) + const value = convertPropertyValue(prop) entries.push({ name: path, @@ -352,140 +352,6 @@ export class HumanInTheLoopBlockHandler implements BlockHandler { .filter((field): field is NormalizedInputField => field !== null) } - private convertBuilderDataToJson(builderData: JSONProperty[]): any { - if (!Array.isArray(builderData)) { - return {} - } - - const result: any = {} - - for (const prop of builderData) { - if (!prop.name || !prop.name.trim()) { - continue - } - - const value = this.convertPropertyValue(prop) - result[prop.name] = value - } - - return result - } - - static convertBuilderDataToJsonString(builderData: JSONProperty[]): string { - if (!Array.isArray(builderData) || builderData.length === 0) { - return '{\n \n}' - } - - const result: any = {} - - for (const prop of builderData) { - if (!prop.name || !prop.name.trim()) { - continue - } - - result[prop.name] = prop.value - } - - let jsonString = JSON.stringify(result, null, 2) - - jsonString = jsonString.replace(/"(<[^>]+>)"/g, '$1') - - return jsonString - } - - private convertPropertyValue(prop: JSONProperty): any { - switch (prop.type) { - case 'object': - return this.convertObjectValue(prop.value) - case 'array': - return this.convertArrayValue(prop.value) - case 'number': - return this.convertNumberValue(prop.value) - case 'boolean': - return this.convertBooleanValue(prop.value) - case 'files': - return prop.value - default: - return prop.value - } - } - - private convertObjectValue(value: any): any { - if (Array.isArray(value)) { - return this.convertBuilderDataToJson(value) - } - - if (typeof value === 'string' && !this.isVariableReference(value)) { - return this.tryParseJson(value, value) - } - - return value - } - - private convertArrayValue(value: any): any { - if (Array.isArray(value)) { - return value.map((item: any) => this.convertArrayItem(item)) - } - - if (typeof value === 'string' && !this.isVariableReference(value)) { - const parsed = this.tryParseJson(value, value) - return Array.isArray(parsed) ? parsed : value - } - - return value - } - - private convertArrayItem(item: any): any { - if (typeof item !== 'object' || !item.type) { - return item - } - - if (item.type === 'object' && Array.isArray(item.value)) { - return this.convertBuilderDataToJson(item.value) - } - - if (item.type === 'array' && Array.isArray(item.value)) { - return item.value.map((subItem: any) => - typeof subItem === 'object' && subItem.type ? subItem.value : subItem - ) - } - - return item.value - } - - private convertNumberValue(value: any): any { - if (this.isVariableReference(value)) { - return value - } - - const numValue = Number(value) - return Number.isNaN(numValue) ? value : numValue - } - - private convertBooleanValue(value: any): any { - if (this.isVariableReference(value)) { - return value - } - - return value === 'true' || value === true - } - - private tryParseJson(jsonString: string, fallback: any): any { - try { - return JSON.parse(jsonString) - } catch { - return fallback - } - } - - private isVariableReference(value: any): boolean { - return ( - typeof value === 'string' && - value.trim().startsWith(REFERENCE.START) && - value.trim().includes(REFERENCE.END) - ) - } - private parseStatus(status?: string): number { if (!status) return HTTP.STATUS.OK const parsed = Number(status) diff --git a/apps/sim/executor/handlers/response/response-handler.ts b/apps/sim/executor/handlers/response/response-handler.ts index 8a3abb22fb..389ddda76b 100644 --- a/apps/sim/executor/handlers/response/response-handler.ts +++ b/apps/sim/executor/handlers/response/response-handler.ts @@ -1,19 +1,15 @@ import { createLogger } from '@sim/logger' -import { BlockType, HTTP, REFERENCE } from '@/executor/constants' +import { BlockType, HTTP } from '@/executor/constants' import type { BlockHandler, ExecutionContext, NormalizedBlockOutput } from '@/executor/types' +import { + convertBuilderDataToJson, + convertBuilderDataToJsonString, +} from '@/executor/utils/builder-data' import { parseObjectStrings } from '@/executor/utils/json' import type { SerializedBlock } from '@/serializer/types' const logger = createLogger('ResponseBlockHandler') -interface JSONProperty { - id: string - name: string - type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'files' - value: any - collapsed?: boolean -} - export class ResponseBlockHandler implements BlockHandler { canHandle(block: SerializedBlock): boolean { return block.metadata?.id === BlockType.RESPONSE @@ -73,154 +69,15 @@ export class ResponseBlockHandler implements BlockHandler { } if (dataMode === 'structured' && inputs.builderData) { - const convertedData = this.convertBuilderDataToJson(inputs.builderData) + const convertedData = convertBuilderDataToJson(inputs.builderData) return parseObjectStrings(convertedData) } return inputs.data || {} } - private convertBuilderDataToJson(builderData: JSONProperty[]): any { - if (!Array.isArray(builderData)) { - return {} - } - - const result: any = {} - - for (const prop of builderData) { - if (!prop.name || !prop.name.trim()) { - continue - } - - const value = this.convertPropertyValue(prop) - result[prop.name] = value - } - - return result - } - - static convertBuilderDataToJsonString(builderData: JSONProperty[]): string { - if (!Array.isArray(builderData) || builderData.length === 0) { - return '{\n \n}' - } - - const result: any = {} - - for (const prop of builderData) { - if (!prop.name || !prop.name.trim()) { - continue - } - - result[prop.name] = prop.value - } - - let jsonString = JSON.stringify(result, null, 2) - - jsonString = jsonString.replace(/"(<[^>]+>)"/g, '$1') - - return jsonString - } - - private convertPropertyValue(prop: JSONProperty): any { - switch (prop.type) { - case 'object': - return this.convertObjectValue(prop.value) - case 'array': - return this.convertArrayValue(prop.value) - case 'number': - return this.convertNumberValue(prop.value) - case 'boolean': - return this.convertBooleanValue(prop.value) - case 'files': - return prop.value - default: - return prop.value - } - } - - private convertObjectValue(value: any): any { - if (Array.isArray(value)) { - return this.convertBuilderDataToJson(value) - } - - if (typeof value === 'string' && !this.isVariableReference(value)) { - return this.tryParseJson(value, value) - } - - return value - } - - private convertArrayValue(value: any): any { - if (Array.isArray(value)) { - return value.map((item: any) => this.convertArrayItem(item)) - } - - if (typeof value === 'string' && !this.isVariableReference(value)) { - const parsed = this.tryParseJson(value, value) - if (Array.isArray(parsed)) { - return parsed - } - return value - } - - return value - } - - private convertArrayItem(item: any): any { - if (typeof item !== 'object' || !item.type) { - return item - } - - if (item.type === 'object' && Array.isArray(item.value)) { - return this.convertBuilderDataToJson(item.value) - } - - if (item.type === 'array' && Array.isArray(item.value)) { - return item.value.map((subItem: any) => { - if (typeof subItem === 'object' && subItem.type) { - return subItem.value - } - return subItem - }) - } - - return item.value - } - - private convertNumberValue(value: any): any { - if (this.isVariableReference(value)) { - return value - } - - const numValue = Number(value) - if (Number.isNaN(numValue)) { - return value - } - return numValue - } - - private convertBooleanValue(value: any): any { - if (this.isVariableReference(value)) { - return value - } - - return value === 'true' || value === true - } - - private tryParseJson(jsonString: string, fallback: any): any { - try { - return JSON.parse(jsonString) - } catch { - return fallback - } - } - - private isVariableReference(value: any): boolean { - return ( - typeof value === 'string' && - value.trim().startsWith(REFERENCE.START) && - value.trim().includes(REFERENCE.END) - ) + static convertBuilderDataToJsonString(builderData: any[]): string { + return convertBuilderDataToJsonString(builderData) } private parseStatus(status?: string): number { diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 723ba94393..c107f67937 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -1,9 +1,5 @@ -import { db } from '@sim/db' -import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' -import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/router' import type { BlockOutput } from '@/blocks/types' import { validateModelProvider } from '@/ee/access-control/utils/permission-check' @@ -16,6 +12,7 @@ import { } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { buildAuthHeaders } from '@/executor/utils/http' +import { resolveVertexCredential } from '@/executor/utils/vertex-credential' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -87,7 +84,7 @@ export class RouterBlockHandler implements BlockHandler { let finalApiKey: string | undefined = routerConfig.apiKey if (providerId === 'vertex' && routerConfig.vertexCredential) { - finalApiKey = await this.resolveVertexCredential(routerConfig.vertexCredential) + finalApiKey = await resolveVertexCredential(routerConfig.vertexCredential, 'vertex-router') } const providerRequest: Record = { @@ -217,7 +214,7 @@ export class RouterBlockHandler implements BlockHandler { let finalApiKey: string | undefined = routerConfig.apiKey if (providerId === 'vertex' && routerConfig.vertexCredential) { - finalApiKey = await this.resolveVertexCredential(routerConfig.vertexCredential) + finalApiKey = await resolveVertexCredential(routerConfig.vertexCredential, 'vertex-router') } const providerRequest: Record = { @@ -416,35 +413,4 @@ export class RouterBlockHandler implements BlockHandler { } }) } - - /** - * Resolves a Vertex AI OAuth credential to an access token - */ - private async resolveVertexCredential(credentialId: string): Promise { - const requestId = `vertex-router-${Date.now()}` - - logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) - - const resolved = await resolveOAuthAccountId(credentialId) - if (!resolved) { - throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) - } - - const credential = await db.query.account.findFirst({ - where: eq(account.id, resolved.accountId), - }) - - if (!credential) { - throw new Error(`Vertex AI credential not found: ${credentialId}`) - } - - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) - - if (!accessToken) { - throw new Error('Failed to get Vertex AI access token') - } - - logger.info(`[${requestId}] Successfully resolved Vertex AI credential`) - return accessToken - } } diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 72bfa6781f..596e879683 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -579,45 +579,6 @@ export class WorkflowBlockHandler implements BlockHandler { return processed } - private flattenChildWorkflowSpans(spans: TraceSpan[]): WorkflowTraceSpan[] { - const flattened: WorkflowTraceSpan[] = [] - - spans.forEach((span) => { - if (this.isSyntheticWorkflowWrapper(span)) { - if (span.children && Array.isArray(span.children)) { - flattened.push(...this.flattenChildWorkflowSpans(span.children)) - } - return - } - - const workflowSpan: WorkflowTraceSpan = { - ...span, - } - - if (Array.isArray(workflowSpan.children)) { - const childSpans = workflowSpan.children as TraceSpan[] - workflowSpan.children = this.flattenChildWorkflowSpans(childSpans) - } - - if (workflowSpan.output && typeof workflowSpan.output === 'object') { - const { childTraceSpans: nestedChildSpans, ...outputRest } = workflowSpan.output as { - childTraceSpans?: TraceSpan[] - } & Record - - if (Array.isArray(nestedChildSpans) && nestedChildSpans.length > 0) { - const flattenedNestedChildren = this.flattenChildWorkflowSpans(nestedChildSpans) - workflowSpan.children = [...(workflowSpan.children || []), ...flattenedNestedChildren] - } - - workflowSpan.output = outputRest - } - - flattened.push(workflowSpan) - }) - - return flattened - } - private toExecutionResult(result: ExecutionResult | StreamingExecution): ExecutionResult { return 'execution' in result ? result.execution : result } diff --git a/apps/sim/executor/utils/builder-data.ts b/apps/sim/executor/utils/builder-data.ts new file mode 100644 index 0000000000..da471e5f4a --- /dev/null +++ b/apps/sim/executor/utils/builder-data.ts @@ -0,0 +1,149 @@ +import { REFERENCE } from '@/executor/constants' + +export interface JSONProperty { + id: string + name: string + type: string + value: any + collapsed?: boolean +} + +/** + * Converts builder data (structured JSON properties) into a plain JSON object. + */ +export function convertBuilderDataToJson(builderData: JSONProperty[]): any { + if (!Array.isArray(builderData)) { + return {} + } + + const result: any = {} + + for (const prop of builderData) { + if (!prop.name || !prop.name.trim()) { + continue + } + + const value = convertPropertyValue(prop) + result[prop.name] = value + } + + return result +} + +/** + * Converts builder data into a JSON string with variable references unquoted. + */ +export function convertBuilderDataToJsonString(builderData: JSONProperty[]): string { + if (!Array.isArray(builderData) || builderData.length === 0) { + return '{\n \n}' + } + + const result: any = {} + + for (const prop of builderData) { + if (!prop.name || !prop.name.trim()) { + continue + } + + result[prop.name] = prop.value + } + + let jsonString = JSON.stringify(result, null, 2) + + jsonString = jsonString.replace(/"(<[^>]+>)"/g, '$1') + + return jsonString +} + +export function convertPropertyValue(prop: JSONProperty): any { + switch (prop.type) { + case 'object': + return convertObjectValue(prop.value) + case 'array': + return convertArrayValue(prop.value) + case 'number': + return convertNumberValue(prop.value) + case 'boolean': + return convertBooleanValue(prop.value) + case 'files': + return prop.value + default: + return prop.value + } +} + +function convertObjectValue(value: any): any { + if (Array.isArray(value)) { + return convertBuilderDataToJson(value) + } + + if (typeof value === 'string' && !isVariableReference(value)) { + return tryParseJson(value, value) + } + + return value +} + +function convertArrayValue(value: any): any { + if (Array.isArray(value)) { + return value.map((item: any) => convertArrayItem(item)) + } + + if (typeof value === 'string' && !isVariableReference(value)) { + const parsed = tryParseJson(value, value) + return Array.isArray(parsed) ? parsed : value + } + + return value +} + +function convertArrayItem(item: any): any { + if (typeof item !== 'object' || !item.type) { + return item + } + + if (item.type === 'object' && Array.isArray(item.value)) { + return convertBuilderDataToJson(item.value) + } + + if (item.type === 'array' && Array.isArray(item.value)) { + return item.value.map((subItem: any) => + typeof subItem === 'object' && subItem.type ? subItem.value : subItem + ) + } + + return item.value +} + +function convertNumberValue(value: any): any { + if (isVariableReference(value)) { + return value + } + + const numValue = Number(value) + return Number.isNaN(numValue) ? value : numValue +} + +function convertBooleanValue(value: any): any { + if (isVariableReference(value)) { + return value + } + + return value === 'true' || value === true +} + +function tryParseJson(jsonString: string, fallback: any): any { + try { + return JSON.parse(jsonString) + } catch { + return fallback + } +} + +function isVariableReference(value: any): boolean { + return ( + typeof value === 'string' && + value.trim().startsWith(REFERENCE.START) && + value.trim().includes(REFERENCE.END) + ) +} diff --git a/apps/sim/executor/utils/vertex-credential.ts b/apps/sim/executor/utils/vertex-credential.ts new file mode 100644 index 0000000000..237262f406 --- /dev/null +++ b/apps/sim/executor/utils/vertex-credential.ts @@ -0,0 +1,42 @@ +import { db } from '@sim/db' +import { account } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('VertexCredential') + +/** + * Resolves a Vertex AI OAuth credential to an access token. + * Shared across agent, evaluator, and router handlers. + */ +export async function resolveVertexCredential( + credentialId: string, + callerLabel = 'vertex' +): Promise { + const requestId = `${callerLabel}-${Date.now()}` + + logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`) + + const resolved = await resolveOAuthAccountId(credentialId) + if (!resolved) { + throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`) + } + + const credential = await db.query.account.findFirst({ + where: eq(account.id, resolved.accountId), + }) + + if (!credential) { + throw new Error(`Vertex AI credential not found: ${credentialId}`) + } + + const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId) + + if (!accessToken) { + throw new Error('Failed to get Vertex AI access token') + } + + logger.info(`[${requestId}] Successfully resolved Vertex AI credential`) + return accessToken +}