mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 13:00:04 +08:00
chore(executor): extract shared utils and remove dead code from handlers (#3334)
This commit is contained in:
@@ -73,3 +73,7 @@ start-collector.sh
|
||||
## Helm Chart Tests
|
||||
helm/sim/test
|
||||
i18n.cache
|
||||
|
||||
## Claude Code
|
||||
.claude/launch.json
|
||||
.claude/worktrees/
|
||||
|
||||
@@ -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<any> {
|
||||
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<any> {
|
||||
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<string, any>
|
||||
usageControl?: string
|
||||
}): Promise<any> {
|
||||
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<string> {
|
||||
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,
|
||||
|
||||
@@ -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<string> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, any> = {
|
||||
@@ -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<string, any> = {
|
||||
@@ -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<string> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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<string> {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user