mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
feat(copilot): add tools to access block outputs and upstream references (#2546)
* Add copilot references tools * Minor fixes * Omit vars field in block outputs when id is provided
This commit is contained in:
committed by
GitHub
parent
c252e885af
commit
3100daa346
@@ -36,6 +36,8 @@ export const ToolIds = z.enum([
|
||||
'manage_custom_tool',
|
||||
'manage_mcp_tool',
|
||||
'sleep',
|
||||
'get_block_outputs',
|
||||
'get_block_upstream_references',
|
||||
])
|
||||
export type ToolId = z.infer<typeof ToolIds>
|
||||
|
||||
@@ -277,6 +279,24 @@ export const ToolArgSchemas = {
|
||||
.max(180)
|
||||
.describe('The number of seconds to sleep (0-180, max 3 minutes)'),
|
||||
}),
|
||||
|
||||
get_block_outputs: z.object({
|
||||
blockIds: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional array of block UUIDs. If provided, returns outputs only for those blocks. If not provided, returns outputs for all blocks in the workflow.'
|
||||
),
|
||||
}),
|
||||
|
||||
get_block_upstream_references: z.object({
|
||||
blockIds: z
|
||||
.array(z.string())
|
||||
.min(1)
|
||||
.describe(
|
||||
'Array of block UUIDs. Returns all upstream references (block outputs and variables) accessible to each block based on workflow connections.'
|
||||
),
|
||||
}),
|
||||
} as const
|
||||
export type ToolArgSchemaMap = typeof ToolArgSchemas
|
||||
|
||||
@@ -346,6 +366,11 @@ export const ToolSSESchemas = {
|
||||
manage_custom_tool: toolCallSSEFor('manage_custom_tool', ToolArgSchemas.manage_custom_tool),
|
||||
manage_mcp_tool: toolCallSSEFor('manage_mcp_tool', ToolArgSchemas.manage_mcp_tool),
|
||||
sleep: toolCallSSEFor('sleep', ToolArgSchemas.sleep),
|
||||
get_block_outputs: toolCallSSEFor('get_block_outputs', ToolArgSchemas.get_block_outputs),
|
||||
get_block_upstream_references: toolCallSSEFor(
|
||||
'get_block_upstream_references',
|
||||
ToolArgSchemas.get_block_upstream_references
|
||||
),
|
||||
} as const
|
||||
export type ToolSSESchemaMap = typeof ToolSSESchemas
|
||||
|
||||
@@ -603,6 +628,60 @@ export const ToolResultSchemas = {
|
||||
seconds: z.number(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
get_block_outputs: z.object({
|
||||
blocks: z.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
blockType: z.string(),
|
||||
outputs: z.array(z.string()),
|
||||
insideSubflowOutputs: z.array(z.string()).optional(),
|
||||
outsideSubflowOutputs: z.array(z.string()).optional(),
|
||||
})
|
||||
),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
tag: z.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
get_block_upstream_references: z.object({
|
||||
results: z.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
insideSubflows: z
|
||||
.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
blockType: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
accessibleBlocks: z.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
blockType: z.string(),
|
||||
outputs: z.array(z.string()),
|
||||
accessContext: z.enum(['inside', 'outside']).optional(),
|
||||
})
|
||||
),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
tag: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
}),
|
||||
} as const
|
||||
export type ToolResultSchemaMap = typeof ToolResultSchemas
|
||||
|
||||
|
||||
142
apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts
Normal file
142
apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
extractFieldsFromSchema,
|
||||
parseResponseFormatSafely,
|
||||
} from '@/lib/core/utils/response-format'
|
||||
import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import type { Variable } from '@/stores/panel/variables/types'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
|
||||
export interface WorkflowContext {
|
||||
workflowId: string
|
||||
blocks: Record<string, BlockState>
|
||||
loops: Record<string, Loop>
|
||||
parallels: Record<string, Parallel>
|
||||
subBlockValues: Record<string, Record<string, any>>
|
||||
}
|
||||
|
||||
export interface VariableOutput {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
export function getWorkflowSubBlockValues(workflowId: string): Record<string, Record<string, any>> {
|
||||
const subBlockStore = useSubBlockStore.getState()
|
||||
return subBlockStore.workflowValues[workflowId] ?? {}
|
||||
}
|
||||
|
||||
export function getMergedSubBlocks(
|
||||
blocks: Record<string, BlockState>,
|
||||
subBlockValues: Record<string, Record<string, any>>,
|
||||
targetBlockId: string
|
||||
): Record<string, any> {
|
||||
const base = blocks[targetBlockId]?.subBlocks || {}
|
||||
const live = subBlockValues?.[targetBlockId] || {}
|
||||
const merged: Record<string, any> = { ...base }
|
||||
for (const [subId, liveVal] of Object.entries(live)) {
|
||||
merged[subId] = { ...(base[subId] || {}), value: liveVal }
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
export function getSubBlockValue(
|
||||
blocks: Record<string, BlockState>,
|
||||
subBlockValues: Record<string, Record<string, any>>,
|
||||
targetBlockId: string,
|
||||
subBlockId: string
|
||||
): any {
|
||||
const live = subBlockValues?.[targetBlockId]?.[subBlockId]
|
||||
if (live !== undefined) return live
|
||||
return blocks[targetBlockId]?.subBlocks?.[subBlockId]?.value
|
||||
}
|
||||
|
||||
export function getWorkflowVariables(workflowId: string): VariableOutput[] {
|
||||
const getVariablesByWorkflowId = useVariablesStore.getState().getVariablesByWorkflowId
|
||||
const workflowVariables = getVariablesByWorkflowId(workflowId)
|
||||
const validVariables = workflowVariables.filter(
|
||||
(variable: Variable) => variable.name.trim() !== ''
|
||||
)
|
||||
return validVariables.map((variable: Variable) => ({
|
||||
id: variable.id,
|
||||
name: variable.name,
|
||||
type: variable.type,
|
||||
tag: `variable.${normalizeName(variable.name)}`,
|
||||
}))
|
||||
}
|
||||
|
||||
export function getSubflowInsidePaths(
|
||||
blockType: 'loop' | 'parallel',
|
||||
blockId: string,
|
||||
loops: Record<string, Loop>,
|
||||
parallels: Record<string, Parallel>
|
||||
): string[] {
|
||||
const paths = ['index']
|
||||
if (blockType === 'loop') {
|
||||
const loopType = loops[blockId]?.loopType || 'for'
|
||||
if (loopType === 'forEach') {
|
||||
paths.push('currentItem', 'items')
|
||||
}
|
||||
} else {
|
||||
const parallelType = parallels[blockId]?.parallelType || 'count'
|
||||
if (parallelType === 'collection') {
|
||||
paths.push('currentItem', 'items')
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
export function computeBlockOutputPaths(block: BlockState, ctx: WorkflowContext): string[] {
|
||||
const { blocks, loops, parallels, subBlockValues } = ctx
|
||||
const blockConfig = getBlock(block.type)
|
||||
const mergedSubBlocks = getMergedSubBlocks(blocks, subBlockValues, block.id)
|
||||
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
const insidePaths = getSubflowInsidePaths(block.type, block.id, loops, parallels)
|
||||
return ['results', ...insidePaths]
|
||||
}
|
||||
|
||||
if (block.type === 'evaluator') {
|
||||
const metricsValue = getSubBlockValue(blocks, subBlockValues, block.id, 'metrics')
|
||||
if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) {
|
||||
const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name)
|
||||
return validMetrics.map((metric: { name: string }) => metric.name.toLowerCase())
|
||||
}
|
||||
return getBlockOutputPaths(block.type, mergedSubBlocks)
|
||||
}
|
||||
|
||||
if (block.type === 'variables') {
|
||||
const variablesValue = getSubBlockValue(blocks, subBlockValues, block.id, 'variables')
|
||||
if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) {
|
||||
const validAssignments = variablesValue.filter((assignment: { variableName?: string }) =>
|
||||
assignment?.variableName?.trim()
|
||||
)
|
||||
return validAssignments.map((assignment: { variableName: string }) =>
|
||||
assignment.variableName.trim()
|
||||
)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
if (blockConfig) {
|
||||
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
||||
const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id)
|
||||
if (responseFormat) {
|
||||
const schemaFields = extractFieldsFromSchema(responseFormat)
|
||||
if (schemaFields.length > 0) {
|
||||
return schemaFields.map((field) => field.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getBlockOutputPaths(block.type, mergedSubBlocks, block.triggerMode)
|
||||
}
|
||||
|
||||
export function formatOutputsWithPrefix(paths: string[], blockName: string): string[] {
|
||||
const normalizedName = normalizeName(blockName)
|
||||
return paths.map((path) => `${normalizedName}.${path}`)
|
||||
}
|
||||
144
apps/sim/lib/copilot/tools/client/workflow/get-block-outputs.ts
Normal file
144
apps/sim/lib/copilot/tools/client/workflow/get-block-outputs.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Loader2, Tag, X, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import {
|
||||
computeBlockOutputPaths,
|
||||
formatOutputsWithPrefix,
|
||||
getSubflowInsidePaths,
|
||||
getWorkflowSubBlockValues,
|
||||
getWorkflowVariables,
|
||||
} from '@/lib/copilot/tools/client/workflow/block-output-utils'
|
||||
import {
|
||||
GetBlockOutputsResult,
|
||||
type GetBlockOutputsResultType,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('GetBlockOutputsClientTool')
|
||||
|
||||
interface GetBlockOutputsArgs {
|
||||
blockIds?: string[]
|
||||
}
|
||||
|
||||
export class GetBlockOutputsClientTool extends BaseClientTool {
|
||||
static readonly id = 'get_block_outputs'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, GetBlockOutputsClientTool.id, GetBlockOutputsClientTool.metadata)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Getting block outputs', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Getting block outputs', icon: Tag },
|
||||
[ClientToolCallState.executing]: { text: 'Getting block outputs', icon: Loader2 },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted getting outputs', icon: XCircle },
|
||||
[ClientToolCallState.success]: { text: 'Retrieved block outputs', icon: Tag },
|
||||
[ClientToolCallState.error]: { text: 'Failed to get outputs', icon: X },
|
||||
[ClientToolCallState.rejected]: { text: 'Skipped getting outputs', icon: XCircle },
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
const blockIds = params?.blockIds
|
||||
if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) {
|
||||
const count = blockIds.length
|
||||
switch (state) {
|
||||
case ClientToolCallState.success:
|
||||
return `Retrieved outputs for ${count} block${count > 1 ? 's' : ''}`
|
||||
case ClientToolCallState.executing:
|
||||
case ClientToolCallState.generating:
|
||||
case ClientToolCallState.pending:
|
||||
return `Getting outputs for ${count} block${count > 1 ? 's' : ''}`
|
||||
case ClientToolCallState.error:
|
||||
return `Failed to get outputs for ${count} block${count > 1 ? 's' : ''}`
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
|
||||
async execute(args?: GetBlockOutputsArgs): Promise<void> {
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
const { activeWorkflowId } = useWorkflowRegistry.getState()
|
||||
if (!activeWorkflowId) {
|
||||
await this.markToolComplete(400, 'No active workflow found')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
const workflowStore = useWorkflowStore.getState()
|
||||
const blocks = workflowStore.blocks || {}
|
||||
const loops = workflowStore.loops || {}
|
||||
const parallels = workflowStore.parallels || {}
|
||||
const subBlockValues = getWorkflowSubBlockValues(activeWorkflowId)
|
||||
|
||||
const ctx = { workflowId: activeWorkflowId, blocks, loops, parallels, subBlockValues }
|
||||
const targetBlockIds =
|
||||
args?.blockIds && args.blockIds.length > 0 ? args.blockIds : Object.keys(blocks)
|
||||
|
||||
const blockOutputs: GetBlockOutputsResultType['blocks'] = []
|
||||
|
||||
for (const blockId of targetBlockIds) {
|
||||
const block = blocks[blockId]
|
||||
if (!block?.type) continue
|
||||
|
||||
const blockName = block.name || block.type
|
||||
const normalizedBlockName = normalizeName(blockName)
|
||||
|
||||
let insideSubflowOutputs: string[] | undefined
|
||||
let outsideSubflowOutputs: string[] | undefined
|
||||
|
||||
const blockOutput: GetBlockOutputsResultType['blocks'][0] = {
|
||||
blockId,
|
||||
blockName,
|
||||
blockType: block.type,
|
||||
outputs: [],
|
||||
}
|
||||
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
const insidePaths = getSubflowInsidePaths(block.type, blockId, loops, parallels)
|
||||
blockOutput.insideSubflowOutputs = formatOutputsWithPrefix(insidePaths, blockName)
|
||||
blockOutput.outsideSubflowOutputs = formatOutputsWithPrefix(['results'], blockName)
|
||||
} else {
|
||||
const outputPaths = computeBlockOutputPaths(block, ctx)
|
||||
blockOutput.outputs = formatOutputsWithPrefix(outputPaths, blockName)
|
||||
}
|
||||
|
||||
blockOutputs.push(blockOutput)
|
||||
}
|
||||
|
||||
const includeVariables = !args?.blockIds || args.blockIds.length === 0
|
||||
const resultData: {
|
||||
blocks: typeof blockOutputs
|
||||
variables?: ReturnType<typeof getWorkflowVariables>
|
||||
} = {
|
||||
blocks: blockOutputs,
|
||||
}
|
||||
if (includeVariables) {
|
||||
resultData.variables = getWorkflowVariables(activeWorkflowId)
|
||||
}
|
||||
|
||||
const result = GetBlockOutputsResult.parse(resultData)
|
||||
|
||||
logger.info('Retrieved block outputs', {
|
||||
blockCount: blockOutputs.length,
|
||||
variableCount: resultData.variables?.length ?? 0,
|
||||
})
|
||||
|
||||
await this.markToolComplete(200, 'Retrieved block outputs', result)
|
||||
this.setState(ClientToolCallState.success)
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logger.error('Error in tool execution', { toolCallId: this.toolCallId, error, message })
|
||||
await this.markToolComplete(500, message || 'Failed to get block outputs')
|
||||
this.setState(ClientToolCallState.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { GitBranch, Loader2, X, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import {
|
||||
computeBlockOutputPaths,
|
||||
formatOutputsWithPrefix,
|
||||
getSubflowInsidePaths,
|
||||
getWorkflowSubBlockValues,
|
||||
getWorkflowVariables,
|
||||
} from '@/lib/copilot/tools/client/workflow/block-output-utils'
|
||||
import {
|
||||
GetBlockUpstreamReferencesResult,
|
||||
type GetBlockUpstreamReferencesResultType,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('GetBlockUpstreamReferencesClientTool')
|
||||
|
||||
interface GetBlockUpstreamReferencesArgs {
|
||||
blockIds: string[]
|
||||
}
|
||||
|
||||
export class GetBlockUpstreamReferencesClientTool extends BaseClientTool {
|
||||
static readonly id = 'get_block_upstream_references'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(
|
||||
toolCallId,
|
||||
GetBlockUpstreamReferencesClientTool.id,
|
||||
GetBlockUpstreamReferencesClientTool.metadata
|
||||
)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Getting upstream references', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Getting upstream references', icon: GitBranch },
|
||||
[ClientToolCallState.executing]: { text: 'Getting upstream references', icon: Loader2 },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted getting references', icon: XCircle },
|
||||
[ClientToolCallState.success]: { text: 'Retrieved upstream references', icon: GitBranch },
|
||||
[ClientToolCallState.error]: { text: 'Failed to get references', icon: X },
|
||||
[ClientToolCallState.rejected]: { text: 'Skipped getting references', icon: XCircle },
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
const blockIds = params?.blockIds
|
||||
if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) {
|
||||
const count = blockIds.length
|
||||
switch (state) {
|
||||
case ClientToolCallState.success:
|
||||
return `Retrieved references for ${count} block${count > 1 ? 's' : ''}`
|
||||
case ClientToolCallState.executing:
|
||||
case ClientToolCallState.generating:
|
||||
case ClientToolCallState.pending:
|
||||
return `Getting references for ${count} block${count > 1 ? 's' : ''}`
|
||||
case ClientToolCallState.error:
|
||||
return `Failed to get references for ${count} block${count > 1 ? 's' : ''}`
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
|
||||
async execute(args?: GetBlockUpstreamReferencesArgs): Promise<void> {
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
if (!args?.blockIds || args.blockIds.length === 0) {
|
||||
await this.markToolComplete(400, 'blockIds array is required')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
const { activeWorkflowId } = useWorkflowRegistry.getState()
|
||||
if (!activeWorkflowId) {
|
||||
await this.markToolComplete(400, 'No active workflow found')
|
||||
this.setState(ClientToolCallState.error)
|
||||
return
|
||||
}
|
||||
|
||||
const workflowStore = useWorkflowStore.getState()
|
||||
const blocks = workflowStore.blocks || {}
|
||||
const edges = workflowStore.edges || []
|
||||
const loops = workflowStore.loops || {}
|
||||
const parallels = workflowStore.parallels || {}
|
||||
const subBlockValues = getWorkflowSubBlockValues(activeWorkflowId)
|
||||
|
||||
const ctx = { workflowId: activeWorkflowId, blocks, loops, parallels, subBlockValues }
|
||||
const variableOutputs = getWorkflowVariables(activeWorkflowId)
|
||||
const graphEdges = edges.map((edge) => ({ source: edge.source, target: edge.target }))
|
||||
|
||||
const results: GetBlockUpstreamReferencesResultType['results'] = []
|
||||
|
||||
for (const blockId of args.blockIds) {
|
||||
const targetBlock = blocks[blockId]
|
||||
if (!targetBlock) {
|
||||
logger.warn(`Block ${blockId} not found`)
|
||||
continue
|
||||
}
|
||||
|
||||
const insideSubflows: { blockId: string; blockName: string; blockType: string }[] = []
|
||||
const containingLoopIds = new Set<string>()
|
||||
const containingParallelIds = new Set<string>()
|
||||
|
||||
Object.values(loops as Record<string, Loop>).forEach((loop) => {
|
||||
if (loop?.nodes?.includes(blockId)) {
|
||||
containingLoopIds.add(loop.id)
|
||||
const loopBlock = blocks[loop.id]
|
||||
if (loopBlock) {
|
||||
insideSubflows.push({
|
||||
blockId: loop.id,
|
||||
blockName: loopBlock.name || loopBlock.type,
|
||||
blockType: 'loop',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.values(parallels as Record<string, Parallel>).forEach((parallel) => {
|
||||
if (parallel?.nodes?.includes(blockId)) {
|
||||
containingParallelIds.add(parallel.id)
|
||||
const parallelBlock = blocks[parallel.id]
|
||||
if (parallelBlock) {
|
||||
insideSubflows.push({
|
||||
blockId: parallel.id,
|
||||
blockName: parallelBlock.name || parallelBlock.type,
|
||||
blockType: 'parallel',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const ancestorIds = BlockPathCalculator.findAllPathNodes(graphEdges, blockId)
|
||||
const accessibleIds = new Set<string>(ancestorIds)
|
||||
accessibleIds.add(blockId)
|
||||
|
||||
const starterBlock = Object.values(blocks).find(
|
||||
(b) => b.type === 'starter' || b.type === 'start_trigger'
|
||||
)
|
||||
if (starterBlock && ancestorIds.includes(starterBlock.id)) {
|
||||
accessibleIds.add(starterBlock.id)
|
||||
}
|
||||
|
||||
containingLoopIds.forEach((loopId) => {
|
||||
accessibleIds.add(loopId)
|
||||
loops[loopId]?.nodes?.forEach((nodeId) => accessibleIds.add(nodeId))
|
||||
})
|
||||
|
||||
containingParallelIds.forEach((parallelId) => {
|
||||
accessibleIds.add(parallelId)
|
||||
parallels[parallelId]?.nodes?.forEach((nodeId) => accessibleIds.add(nodeId))
|
||||
})
|
||||
|
||||
const accessibleBlocks: GetBlockUpstreamReferencesResultType['results'][0]['accessibleBlocks'] =
|
||||
[]
|
||||
|
||||
for (const accessibleBlockId of accessibleIds) {
|
||||
const block = blocks[accessibleBlockId]
|
||||
if (!block?.type) continue
|
||||
|
||||
const canSelfReference = block.type === 'approval' || block.type === 'human_in_the_loop'
|
||||
if (accessibleBlockId === blockId && !canSelfReference) continue
|
||||
|
||||
const blockName = block.name || block.type
|
||||
let accessContext: 'inside' | 'outside' | undefined
|
||||
let outputPaths: string[]
|
||||
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
const isInside =
|
||||
(block.type === 'loop' && containingLoopIds.has(accessibleBlockId)) ||
|
||||
(block.type === 'parallel' && containingParallelIds.has(accessibleBlockId))
|
||||
|
||||
accessContext = isInside ? 'inside' : 'outside'
|
||||
outputPaths = isInside
|
||||
? getSubflowInsidePaths(block.type, accessibleBlockId, loops, parallels)
|
||||
: ['results']
|
||||
} else {
|
||||
outputPaths = computeBlockOutputPaths(block, ctx)
|
||||
}
|
||||
|
||||
const formattedOutputs = formatOutputsWithPrefix(outputPaths, blockName)
|
||||
|
||||
const entry: GetBlockUpstreamReferencesResultType['results'][0]['accessibleBlocks'][0] = {
|
||||
blockId: accessibleBlockId,
|
||||
blockName,
|
||||
blockType: block.type,
|
||||
outputs: formattedOutputs,
|
||||
}
|
||||
|
||||
if (accessContext) entry.accessContext = accessContext
|
||||
accessibleBlocks.push(entry)
|
||||
}
|
||||
|
||||
const resultEntry: GetBlockUpstreamReferencesResultType['results'][0] = {
|
||||
blockId,
|
||||
blockName: targetBlock.name || targetBlock.type,
|
||||
accessibleBlocks,
|
||||
variables: variableOutputs,
|
||||
}
|
||||
|
||||
if (insideSubflows.length > 0) resultEntry.insideSubflows = insideSubflows
|
||||
results.push(resultEntry)
|
||||
}
|
||||
|
||||
const result = GetBlockUpstreamReferencesResult.parse({ results })
|
||||
|
||||
logger.info('Retrieved upstream references', {
|
||||
blockIds: args.blockIds,
|
||||
resultCount: results.length,
|
||||
})
|
||||
|
||||
await this.markToolComplete(200, 'Retrieved upstream references', result)
|
||||
this.setState(ClientToolCallState.success)
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logger.error('Error in tool execution', { toolCallId: this.toolCallId, error, message })
|
||||
await this.markToolComplete(500, message || 'Failed to get upstream references')
|
||||
this.setState(ClientToolCallState.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,3 +104,71 @@ export const KnowledgeBaseResultSchema = z.object({
|
||||
data: z.any().optional(),
|
||||
})
|
||||
export type KnowledgeBaseResult = z.infer<typeof KnowledgeBaseResultSchema>
|
||||
|
||||
export const GetBlockOutputsInput = z.object({
|
||||
blockIds: z.array(z.string()).optional(),
|
||||
})
|
||||
export const GetBlockOutputsResult = z.object({
|
||||
blocks: z.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
blockType: z.string(),
|
||||
outputs: z.array(z.string()),
|
||||
insideSubflowOutputs: z.array(z.string()).optional(),
|
||||
outsideSubflowOutputs: z.array(z.string()).optional(),
|
||||
})
|
||||
),
|
||||
variables: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
tag: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
export type GetBlockOutputsInputType = z.infer<typeof GetBlockOutputsInput>
|
||||
export type GetBlockOutputsResultType = z.infer<typeof GetBlockOutputsResult>
|
||||
|
||||
export const GetBlockUpstreamReferencesInput = z.object({
|
||||
blockIds: z.array(z.string()).min(1),
|
||||
})
|
||||
export const GetBlockUpstreamReferencesResult = z.object({
|
||||
results: z.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
insideSubflows: z
|
||||
.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
blockType: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
accessibleBlocks: z.array(
|
||||
z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
blockType: z.string(),
|
||||
outputs: z.array(z.string()),
|
||||
accessContext: z.enum(['inside', 'outside']).optional(),
|
||||
})
|
||||
),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
tag: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
export type GetBlockUpstreamReferencesInputType = z.infer<typeof GetBlockUpstreamReferencesInput>
|
||||
export type GetBlockUpstreamReferencesResultType = z.infer<typeof GetBlockUpstreamReferencesResult>
|
||||
|
||||
@@ -41,6 +41,8 @@ import { SetEnvironmentVariablesClientTool } from '@/lib/copilot/tools/client/us
|
||||
import { CheckDeploymentStatusClientTool } from '@/lib/copilot/tools/client/workflow/check-deployment-status'
|
||||
import { DeployWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/deploy-workflow'
|
||||
import { EditWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/edit-workflow'
|
||||
import { GetBlockOutputsClientTool } from '@/lib/copilot/tools/client/workflow/get-block-outputs'
|
||||
import { GetBlockUpstreamReferencesClientTool } from '@/lib/copilot/tools/client/workflow/get-block-upstream-references'
|
||||
import { GetUserWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/get-user-workflow'
|
||||
import { GetWorkflowConsoleClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-console'
|
||||
import { GetWorkflowDataClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-data'
|
||||
@@ -110,6 +112,8 @@ const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
|
||||
manage_custom_tool: (id) => new ManageCustomToolClientTool(id),
|
||||
manage_mcp_tool: (id) => new ManageMcpToolClientTool(id),
|
||||
sleep: (id) => new SleepClientTool(id),
|
||||
get_block_outputs: (id) => new GetBlockOutputsClientTool(id),
|
||||
get_block_upstream_references: (id) => new GetBlockUpstreamReferencesClientTool(id),
|
||||
}
|
||||
|
||||
// Read-only static metadata for class-based tools (no instances)
|
||||
@@ -150,6 +154,8 @@ export const CLASS_TOOL_METADATA: Record<string, BaseClientToolMetadata | undefi
|
||||
manage_custom_tool: (ManageCustomToolClientTool as any)?.metadata,
|
||||
manage_mcp_tool: (ManageMcpToolClientTool as any)?.metadata,
|
||||
sleep: (SleepClientTool as any)?.metadata,
|
||||
get_block_outputs: (GetBlockOutputsClientTool as any)?.metadata,
|
||||
get_block_upstream_references: (GetBlockUpstreamReferencesClientTool as any)?.metadata,
|
||||
}
|
||||
|
||||
function ensureClientToolInstance(toolName: string | undefined, toolCallId: string | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user