Skip to content

Commit 3100daa

Browse files
authored
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
1 parent c252e88 commit 3100daa

File tree

6 files changed

+666
-0
lines changed

6 files changed

+666
-0
lines changed

apps/sim/lib/copilot/registry.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export const ToolIds = z.enum([
3636
'manage_custom_tool',
3737
'manage_mcp_tool',
3838
'sleep',
39+
'get_block_outputs',
40+
'get_block_upstream_references',
3941
])
4042
export type ToolId = z.infer<typeof ToolIds>
4143

@@ -277,6 +279,24 @@ export const ToolArgSchemas = {
277279
.max(180)
278280
.describe('The number of seconds to sleep (0-180, max 3 minutes)'),
279281
}),
282+
283+
get_block_outputs: z.object({
284+
blockIds: z
285+
.array(z.string())
286+
.optional()
287+
.describe(
288+
'Optional array of block UUIDs. If provided, returns outputs only for those blocks. If not provided, returns outputs for all blocks in the workflow.'
289+
),
290+
}),
291+
292+
get_block_upstream_references: z.object({
293+
blockIds: z
294+
.array(z.string())
295+
.min(1)
296+
.describe(
297+
'Array of block UUIDs. Returns all upstream references (block outputs and variables) accessible to each block based on workflow connections.'
298+
),
299+
}),
280300
} as const
281301
export type ToolArgSchemaMap = typeof ToolArgSchemas
282302

@@ -346,6 +366,11 @@ export const ToolSSESchemas = {
346366
manage_custom_tool: toolCallSSEFor('manage_custom_tool', ToolArgSchemas.manage_custom_tool),
347367
manage_mcp_tool: toolCallSSEFor('manage_mcp_tool', ToolArgSchemas.manage_mcp_tool),
348368
sleep: toolCallSSEFor('sleep', ToolArgSchemas.sleep),
369+
get_block_outputs: toolCallSSEFor('get_block_outputs', ToolArgSchemas.get_block_outputs),
370+
get_block_upstream_references: toolCallSSEFor(
371+
'get_block_upstream_references',
372+
ToolArgSchemas.get_block_upstream_references
373+
),
349374
} as const
350375
export type ToolSSESchemaMap = typeof ToolSSESchemas
351376

@@ -603,6 +628,60 @@ export const ToolResultSchemas = {
603628
seconds: z.number(),
604629
message: z.string().optional(),
605630
}),
631+
get_block_outputs: z.object({
632+
blocks: z.array(
633+
z.object({
634+
blockId: z.string(),
635+
blockName: z.string(),
636+
blockType: z.string(),
637+
outputs: z.array(z.string()),
638+
insideSubflowOutputs: z.array(z.string()).optional(),
639+
outsideSubflowOutputs: z.array(z.string()).optional(),
640+
})
641+
),
642+
variables: z.array(
643+
z.object({
644+
id: z.string(),
645+
name: z.string(),
646+
type: z.string(),
647+
tag: z.string(),
648+
})
649+
),
650+
}),
651+
get_block_upstream_references: z.object({
652+
results: z.array(
653+
z.object({
654+
blockId: z.string(),
655+
blockName: z.string(),
656+
insideSubflows: z
657+
.array(
658+
z.object({
659+
blockId: z.string(),
660+
blockName: z.string(),
661+
blockType: z.string(),
662+
})
663+
)
664+
.optional(),
665+
accessibleBlocks: z.array(
666+
z.object({
667+
blockId: z.string(),
668+
blockName: z.string(),
669+
blockType: z.string(),
670+
outputs: z.array(z.string()),
671+
accessContext: z.enum(['inside', 'outside']).optional(),
672+
})
673+
),
674+
variables: z.array(
675+
z.object({
676+
id: z.string(),
677+
name: z.string(),
678+
type: z.string(),
679+
tag: z.string(),
680+
})
681+
),
682+
})
683+
),
684+
}),
606685
} as const
607686
export type ToolResultSchemaMap = typeof ToolResultSchemas
608687

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import {
2+
extractFieldsFromSchema,
3+
parseResponseFormatSafely,
4+
} from '@/lib/core/utils/response-format'
5+
import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs'
6+
import { getBlock } from '@/blocks'
7+
import { useVariablesStore } from '@/stores/panel/variables/store'
8+
import type { Variable } from '@/stores/panel/variables/types'
9+
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
10+
import { normalizeName } from '@/stores/workflows/utils'
11+
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
12+
13+
export interface WorkflowContext {
14+
workflowId: string
15+
blocks: Record<string, BlockState>
16+
loops: Record<string, Loop>
17+
parallels: Record<string, Parallel>
18+
subBlockValues: Record<string, Record<string, any>>
19+
}
20+
21+
export interface VariableOutput {
22+
id: string
23+
name: string
24+
type: string
25+
tag: string
26+
}
27+
28+
export function getWorkflowSubBlockValues(workflowId: string): Record<string, Record<string, any>> {
29+
const subBlockStore = useSubBlockStore.getState()
30+
return subBlockStore.workflowValues[workflowId] ?? {}
31+
}
32+
33+
export function getMergedSubBlocks(
34+
blocks: Record<string, BlockState>,
35+
subBlockValues: Record<string, Record<string, any>>,
36+
targetBlockId: string
37+
): Record<string, any> {
38+
const base = blocks[targetBlockId]?.subBlocks || {}
39+
const live = subBlockValues?.[targetBlockId] || {}
40+
const merged: Record<string, any> = { ...base }
41+
for (const [subId, liveVal] of Object.entries(live)) {
42+
merged[subId] = { ...(base[subId] || {}), value: liveVal }
43+
}
44+
return merged
45+
}
46+
47+
export function getSubBlockValue(
48+
blocks: Record<string, BlockState>,
49+
subBlockValues: Record<string, Record<string, any>>,
50+
targetBlockId: string,
51+
subBlockId: string
52+
): any {
53+
const live = subBlockValues?.[targetBlockId]?.[subBlockId]
54+
if (live !== undefined) return live
55+
return blocks[targetBlockId]?.subBlocks?.[subBlockId]?.value
56+
}
57+
58+
export function getWorkflowVariables(workflowId: string): VariableOutput[] {
59+
const getVariablesByWorkflowId = useVariablesStore.getState().getVariablesByWorkflowId
60+
const workflowVariables = getVariablesByWorkflowId(workflowId)
61+
const validVariables = workflowVariables.filter(
62+
(variable: Variable) => variable.name.trim() !== ''
63+
)
64+
return validVariables.map((variable: Variable) => ({
65+
id: variable.id,
66+
name: variable.name,
67+
type: variable.type,
68+
tag: `variable.${normalizeName(variable.name)}`,
69+
}))
70+
}
71+
72+
export function getSubflowInsidePaths(
73+
blockType: 'loop' | 'parallel',
74+
blockId: string,
75+
loops: Record<string, Loop>,
76+
parallels: Record<string, Parallel>
77+
): string[] {
78+
const paths = ['index']
79+
if (blockType === 'loop') {
80+
const loopType = loops[blockId]?.loopType || 'for'
81+
if (loopType === 'forEach') {
82+
paths.push('currentItem', 'items')
83+
}
84+
} else {
85+
const parallelType = parallels[blockId]?.parallelType || 'count'
86+
if (parallelType === 'collection') {
87+
paths.push('currentItem', 'items')
88+
}
89+
}
90+
return paths
91+
}
92+
93+
export function computeBlockOutputPaths(block: BlockState, ctx: WorkflowContext): string[] {
94+
const { blocks, loops, parallels, subBlockValues } = ctx
95+
const blockConfig = getBlock(block.type)
96+
const mergedSubBlocks = getMergedSubBlocks(blocks, subBlockValues, block.id)
97+
98+
if (block.type === 'loop' || block.type === 'parallel') {
99+
const insidePaths = getSubflowInsidePaths(block.type, block.id, loops, parallels)
100+
return ['results', ...insidePaths]
101+
}
102+
103+
if (block.type === 'evaluator') {
104+
const metricsValue = getSubBlockValue(blocks, subBlockValues, block.id, 'metrics')
105+
if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) {
106+
const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name)
107+
return validMetrics.map((metric: { name: string }) => metric.name.toLowerCase())
108+
}
109+
return getBlockOutputPaths(block.type, mergedSubBlocks)
110+
}
111+
112+
if (block.type === 'variables') {
113+
const variablesValue = getSubBlockValue(blocks, subBlockValues, block.id, 'variables')
114+
if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) {
115+
const validAssignments = variablesValue.filter((assignment: { variableName?: string }) =>
116+
assignment?.variableName?.trim()
117+
)
118+
return validAssignments.map((assignment: { variableName: string }) =>
119+
assignment.variableName.trim()
120+
)
121+
}
122+
return []
123+
}
124+
125+
if (blockConfig) {
126+
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
127+
const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id)
128+
if (responseFormat) {
129+
const schemaFields = extractFieldsFromSchema(responseFormat)
130+
if (schemaFields.length > 0) {
131+
return schemaFields.map((field) => field.name)
132+
}
133+
}
134+
}
135+
136+
return getBlockOutputPaths(block.type, mergedSubBlocks, block.triggerMode)
137+
}
138+
139+
export function formatOutputsWithPrefix(paths: string[], blockName: string): string[] {
140+
const normalizedName = normalizeName(blockName)
141+
return paths.map((path) => `${normalizedName}.${path}`)
142+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { Loader2, Tag, X, XCircle } from 'lucide-react'
2+
import {
3+
BaseClientTool,
4+
type BaseClientToolMetadata,
5+
ClientToolCallState,
6+
} from '@/lib/copilot/tools/client/base-tool'
7+
import {
8+
computeBlockOutputPaths,
9+
formatOutputsWithPrefix,
10+
getSubflowInsidePaths,
11+
getWorkflowSubBlockValues,
12+
getWorkflowVariables,
13+
} from '@/lib/copilot/tools/client/workflow/block-output-utils'
14+
import {
15+
GetBlockOutputsResult,
16+
type GetBlockOutputsResultType,
17+
} from '@/lib/copilot/tools/shared/schemas'
18+
import { createLogger } from '@/lib/logs/console/logger'
19+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
20+
import { normalizeName } from '@/stores/workflows/utils'
21+
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
22+
23+
const logger = createLogger('GetBlockOutputsClientTool')
24+
25+
interface GetBlockOutputsArgs {
26+
blockIds?: string[]
27+
}
28+
29+
export class GetBlockOutputsClientTool extends BaseClientTool {
30+
static readonly id = 'get_block_outputs'
31+
32+
constructor(toolCallId: string) {
33+
super(toolCallId, GetBlockOutputsClientTool.id, GetBlockOutputsClientTool.metadata)
34+
}
35+
36+
static readonly metadata: BaseClientToolMetadata = {
37+
displayNames: {
38+
[ClientToolCallState.generating]: { text: 'Getting block outputs', icon: Loader2 },
39+
[ClientToolCallState.pending]: { text: 'Getting block outputs', icon: Tag },
40+
[ClientToolCallState.executing]: { text: 'Getting block outputs', icon: Loader2 },
41+
[ClientToolCallState.aborted]: { text: 'Aborted getting outputs', icon: XCircle },
42+
[ClientToolCallState.success]: { text: 'Retrieved block outputs', icon: Tag },
43+
[ClientToolCallState.error]: { text: 'Failed to get outputs', icon: X },
44+
[ClientToolCallState.rejected]: { text: 'Skipped getting outputs', icon: XCircle },
45+
},
46+
getDynamicText: (params, state) => {
47+
const blockIds = params?.blockIds
48+
if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) {
49+
const count = blockIds.length
50+
switch (state) {
51+
case ClientToolCallState.success:
52+
return `Retrieved outputs for ${count} block${count > 1 ? 's' : ''}`
53+
case ClientToolCallState.executing:
54+
case ClientToolCallState.generating:
55+
case ClientToolCallState.pending:
56+
return `Getting outputs for ${count} block${count > 1 ? 's' : ''}`
57+
case ClientToolCallState.error:
58+
return `Failed to get outputs for ${count} block${count > 1 ? 's' : ''}`
59+
}
60+
}
61+
return undefined
62+
},
63+
}
64+
65+
async execute(args?: GetBlockOutputsArgs): Promise<void> {
66+
try {
67+
this.setState(ClientToolCallState.executing)
68+
69+
const { activeWorkflowId } = useWorkflowRegistry.getState()
70+
if (!activeWorkflowId) {
71+
await this.markToolComplete(400, 'No active workflow found')
72+
this.setState(ClientToolCallState.error)
73+
return
74+
}
75+
76+
const workflowStore = useWorkflowStore.getState()
77+
const blocks = workflowStore.blocks || {}
78+
const loops = workflowStore.loops || {}
79+
const parallels = workflowStore.parallels || {}
80+
const subBlockValues = getWorkflowSubBlockValues(activeWorkflowId)
81+
82+
const ctx = { workflowId: activeWorkflowId, blocks, loops, parallels, subBlockValues }
83+
const targetBlockIds =
84+
args?.blockIds && args.blockIds.length > 0 ? args.blockIds : Object.keys(blocks)
85+
86+
const blockOutputs: GetBlockOutputsResultType['blocks'] = []
87+
88+
for (const blockId of targetBlockIds) {
89+
const block = blocks[blockId]
90+
if (!block?.type) continue
91+
92+
const blockName = block.name || block.type
93+
const normalizedBlockName = normalizeName(blockName)
94+
95+
let insideSubflowOutputs: string[] | undefined
96+
let outsideSubflowOutputs: string[] | undefined
97+
98+
const blockOutput: GetBlockOutputsResultType['blocks'][0] = {
99+
blockId,
100+
blockName,
101+
blockType: block.type,
102+
outputs: [],
103+
}
104+
105+
if (block.type === 'loop' || block.type === 'parallel') {
106+
const insidePaths = getSubflowInsidePaths(block.type, blockId, loops, parallels)
107+
blockOutput.insideSubflowOutputs = formatOutputsWithPrefix(insidePaths, blockName)
108+
blockOutput.outsideSubflowOutputs = formatOutputsWithPrefix(['results'], blockName)
109+
} else {
110+
const outputPaths = computeBlockOutputPaths(block, ctx)
111+
blockOutput.outputs = formatOutputsWithPrefix(outputPaths, blockName)
112+
}
113+
114+
blockOutputs.push(blockOutput)
115+
}
116+
117+
const includeVariables = !args?.blockIds || args.blockIds.length === 0
118+
const resultData: {
119+
blocks: typeof blockOutputs
120+
variables?: ReturnType<typeof getWorkflowVariables>
121+
} = {
122+
blocks: blockOutputs,
123+
}
124+
if (includeVariables) {
125+
resultData.variables = getWorkflowVariables(activeWorkflowId)
126+
}
127+
128+
const result = GetBlockOutputsResult.parse(resultData)
129+
130+
logger.info('Retrieved block outputs', {
131+
blockCount: blockOutputs.length,
132+
variableCount: resultData.variables?.length ?? 0,
133+
})
134+
135+
await this.markToolComplete(200, 'Retrieved block outputs', result)
136+
this.setState(ClientToolCallState.success)
137+
} catch (error: any) {
138+
const message = error instanceof Error ? error.message : String(error)
139+
logger.error('Error in tool execution', { toolCallId: this.toolCallId, error, message })
140+
await this.markToolComplete(500, message || 'Failed to get block outputs')
141+
this.setState(ClientToolCallState.error)
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)