mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 14:43:54 -05:00
* Superagent poc * Checkpoint brokeN * tool call rag * Fix * Fixes * Improvements * Creds stuff * Fix * Fix tools * Fix stream * Prompt * Update sheets descriptions * Better * Copilot components * Delete stuff * Remove db migration * Fix migrations * Fix things * Copilot side superagent * Build workflow from chat * Combine superagent into copilkot * Render tools * Function execution * Max mode indicators * Tool call confirmations * Credential settings * Remove betas * Bump version * Dropdown options in block metadata * Copilot kb tools * Fix lint * Credentials modal * Fix lint * Cleanup * Env var resolution in superagent tools * Get id for workflow vars * Fix insert into subflow * Fix executor for while and do while loops * Fix metadata for parallel * Remove db migration * Rebase * Add migrations back * Clean up code * Fix executor logic issue * Cleanup * Diagram tool * Fix tool naems * Comment out g3p * Remove popup option * Hide o3 * Remove db migration * Fix merge conflicts * Fix lint * Fix tests * Remove webhook change * Remove cb change * Fix lint * Fix * Fix lint * Fix build * comment out gemini * Add gemini back * Remove bad test * Fix * Fix test * Fix * Nuke bad test * Fix lint --------- Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com> Co-authored-by: Waleed <walif6@gmail.com> Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
1151 lines
35 KiB
TypeScript
1151 lines
35 KiB
TypeScript
import { existsSync, readFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
|
import {
|
|
type GetBlocksMetadataInput,
|
|
GetBlocksMetadataResult,
|
|
} from '@/lib/copilot/tools/shared/schemas'
|
|
import { createLogger } from '@/lib/logs/console/logger'
|
|
import { registry as blockRegistry } from '@/blocks/registry'
|
|
import type { BlockConfig } from '@/blocks/types'
|
|
import { AuthMode } from '@/blocks/types'
|
|
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
|
import { tools as toolsRegistry } from '@/tools/registry'
|
|
import { getTrigger, isTriggerValid } from '@/triggers'
|
|
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/consts'
|
|
|
|
export interface CopilotSubblockMetadata {
|
|
id: string
|
|
type: string
|
|
title?: string
|
|
required?: boolean
|
|
description?: string
|
|
placeholder?: string
|
|
layout?: string
|
|
mode?: string
|
|
hidden?: boolean
|
|
condition?: any
|
|
// Dropdown/combobox options
|
|
options?: { id: string; label?: string; hasIcon?: boolean }[]
|
|
// Numeric constraints
|
|
min?: number
|
|
max?: number
|
|
step?: number
|
|
integer?: boolean
|
|
// Text input properties
|
|
rows?: number
|
|
password?: boolean
|
|
multiSelect?: boolean
|
|
// Code/generation properties
|
|
language?: string
|
|
generationType?: string
|
|
// OAuth/credential properties
|
|
serviceId?: string
|
|
requiredScopes?: string[]
|
|
// File properties
|
|
mimeType?: string
|
|
acceptedTypes?: string
|
|
multiple?: boolean
|
|
maxSize?: number
|
|
// Other properties
|
|
connectionDroppable?: boolean
|
|
columns?: string[]
|
|
wandConfig?: any
|
|
availableTriggers?: string[]
|
|
triggerProvider?: string
|
|
dependsOn?: string[]
|
|
canonicalParamId?: string
|
|
defaultValue?: any
|
|
value?: string // 'function' if it's a function, undefined otherwise
|
|
}
|
|
|
|
export interface CopilotToolMetadata {
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
inputs?: any
|
|
outputs?: any
|
|
}
|
|
|
|
export interface CopilotTriggerMetadata {
|
|
id: string
|
|
outputs?: any
|
|
configFields?: any
|
|
}
|
|
|
|
export interface CopilotBlockMetadata {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
bestPractices?: string
|
|
inputSchema: CopilotSubblockMetadata[]
|
|
inputDefinitions?: Record<string, any>
|
|
triggerAllowed?: boolean
|
|
authType?: 'OAuth' | 'API Key' | 'Bot Token'
|
|
tools: CopilotToolMetadata[]
|
|
triggers: CopilotTriggerMetadata[]
|
|
operationInputSchema: Record<string, CopilotSubblockMetadata[]>
|
|
operations?: Record<
|
|
string,
|
|
{
|
|
toolId?: string
|
|
toolName?: string
|
|
description?: string
|
|
inputs?: Record<string, any>
|
|
outputs?: Record<string, any>
|
|
inputSchema?: CopilotSubblockMetadata[]
|
|
}
|
|
>
|
|
outputs?: Record<string, any>
|
|
yamlDocumentation?: string
|
|
}
|
|
|
|
export const getBlocksMetadataServerTool: BaseServerTool<
|
|
ReturnType<typeof GetBlocksMetadataInput.parse>,
|
|
ReturnType<typeof GetBlocksMetadataResult.parse>
|
|
> = {
|
|
name: 'get_blocks_metadata',
|
|
async execute({
|
|
blockIds,
|
|
}: ReturnType<typeof GetBlocksMetadataInput.parse>): Promise<
|
|
ReturnType<typeof GetBlocksMetadataResult.parse>
|
|
> {
|
|
const logger = createLogger('GetBlocksMetadataServerTool')
|
|
logger.debug('Executing get_blocks_metadata', { count: blockIds?.length })
|
|
|
|
const result: Record<string, CopilotBlockMetadata> = {}
|
|
for (const blockId of blockIds || []) {
|
|
let metadata: any
|
|
|
|
if (SPECIAL_BLOCKS_METADATA[blockId]) {
|
|
const specialBlock = SPECIAL_BLOCKS_METADATA[blockId]
|
|
const { commonParameters, operationParameters } = splitParametersByOperation(
|
|
specialBlock.subBlocks || [],
|
|
specialBlock.inputs || {}
|
|
)
|
|
metadata = {
|
|
id: specialBlock.id,
|
|
name: specialBlock.name,
|
|
description: specialBlock.description || '',
|
|
inputSchema: commonParameters,
|
|
inputDefinitions: specialBlock.inputs || {},
|
|
tools: [],
|
|
triggers: [],
|
|
operationInputSchema: operationParameters,
|
|
outputs: specialBlock.outputs,
|
|
}
|
|
;(metadata as any).subBlocks = undefined
|
|
} else {
|
|
const blockConfig: BlockConfig | undefined = blockRegistry[blockId]
|
|
if (!blockConfig) {
|
|
logger.debug('Block not found in registry', { blockId })
|
|
continue
|
|
}
|
|
|
|
if (blockConfig.hideFromToolbar) {
|
|
logger.debug('Skipping block hidden from toolbar', { blockId })
|
|
continue
|
|
}
|
|
const tools: CopilotToolMetadata[] = Array.isArray(blockConfig.tools?.access)
|
|
? blockConfig.tools!.access.map((toolId) => {
|
|
const tool = toolsRegistry[toolId]
|
|
if (!tool) return { id: toolId, name: toolId }
|
|
return {
|
|
id: toolId,
|
|
name: tool.name || toolId,
|
|
description: tool.description || '',
|
|
inputs: tool.params || {},
|
|
outputs: tool.outputs || {},
|
|
}
|
|
})
|
|
: []
|
|
|
|
const triggers: CopilotTriggerMetadata[] = []
|
|
const availableTriggerIds = blockConfig.triggers?.available || []
|
|
for (const tid of availableTriggerIds) {
|
|
if (!isTriggerValid(tid)) {
|
|
logger.debug('Invalid trigger ID found in block config', { blockId, triggerId: tid })
|
|
continue
|
|
}
|
|
|
|
const trig = getTrigger(tid)
|
|
|
|
const configFields: Record<string, any> = {}
|
|
for (const subBlock of trig.subBlocks) {
|
|
if (subBlock.mode === 'trigger' && !SYSTEM_SUBBLOCK_IDS.includes(subBlock.id)) {
|
|
const fieldDef: any = {
|
|
type: subBlock.type,
|
|
required: subBlock.required || false,
|
|
}
|
|
|
|
if (subBlock.title) fieldDef.title = subBlock.title
|
|
if (subBlock.description) fieldDef.description = subBlock.description
|
|
if (subBlock.placeholder) fieldDef.placeholder = subBlock.placeholder
|
|
if (subBlock.defaultValue !== undefined) fieldDef.default = subBlock.defaultValue
|
|
|
|
if (subBlock.options && Array.isArray(subBlock.options)) {
|
|
fieldDef.options = subBlock.options.map((opt: any) => ({
|
|
id: opt.id,
|
|
label: opt.label || opt.id,
|
|
}))
|
|
}
|
|
|
|
if (subBlock.condition) {
|
|
const cond =
|
|
typeof subBlock.condition === 'function'
|
|
? subBlock.condition()
|
|
: subBlock.condition
|
|
if (cond) {
|
|
fieldDef.condition = cond
|
|
}
|
|
}
|
|
|
|
configFields[subBlock.id] = fieldDef
|
|
}
|
|
}
|
|
|
|
triggers.push({
|
|
id: tid,
|
|
outputs: trig.outputs || {},
|
|
configFields,
|
|
})
|
|
}
|
|
|
|
const blockInputs = computeBlockLevelInputs(blockConfig)
|
|
const { commonParameters, operationParameters } = splitParametersByOperation(
|
|
Array.isArray(blockConfig.subBlocks)
|
|
? blockConfig.subBlocks.filter((sb) => sb.mode !== 'trigger')
|
|
: [],
|
|
blockInputs
|
|
)
|
|
|
|
const operationInputs = computeOperationLevelInputs(blockConfig)
|
|
const operationIds = resolveOperationIds(blockConfig, operationParameters)
|
|
const operations: Record<string, any> = {}
|
|
for (const opId of operationIds) {
|
|
const resolvedToolId = resolveToolIdForOperation(blockConfig, opId)
|
|
const toolCfg = resolvedToolId ? toolsRegistry[resolvedToolId] : undefined
|
|
const toolParams: Record<string, any> = toolCfg?.params || {}
|
|
const toolOutputs: Record<string, any> = toolCfg?.outputs || {}
|
|
const filteredToolParams: Record<string, any> = {}
|
|
for (const [k, v] of Object.entries(toolParams)) {
|
|
if (!(k in blockInputs)) filteredToolParams[k] = v
|
|
}
|
|
operations[opId] = {
|
|
toolId: resolvedToolId,
|
|
toolName: toolCfg?.name || resolvedToolId,
|
|
description: toolCfg?.description || undefined,
|
|
inputs: { ...filteredToolParams, ...(operationInputs[opId] || {}) },
|
|
outputs: toolOutputs,
|
|
inputSchema: operationParameters[opId] || [],
|
|
}
|
|
}
|
|
|
|
metadata = {
|
|
id: blockId,
|
|
name: blockConfig.name || blockId,
|
|
description: blockConfig.longDescription || blockConfig.description || '',
|
|
bestPractices: blockConfig.bestPractices,
|
|
inputSchema: commonParameters,
|
|
inputDefinitions: blockInputs,
|
|
triggerAllowed: !!blockConfig.triggerAllowed,
|
|
authType: resolveAuthType(blockConfig.authMode),
|
|
tools,
|
|
triggers,
|
|
operationInputSchema: operationParameters,
|
|
operations,
|
|
outputs: blockConfig.outputs,
|
|
}
|
|
}
|
|
|
|
try {
|
|
const workingDir = process.cwd()
|
|
const isInAppsSim = workingDir.endsWith('/apps/sim') || workingDir.endsWith('\\apps\\sim')
|
|
const basePath = isInAppsSim ? join(workingDir, '..', '..') : workingDir
|
|
const docPath = join(
|
|
basePath,
|
|
'apps',
|
|
'docs',
|
|
'content',
|
|
'docs',
|
|
'yaml',
|
|
'blocks',
|
|
`${DOCS_FILE_MAPPING[blockId] || blockId}.mdx`
|
|
)
|
|
if (existsSync(docPath)) {
|
|
metadata.yamlDocumentation = readFileSync(docPath, 'utf-8')
|
|
}
|
|
} catch {}
|
|
|
|
if (metadata) {
|
|
result[blockId] = removeNullish(metadata) as CopilotBlockMetadata
|
|
}
|
|
}
|
|
|
|
const transformedResult: Record<string, any> = {}
|
|
for (const [blockId, metadata] of Object.entries(result)) {
|
|
transformedResult[blockId] = transformBlockMetadata(metadata)
|
|
}
|
|
|
|
return GetBlocksMetadataResult.parse({ metadata: transformedResult })
|
|
},
|
|
}
|
|
|
|
function transformBlockMetadata(metadata: CopilotBlockMetadata): any {
|
|
const transformed: any = {
|
|
blockType: metadata.id,
|
|
name: metadata.name,
|
|
description: metadata.description,
|
|
}
|
|
|
|
if (metadata.bestPractices) {
|
|
transformed.bestPractices = metadata.bestPractices
|
|
}
|
|
|
|
if (metadata.authType) {
|
|
transformed.authType = metadata.authType
|
|
|
|
if (metadata.authType === 'OAuth') {
|
|
transformed.requiredCredentials = {
|
|
type: 'oauth',
|
|
service: metadata.id, // e.g., 'gmail', 'slack', etc.
|
|
description: `OAuth authentication required for ${metadata.name}`,
|
|
}
|
|
} else if (metadata.authType === 'API Key') {
|
|
transformed.requiredCredentials = {
|
|
type: 'api_key',
|
|
description: `API key required for ${metadata.name}`,
|
|
}
|
|
} else if (metadata.authType === 'Bot Token') {
|
|
transformed.requiredCredentials = {
|
|
type: 'bot_token',
|
|
description: `Bot token required for ${metadata.name}`,
|
|
}
|
|
}
|
|
}
|
|
|
|
const inputs = extractInputs(metadata)
|
|
if (inputs.required.length > 0 || inputs.optional.length > 0) {
|
|
transformed.inputs = inputs
|
|
}
|
|
|
|
const hasOperations = metadata.operations && Object.keys(metadata.operations).length > 0
|
|
if (hasOperations && metadata.operations) {
|
|
const blockLevelInputs = new Set(Object.keys(metadata.inputDefinitions || {}))
|
|
transformed.operations = Object.entries(metadata.operations).reduce(
|
|
(acc, [opId, opData]) => {
|
|
acc[opId] = {
|
|
name: opData.toolName || opId,
|
|
description: opData.description,
|
|
inputs: extractOperationInputs(opData, blockLevelInputs),
|
|
outputs: formatOutputsFromDefinition(opData.outputs || {}),
|
|
}
|
|
return acc
|
|
},
|
|
{} as Record<string, any>
|
|
)
|
|
}
|
|
|
|
if (!hasOperations) {
|
|
const outputs = extractOutputs(metadata)
|
|
if (outputs.length > 0) {
|
|
transformed.outputs = outputs
|
|
}
|
|
}
|
|
|
|
if (metadata.triggers && metadata.triggers.length > 0) {
|
|
transformed.triggers = metadata.triggers.map((t) => ({
|
|
id: t.id,
|
|
outputs: formatOutputsFromDefinition(t.outputs || {}),
|
|
configFields: t.configFields || {},
|
|
}))
|
|
}
|
|
|
|
if (metadata.yamlDocumentation) {
|
|
transformed.yamlDocumentation = metadata.yamlDocumentation
|
|
}
|
|
|
|
return transformed
|
|
}
|
|
|
|
function extractInputs(metadata: CopilotBlockMetadata): {
|
|
required: any[]
|
|
optional: any[]
|
|
} {
|
|
const required: any[] = []
|
|
const optional: any[] = []
|
|
const inputDefs = metadata.inputDefinitions || {}
|
|
|
|
for (const schema of metadata.inputSchema || []) {
|
|
// Skip trigger subBlocks - they're handled separately in triggers.configFields
|
|
if (schema.mode === 'trigger') {
|
|
continue
|
|
}
|
|
|
|
if (schema.id === 'triggerConfig' || schema.type === 'trigger-config') {
|
|
continue
|
|
}
|
|
|
|
const inputDef = inputDefs[schema.id] || inputDefs[schema.canonicalParamId || '']
|
|
|
|
let description = schema.description || inputDef?.description || schema.title
|
|
if (schema.id === 'operation') {
|
|
description = 'Operation to perform'
|
|
}
|
|
|
|
const input: any = {
|
|
name: schema.id,
|
|
type: mapSchemaTypeToSimpleType(schema.type, schema),
|
|
description,
|
|
}
|
|
|
|
if (schema.options && schema.options.length > 0) {
|
|
if (schema.id === 'operation') {
|
|
input.options = schema.options.map((opt) => opt.id)
|
|
} else {
|
|
input.options = schema.options.map((opt) => opt.label || opt.id)
|
|
}
|
|
}
|
|
|
|
if (inputDef?.enum && Array.isArray(inputDef.enum)) {
|
|
input.options = inputDef.enum
|
|
}
|
|
|
|
if (schema.defaultValue !== undefined) {
|
|
input.default = schema.defaultValue
|
|
} else if (inputDef?.default !== undefined) {
|
|
input.default = inputDef.default
|
|
}
|
|
|
|
if (schema.type === 'slider' || schema.type === 'number-input') {
|
|
if (schema.min !== undefined) input.min = schema.min
|
|
if (schema.max !== undefined) input.max = schema.max
|
|
} else if (inputDef?.minimum !== undefined || inputDef?.maximum !== undefined) {
|
|
if (inputDef.minimum !== undefined) input.min = inputDef.minimum
|
|
if (inputDef.maximum !== undefined) input.max = inputDef.maximum
|
|
}
|
|
|
|
const example = generateInputExample(schema, inputDef)
|
|
if (example !== undefined) {
|
|
input.example = example
|
|
}
|
|
|
|
const isOperationField =
|
|
schema.id === 'operation' &&
|
|
metadata.operations &&
|
|
Object.keys(metadata.operations).length > 0
|
|
const isRequired = schema.required || inputDef?.required || isOperationField
|
|
|
|
if (isRequired) {
|
|
required.push(input)
|
|
} else {
|
|
optional.push(input)
|
|
}
|
|
}
|
|
|
|
return { required, optional }
|
|
}
|
|
|
|
function extractOperationInputs(
|
|
opData: any,
|
|
blockLevelInputs: Set<string>
|
|
): {
|
|
required: any[]
|
|
optional: any[]
|
|
} {
|
|
const required: any[] = []
|
|
const optional: any[] = []
|
|
const inputs = opData.inputs || {}
|
|
|
|
for (const [key, inputDef] of Object.entries(inputs)) {
|
|
if (blockLevelInputs.has(key)) {
|
|
continue
|
|
}
|
|
|
|
const input: any = {
|
|
name: key,
|
|
type: (inputDef as any)?.type || 'string',
|
|
description: (inputDef as any)?.description,
|
|
}
|
|
|
|
if ((inputDef as any)?.enum) {
|
|
input.options = (inputDef as any).enum
|
|
}
|
|
|
|
if ((inputDef as any)?.default !== undefined) {
|
|
input.default = (inputDef as any).default
|
|
}
|
|
|
|
if ((inputDef as any)?.example !== undefined) {
|
|
input.example = (inputDef as any).example
|
|
}
|
|
|
|
if ((inputDef as any)?.required) {
|
|
required.push(input)
|
|
} else {
|
|
optional.push(input)
|
|
}
|
|
}
|
|
|
|
return { required, optional }
|
|
}
|
|
|
|
function extractOutputs(metadata: CopilotBlockMetadata): any[] {
|
|
const outputs: any[] = []
|
|
|
|
if (metadata.outputs && Object.keys(metadata.outputs).length > 0) {
|
|
return formatOutputsFromDefinition(metadata.outputs)
|
|
}
|
|
|
|
if (metadata.operations && Object.keys(metadata.operations).length > 0) {
|
|
const firstOp = Object.values(metadata.operations)[0]
|
|
return formatOutputsFromDefinition(firstOp.outputs || {})
|
|
}
|
|
|
|
return outputs
|
|
}
|
|
|
|
function formatOutputsFromDefinition(outputDefs: Record<string, any>): any[] {
|
|
const outputs: any[] = []
|
|
|
|
for (const [key, def] of Object.entries(outputDefs)) {
|
|
const output: any = {
|
|
name: key,
|
|
type: typeof def === 'string' ? def : def?.type || 'any',
|
|
}
|
|
|
|
if (typeof def === 'object') {
|
|
if (def.description) output.description = def.description
|
|
if (def.example) output.example = def.example
|
|
}
|
|
|
|
outputs.push(output)
|
|
}
|
|
|
|
return outputs
|
|
}
|
|
|
|
function mapSchemaTypeToSimpleType(schemaType: string, schema: CopilotSubblockMetadata): string {
|
|
const typeMap: Record<string, string> = {
|
|
'short-input': 'string',
|
|
'long-input': 'string',
|
|
'code-input': 'string',
|
|
'number-input': 'number',
|
|
slider: 'number',
|
|
dropdown: 'string',
|
|
combobox: 'string',
|
|
toggle: 'boolean',
|
|
'json-input': 'json',
|
|
'file-upload': 'file',
|
|
'multi-select': 'array',
|
|
'credential-input': 'credential',
|
|
'oauth-credential': 'credential',
|
|
'oauth-input': 'credential',
|
|
}
|
|
|
|
const mappedType = typeMap[schemaType] || schemaType
|
|
|
|
if (schema.multiSelect) return 'array'
|
|
|
|
return mappedType
|
|
}
|
|
|
|
function generateInputExample(schema: CopilotSubblockMetadata, inputDef?: any): any {
|
|
if (inputDef?.example !== undefined) return inputDef.example
|
|
|
|
switch (schema.type) {
|
|
case 'short-input':
|
|
case 'long-input':
|
|
if (schema.id === 'systemPrompt') return 'You are a helpful assistant...'
|
|
if (schema.id === 'userPrompt') return 'What is the weather today?'
|
|
if (schema.placeholder) return schema.placeholder
|
|
return undefined
|
|
case 'number-input':
|
|
case 'slider':
|
|
return schema.defaultValue ?? schema.min ?? 0
|
|
case 'toggle':
|
|
return schema.defaultValue ?? false
|
|
case 'json-input':
|
|
return schema.defaultValue ?? {}
|
|
case 'dropdown':
|
|
case 'combobox':
|
|
if (schema.options && schema.options.length > 0) {
|
|
return schema.options[0].id
|
|
}
|
|
return undefined
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
function processSubBlock(sb: any): CopilotSubblockMetadata {
|
|
const processed: CopilotSubblockMetadata = {
|
|
id: sb.id,
|
|
type: sb.type,
|
|
}
|
|
|
|
const optionalFields = {
|
|
title: sb.title,
|
|
required: sb.required,
|
|
description: sb.description,
|
|
placeholder: sb.placeholder,
|
|
layout: sb.layout,
|
|
mode: sb.mode,
|
|
hidden: sb.hidden,
|
|
canonicalParamId: sb.canonicalParamId,
|
|
defaultValue: sb.defaultValue,
|
|
|
|
// Numeric constraints
|
|
min: sb.min,
|
|
max: sb.max,
|
|
step: sb.step,
|
|
integer: sb.integer,
|
|
|
|
// Text input properties
|
|
rows: sb.rows,
|
|
password: sb.password,
|
|
multiSelect: sb.multiSelect,
|
|
|
|
// Code/generation properties
|
|
language: sb.language,
|
|
generationType: sb.generationType,
|
|
|
|
// OAuth/credential properties
|
|
serviceId: sb.serviceId,
|
|
requiredScopes: sb.requiredScopes,
|
|
|
|
// File properties
|
|
mimeType: sb.mimeType,
|
|
acceptedTypes: sb.acceptedTypes,
|
|
multiple: sb.multiple,
|
|
maxSize: sb.maxSize,
|
|
|
|
// Other properties
|
|
connectionDroppable: sb.connectionDroppable,
|
|
columns: sb.columns,
|
|
wandConfig: sb.wandConfig,
|
|
availableTriggers: sb.availableTriggers,
|
|
triggerProvider: sb.triggerProvider,
|
|
dependsOn: sb.dependsOn,
|
|
}
|
|
|
|
// Add non-null optional fields
|
|
for (const [key, value] of Object.entries(optionalFields)) {
|
|
if (value !== undefined && value !== null) {
|
|
;(processed as any)[key] = value
|
|
}
|
|
}
|
|
|
|
// Handle condition normalization
|
|
const condition = normalizeCondition(sb.condition)
|
|
if (condition !== undefined) {
|
|
processed.condition = condition
|
|
}
|
|
|
|
// Handle value field (check if it's a function)
|
|
if (typeof sb.value === 'function') {
|
|
processed.value = 'function'
|
|
}
|
|
|
|
// Process options with icon detection
|
|
const options = resolveSubblockOptions(sb)
|
|
if (options) {
|
|
processed.options = options
|
|
}
|
|
|
|
return processed
|
|
}
|
|
|
|
function resolveAuthType(
|
|
authMode: AuthMode | undefined
|
|
): 'OAuth' | 'API Key' | 'Bot Token' | undefined {
|
|
if (!authMode) return undefined
|
|
if (authMode === AuthMode.OAuth) return 'OAuth'
|
|
if (authMode === AuthMode.ApiKey) return 'API Key'
|
|
if (authMode === AuthMode.BotToken) return 'Bot Token'
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Gets all available models from PROVIDER_DEFINITIONS as static options.
|
|
* This provides fallback data when store state is not available server-side.
|
|
* Excludes dynamic providers (ollama, vllm, openrouter) which require runtime fetching.
|
|
*/
|
|
function getStaticModelOptions(): { id: string; label?: string }[] {
|
|
const models: { id: string; label?: string }[] = []
|
|
|
|
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
|
|
// Skip providers with dynamic/fetched models
|
|
if (provider.id === 'ollama' || provider.id === 'vllm' || provider.id === 'openrouter') {
|
|
continue
|
|
}
|
|
if (provider?.models) {
|
|
for (const model of provider.models) {
|
|
models.push({ id: model.id, label: model.id })
|
|
}
|
|
}
|
|
}
|
|
|
|
return models
|
|
}
|
|
|
|
/**
|
|
* Attempts to call a dynamic options function with fallback data injected.
|
|
* When the function accesses store state that's unavailable server-side,
|
|
* this provides static fallback data from known sources.
|
|
*
|
|
* @param optionsFn - The options function to call
|
|
* @returns Options array or undefined if options cannot be resolved
|
|
*/
|
|
function callOptionsWithFallback(
|
|
optionsFn: () => any[]
|
|
): { id: string; label?: string; hasIcon?: boolean }[] | undefined {
|
|
// Get static model data to use as fallback
|
|
const staticModels = getStaticModelOptions()
|
|
|
|
// Create a mock providers state with static data
|
|
const mockProvidersState = {
|
|
providers: {
|
|
base: { models: staticModels.map((m) => m.id) },
|
|
ollama: { models: [] },
|
|
vllm: { models: [] },
|
|
openrouter: { models: [] },
|
|
},
|
|
}
|
|
|
|
// Store original getState if it exists
|
|
let originalGetState: (() => any) | undefined
|
|
let store: any
|
|
|
|
try {
|
|
// Try to get the providers store module
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
store = require('@/stores/providers/store')
|
|
if (store?.useProvidersStore?.getState) {
|
|
originalGetState = store.useProvidersStore.getState
|
|
// Temporarily replace getState with our mock
|
|
store.useProvidersStore.getState = () => mockProvidersState
|
|
}
|
|
} catch {
|
|
// Store module not available, continue with mock
|
|
}
|
|
|
|
try {
|
|
const result = optionsFn()
|
|
return result
|
|
} finally {
|
|
// Restore original getState
|
|
if (store?.useProvidersStore && originalGetState) {
|
|
store.useProvidersStore.getState = originalGetState
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveSubblockOptions(
|
|
sb: any
|
|
): { id: string; label?: string; hasIcon?: boolean }[] | undefined {
|
|
// Skip if subblock uses fetchOptions (async network calls)
|
|
if (sb.fetchOptions) {
|
|
return undefined
|
|
}
|
|
|
|
let rawOptions: any[] | undefined
|
|
|
|
try {
|
|
if (typeof sb.options === 'function') {
|
|
// Try calling with fallback data injection for store-dependent options
|
|
rawOptions = callOptionsWithFallback(sb.options)
|
|
} else {
|
|
rawOptions = sb.options
|
|
}
|
|
} catch {
|
|
// Options function failed even with fallback, skip
|
|
return undefined
|
|
}
|
|
|
|
if (!Array.isArray(rawOptions) || rawOptions.length === 0) {
|
|
return undefined
|
|
}
|
|
|
|
const normalized = rawOptions
|
|
.map((opt: any) => {
|
|
if (!opt) return undefined
|
|
|
|
const id = typeof opt === 'object' ? opt.id : opt
|
|
if (id === undefined || id === null) return undefined
|
|
|
|
const result: { id: string; label?: string; hasIcon?: boolean } = {
|
|
id: String(id),
|
|
}
|
|
|
|
if (typeof opt === 'object' && typeof opt.label === 'string') {
|
|
result.label = opt.label
|
|
}
|
|
|
|
if (typeof opt === 'object' && opt.icon) {
|
|
result.hasIcon = true
|
|
}
|
|
|
|
return result
|
|
})
|
|
.filter((o): o is { id: string; label?: string; hasIcon?: boolean } => o !== undefined)
|
|
|
|
return normalized.length > 0 ? normalized : undefined
|
|
}
|
|
|
|
function removeNullish(obj: any): any {
|
|
if (!obj || typeof obj !== 'object') return obj
|
|
|
|
const cleaned: any = Array.isArray(obj) ? [] : {}
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (value !== null && value !== undefined) {
|
|
cleaned[key] = value
|
|
}
|
|
}
|
|
|
|
return cleaned
|
|
}
|
|
|
|
function normalizeCondition(condition: any): any | undefined {
|
|
try {
|
|
if (!condition) return undefined
|
|
if (typeof condition === 'function') {
|
|
return condition()
|
|
}
|
|
return condition
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
function splitParametersByOperation(
|
|
subBlocks: any[],
|
|
blockInputsForDescriptions?: Record<string, any>
|
|
): {
|
|
commonParameters: CopilotSubblockMetadata[]
|
|
operationParameters: Record<string, CopilotSubblockMetadata[]>
|
|
} {
|
|
const commonParameters: CopilotSubblockMetadata[] = []
|
|
const operationParameters: Record<string, CopilotSubblockMetadata[]> = {}
|
|
|
|
for (const sb of subBlocks || []) {
|
|
const cond = normalizeCondition(sb.condition)
|
|
const processed = processSubBlock(sb)
|
|
|
|
if (cond && cond.field === 'operation' && !cond.not && cond.value !== undefined) {
|
|
const values: any[] = Array.isArray(cond.value) ? cond.value : [cond.value]
|
|
for (const v of values) {
|
|
const key = String(v)
|
|
if (!operationParameters[key]) operationParameters[key] = []
|
|
operationParameters[key].push(processed)
|
|
}
|
|
} else {
|
|
// Override description from inputDefinitions if available (by id or canonicalParamId)
|
|
if (blockInputsForDescriptions) {
|
|
const candidates = [sb.id, sb.canonicalParamId].filter(Boolean)
|
|
for (const key of candidates) {
|
|
const bi = (blockInputsForDescriptions as any)[key as string]
|
|
if (bi && typeof bi.description === 'string') {
|
|
processed.description = bi.description
|
|
break
|
|
}
|
|
}
|
|
}
|
|
commonParameters.push(processed)
|
|
}
|
|
}
|
|
|
|
return { commonParameters, operationParameters }
|
|
}
|
|
|
|
function computeBlockLevelInputs(blockConfig: BlockConfig): Record<string, any> {
|
|
const inputs = blockConfig.inputs || {}
|
|
const subBlocks: any[] = Array.isArray(blockConfig.subBlocks)
|
|
? blockConfig.subBlocks.filter((sb) => sb.mode !== 'trigger')
|
|
: []
|
|
|
|
const byParamKey: Record<string, any[]> = {}
|
|
for (const sb of subBlocks) {
|
|
if (sb.id) {
|
|
byParamKey[sb.id] = byParamKey[sb.id] || []
|
|
byParamKey[sb.id].push(sb)
|
|
}
|
|
if (sb.canonicalParamId) {
|
|
byParamKey[sb.canonicalParamId] = byParamKey[sb.canonicalParamId] || []
|
|
byParamKey[sb.canonicalParamId].push(sb)
|
|
}
|
|
}
|
|
|
|
const blockInputs: Record<string, any> = {}
|
|
for (const key of Object.keys(inputs)) {
|
|
const sbs = byParamKey[key] || []
|
|
const isOperationGated = sbs.some((sb) => {
|
|
const cond = normalizeCondition(sb.condition)
|
|
return cond && cond.field === 'operation' && !cond.not && cond.value !== undefined
|
|
})
|
|
if (!isOperationGated) {
|
|
blockInputs[key] = inputs[key]
|
|
}
|
|
}
|
|
|
|
return blockInputs
|
|
}
|
|
|
|
function computeOperationLevelInputs(
|
|
blockConfig: BlockConfig
|
|
): Record<string, Record<string, any>> {
|
|
const inputs = blockConfig.inputs || {}
|
|
const subBlocks = Array.isArray(blockConfig.subBlocks)
|
|
? blockConfig.subBlocks.filter((sb) => sb.mode !== 'trigger')
|
|
: []
|
|
|
|
const opInputs: Record<string, Record<string, any>> = {}
|
|
|
|
for (const sb of subBlocks) {
|
|
const cond = normalizeCondition(sb.condition)
|
|
if (!cond || cond.field !== 'operation' || cond.not) continue
|
|
const keys: string[] = []
|
|
if (sb.canonicalParamId) keys.push(sb.canonicalParamId)
|
|
if (sb.id) keys.push(sb.id)
|
|
const values = Array.isArray(cond.value) ? cond.value : [cond.value]
|
|
for (const key of keys) {
|
|
if (!(key in inputs)) continue
|
|
for (const v of values) {
|
|
const op = String(v)
|
|
if (!opInputs[op]) opInputs[op] = {}
|
|
opInputs[op][key] = inputs[key]
|
|
}
|
|
}
|
|
}
|
|
|
|
return opInputs
|
|
}
|
|
|
|
function resolveOperationIds(
|
|
blockConfig: BlockConfig,
|
|
operationParameters: Record<string, CopilotSubblockMetadata[]>
|
|
): string[] {
|
|
const opBlock = (blockConfig.subBlocks || []).find((sb) => sb.id === 'operation')
|
|
if (opBlock && Array.isArray(opBlock.options)) {
|
|
const ids = opBlock.options.map((o) => o.id).filter(Boolean)
|
|
if (ids.length > 0) return ids
|
|
}
|
|
return Object.keys(operationParameters)
|
|
}
|
|
|
|
function resolveToolIdForOperation(blockConfig: BlockConfig, opId: string): string | undefined {
|
|
try {
|
|
const toolSelector = blockConfig.tools?.config?.tool
|
|
if (typeof toolSelector === 'function') {
|
|
const maybeToolId = toolSelector({ operation: opId })
|
|
if (typeof maybeToolId === 'string') return maybeToolId
|
|
}
|
|
} catch {}
|
|
return undefined
|
|
}
|
|
|
|
const DOCS_FILE_MAPPING: Record<string, string> = {}
|
|
|
|
const SPECIAL_BLOCKS_METADATA: Record<string, any> = {
|
|
loop: {
|
|
id: 'loop',
|
|
name: 'Loop',
|
|
description: 'Control flow block for iterating over collections or repeating actions',
|
|
longDescription:
|
|
'Control flow block for iterating over collections or repeating actions serially',
|
|
bestPractices: `
|
|
- Set reasonable limits for iterations.
|
|
- Use forEach for collection processing, for loops for fixed iterations.
|
|
- Cannot have loops/parallels inside a loop block.
|
|
- For yaml it needs to connect blocks inside to the start field of the block.
|
|
- IMPORTANT for while/doWhile: The condition is evaluated BEFORE each iteration starts, so blocks INSIDE the loop cannot be referenced in the condition (their outputs don't exist yet when the condition runs).
|
|
- For while/doWhile conditions, use: <loop.index> for iteration count, workflow variables (set by blocks OUTSIDE the loop), or references to blocks OUTSIDE the loop.
|
|
- To break a while/doWhile loop based on internal block results, use a variables block OUTSIDE the loop and update it from inside, then reference that variable in the condition.
|
|
`,
|
|
inputs: {
|
|
loopType: {
|
|
type: 'string',
|
|
required: true,
|
|
enum: ['for', 'forEach', 'while', 'doWhile'],
|
|
description:
|
|
"Loop Type - 'for' runs N times, 'forEach' iterates over collection, 'while' runs while condition is true, 'doWhile' runs at least once then checks condition",
|
|
},
|
|
iterations: {
|
|
type: 'number',
|
|
required: false,
|
|
minimum: 1,
|
|
maximum: 1000,
|
|
description: "Number of iterations (for 'for' loopType)",
|
|
example: 5,
|
|
},
|
|
collection: {
|
|
type: 'string',
|
|
required: false,
|
|
description: "Collection to iterate over (for 'forEach' loopType)",
|
|
example: '<previousblock.items>',
|
|
},
|
|
condition: {
|
|
type: 'string',
|
|
required: false,
|
|
description:
|
|
"Condition to evaluate (for 'while' and 'doWhile' loopType). IMPORTANT: Cannot reference blocks INSIDE the loop - use <loop.index>, workflow variables, or blocks OUTSIDE the loop instead.",
|
|
example: '<loop.index> < 10',
|
|
},
|
|
maxConcurrency: {
|
|
type: 'number',
|
|
required: false,
|
|
default: 1,
|
|
minimum: 1,
|
|
maximum: 10,
|
|
description: 'Max parallel executions (1 = sequential)',
|
|
example: 1,
|
|
},
|
|
},
|
|
outputs: {
|
|
results: { type: 'array', description: 'Array of results from each iteration' },
|
|
currentIndex: { type: 'number', description: 'Current iteration index (0-based)' },
|
|
currentItem: { type: 'any', description: 'Current item being iterated (for forEach loops)' },
|
|
totalIterations: { type: 'number', description: 'Total number of iterations' },
|
|
},
|
|
subBlocks: [
|
|
{
|
|
id: 'loopType',
|
|
title: 'Loop Type',
|
|
type: 'dropdown',
|
|
required: true,
|
|
options: [
|
|
{ label: 'For Loop (count)', id: 'for' },
|
|
{ label: 'For Each (collection)', id: 'forEach' },
|
|
{ label: 'While (condition)', id: 'while' },
|
|
{ label: 'Do While (condition)', id: 'doWhile' },
|
|
],
|
|
},
|
|
{
|
|
id: 'iterations',
|
|
title: 'Iterations',
|
|
type: 'slider',
|
|
min: 1,
|
|
max: 1000,
|
|
integer: true,
|
|
condition: { field: 'loopType', value: 'for' },
|
|
},
|
|
{
|
|
id: 'collection',
|
|
title: 'Collection',
|
|
type: 'short-input',
|
|
placeholder: 'Array or object to iterate over...',
|
|
condition: { field: 'loopType', value: 'forEach' },
|
|
},
|
|
{
|
|
id: 'condition',
|
|
title: 'Condition',
|
|
type: 'code',
|
|
language: 'javascript',
|
|
placeholder: '<loop.index> < 10 or <variable.variablename>',
|
|
description:
|
|
'Cannot reference blocks inside the loop. Use <loop.index>, workflow variables, or blocks outside the loop.',
|
|
condition: { field: 'loopType', value: ['while', 'doWhile'] },
|
|
},
|
|
{
|
|
id: 'maxConcurrency',
|
|
title: 'Max Concurrency',
|
|
type: 'slider',
|
|
min: 1,
|
|
max: 10,
|
|
integer: true,
|
|
default: 1,
|
|
},
|
|
],
|
|
},
|
|
parallel: {
|
|
id: 'parallel',
|
|
name: 'Parallel',
|
|
description: 'Control flow block for executing multiple branches simultaneously',
|
|
longDescription: 'Control flow block for executing multiple branches simultaneously',
|
|
bestPractices: `
|
|
- Keep structures inside simple. Cannot have multiple blocks within a parallel block.
|
|
- Cannot have loops/parallels inside a parallel block.
|
|
- Agent block combobox can be <parallel.currentItem> if the user wants to query multiple models in parallel. The collection has to be an array of correct model strings available for the agent block.
|
|
- For yaml it needs to connect blocks inside to the start field of the block.
|
|
`,
|
|
inputs: {
|
|
parallelType: {
|
|
type: 'string',
|
|
required: true,
|
|
enum: ['count', 'collection'],
|
|
description: "Parallel Type - 'count' runs N branches, 'collection' runs one per item",
|
|
},
|
|
count: {
|
|
type: 'number',
|
|
required: false,
|
|
minimum: 1,
|
|
maximum: 100,
|
|
description: "Number of parallel branches (for 'count' type)",
|
|
example: 3,
|
|
},
|
|
collection: {
|
|
type: 'string',
|
|
required: false,
|
|
description: "Collection to process in parallel (for 'collection' type)",
|
|
example: '<previousblock.items>',
|
|
},
|
|
maxConcurrency: {
|
|
type: 'number',
|
|
required: false,
|
|
default: 10,
|
|
minimum: 1,
|
|
maximum: 50,
|
|
description: 'Max concurrent executions at once',
|
|
example: 10,
|
|
},
|
|
},
|
|
outputs: {
|
|
results: { type: 'array', description: 'Array of results from all parallel branches' },
|
|
index: { type: 'number', description: 'Current branch index (0-based)' },
|
|
currentItem: {
|
|
type: 'any',
|
|
description: 'Current item for this branch (for collection type)',
|
|
},
|
|
items: { type: 'array', description: 'All distribution items' },
|
|
},
|
|
subBlocks: [
|
|
{
|
|
id: 'parallelType',
|
|
title: 'Parallel Type',
|
|
type: 'dropdown',
|
|
required: true,
|
|
options: [
|
|
{ label: 'Count (number)', id: 'count' },
|
|
{ label: 'Collection (array)', id: 'collection' },
|
|
],
|
|
},
|
|
{
|
|
id: 'count',
|
|
title: 'Count',
|
|
type: 'slider',
|
|
min: 1,
|
|
max: 100,
|
|
integer: true,
|
|
condition: { field: 'parallelType', value: 'count' },
|
|
},
|
|
{
|
|
id: 'collection',
|
|
title: 'Collection',
|
|
type: 'short-input',
|
|
placeholder: 'Array to process in parallel...',
|
|
condition: { field: 'parallelType', value: 'collection' },
|
|
},
|
|
{
|
|
id: 'maxConcurrency',
|
|
title: 'Max Concurrency',
|
|
type: 'slider',
|
|
min: 1,
|
|
max: 50,
|
|
integer: true,
|
|
default: 10,
|
|
},
|
|
],
|
|
},
|
|
}
|