mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -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_custom_tool',
|
||||||
'manage_mcp_tool',
|
'manage_mcp_tool',
|
||||||
'sleep',
|
'sleep',
|
||||||
|
'get_block_outputs',
|
||||||
|
'get_block_upstream_references',
|
||||||
])
|
])
|
||||||
export type ToolId = z.infer<typeof ToolIds>
|
export type ToolId = z.infer<typeof ToolIds>
|
||||||
|
|
||||||
@@ -277,6 +279,24 @@ export const ToolArgSchemas = {
|
|||||||
.max(180)
|
.max(180)
|
||||||
.describe('The number of seconds to sleep (0-180, max 3 minutes)'),
|
.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
|
} as const
|
||||||
export type ToolArgSchemaMap = typeof ToolArgSchemas
|
export type ToolArgSchemaMap = typeof ToolArgSchemas
|
||||||
|
|
||||||
@@ -346,6 +366,11 @@ export const ToolSSESchemas = {
|
|||||||
manage_custom_tool: toolCallSSEFor('manage_custom_tool', ToolArgSchemas.manage_custom_tool),
|
manage_custom_tool: toolCallSSEFor('manage_custom_tool', ToolArgSchemas.manage_custom_tool),
|
||||||
manage_mcp_tool: toolCallSSEFor('manage_mcp_tool', ToolArgSchemas.manage_mcp_tool),
|
manage_mcp_tool: toolCallSSEFor('manage_mcp_tool', ToolArgSchemas.manage_mcp_tool),
|
||||||
sleep: toolCallSSEFor('sleep', ToolArgSchemas.sleep),
|
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
|
} as const
|
||||||
export type ToolSSESchemaMap = typeof ToolSSESchemas
|
export type ToolSSESchemaMap = typeof ToolSSESchemas
|
||||||
|
|
||||||
@@ -603,6 +628,60 @@ export const ToolResultSchemas = {
|
|||||||
seconds: z.number(),
|
seconds: z.number(),
|
||||||
message: z.string().optional(),
|
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
|
} as const
|
||||||
export type ToolResultSchemaMap = typeof ToolResultSchemas
|
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(),
|
data: z.any().optional(),
|
||||||
})
|
})
|
||||||
export type KnowledgeBaseResult = z.infer<typeof KnowledgeBaseResultSchema>
|
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 { CheckDeploymentStatusClientTool } from '@/lib/copilot/tools/client/workflow/check-deployment-status'
|
||||||
import { DeployWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/deploy-workflow'
|
import { DeployWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/deploy-workflow'
|
||||||
import { EditWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/edit-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 { GetUserWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/get-user-workflow'
|
||||||
import { GetWorkflowConsoleClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-console'
|
import { GetWorkflowConsoleClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-console'
|
||||||
import { GetWorkflowDataClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-data'
|
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_custom_tool: (id) => new ManageCustomToolClientTool(id),
|
||||||
manage_mcp_tool: (id) => new ManageMcpToolClientTool(id),
|
manage_mcp_tool: (id) => new ManageMcpToolClientTool(id),
|
||||||
sleep: (id) => new SleepClientTool(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)
|
// 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_custom_tool: (ManageCustomToolClientTool as any)?.metadata,
|
||||||
manage_mcp_tool: (ManageMcpToolClientTool as any)?.metadata,
|
manage_mcp_tool: (ManageMcpToolClientTool as any)?.metadata,
|
||||||
sleep: (SleepClientTool 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) {
|
function ensureClientToolInstance(toolName: string | undefined, toolCallId: string | undefined) {
|
||||||
|
|||||||
Reference in New Issue
Block a user