mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-06 21:54:01 -05:00
improvement(copilot): improve copilot metadata processing and tool output memory (#2516)
This commit is contained in:
committed by
GitHub
parent
58fcb4ed80
commit
e981b1dc1b
@@ -9,6 +9,8 @@ export const ToolIds = z.enum([
|
||||
'get_workflow_console',
|
||||
'get_blocks_and_tools',
|
||||
'get_blocks_metadata',
|
||||
'get_block_options',
|
||||
'get_block_config',
|
||||
'get_trigger_examples',
|
||||
'get_examples_rag',
|
||||
'get_operations_examples',
|
||||
@@ -120,6 +122,20 @@ export const ToolArgSchemas = {
|
||||
blockIds: StringArray.min(1),
|
||||
}),
|
||||
|
||||
get_block_options: z.object({
|
||||
blockId: z.string().describe('The block type ID (e.g., "google_sheets", "slack", "gmail")'),
|
||||
}),
|
||||
|
||||
get_block_config: z.object({
|
||||
blockType: z.string().describe('The block type ID (e.g., "google_sheets", "slack", "gmail")'),
|
||||
operation: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional operation ID (e.g., "read", "write"). If not provided, returns full block schema.'
|
||||
),
|
||||
}),
|
||||
|
||||
get_trigger_blocks: z.object({}),
|
||||
|
||||
get_block_best_practices: z.object({
|
||||
@@ -296,6 +312,8 @@ export const ToolSSESchemas = {
|
||||
get_workflow_console: toolCallSSEFor('get_workflow_console', ToolArgSchemas.get_workflow_console),
|
||||
get_blocks_and_tools: toolCallSSEFor('get_blocks_and_tools', ToolArgSchemas.get_blocks_and_tools),
|
||||
get_blocks_metadata: toolCallSSEFor('get_blocks_metadata', ToolArgSchemas.get_blocks_metadata),
|
||||
get_block_options: toolCallSSEFor('get_block_options', ToolArgSchemas.get_block_options),
|
||||
get_block_config: toolCallSSEFor('get_block_config', ToolArgSchemas.get_block_config),
|
||||
get_trigger_blocks: toolCallSSEFor('get_trigger_blocks', ToolArgSchemas.get_trigger_blocks),
|
||||
|
||||
get_trigger_examples: toolCallSSEFor('get_trigger_examples', ToolArgSchemas.get_trigger_examples),
|
||||
@@ -434,6 +452,24 @@ export const ToolResultSchemas = {
|
||||
get_workflow_console: z.object({ entries: z.array(ExecutionEntry) }),
|
||||
get_blocks_and_tools: z.object({ blocks: z.array(z.any()), tools: z.array(z.any()) }),
|
||||
get_blocks_metadata: z.object({ metadata: z.record(z.any()) }),
|
||||
get_block_options: z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
operations: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
get_block_config: z.object({
|
||||
blockType: z.string(),
|
||||
blockName: z.string(),
|
||||
operation: z.string().optional(),
|
||||
inputs: z.record(z.any()),
|
||||
outputs: z.record(z.any()),
|
||||
}),
|
||||
get_trigger_blocks: z.object({ triggerBlockIds: z.array(z.string()) }),
|
||||
get_block_best_practices: z.object({ bestPractices: z.array(z.any()) }),
|
||||
get_edit_workflow_examples: z.object({
|
||||
|
||||
94
apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts
Normal file
94
apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { FileCode, Loader2, MinusCircle, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import {
|
||||
ExecuteResponseSuccessSchema,
|
||||
GetBlockConfigInput,
|
||||
GetBlockConfigResult,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
interface GetBlockConfigArgs {
|
||||
blockType: string
|
||||
operation?: string
|
||||
}
|
||||
|
||||
export class GetBlockConfigClientTool extends BaseClientTool {
|
||||
static readonly id = 'get_block_config'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, GetBlockConfigClientTool.id, GetBlockConfigClientTool.metadata)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Getting block config', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Getting block config', icon: Loader2 },
|
||||
[ClientToolCallState.executing]: { text: 'Getting block config', icon: Loader2 },
|
||||
[ClientToolCallState.success]: { text: 'Got block config', icon: FileCode },
|
||||
[ClientToolCallState.error]: { text: 'Failed to get block config', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted getting block config', icon: XCircle },
|
||||
[ClientToolCallState.rejected]: {
|
||||
text: 'Skipped getting block config',
|
||||
icon: MinusCircle,
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
if (params?.blockType && typeof params.blockType === 'string') {
|
||||
const blockName = params.blockType.replace(/_/g, ' ')
|
||||
const opSuffix = params.operation ? ` (${params.operation})` : ''
|
||||
|
||||
switch (state) {
|
||||
case ClientToolCallState.success:
|
||||
return `Got ${blockName}${opSuffix} config`
|
||||
case ClientToolCallState.executing:
|
||||
case ClientToolCallState.generating:
|
||||
case ClientToolCallState.pending:
|
||||
return `Getting ${blockName}${opSuffix} config`
|
||||
case ClientToolCallState.error:
|
||||
return `Failed to get ${blockName}${opSuffix} config`
|
||||
case ClientToolCallState.aborted:
|
||||
return `Aborted getting ${blockName}${opSuffix} config`
|
||||
case ClientToolCallState.rejected:
|
||||
return `Skipped getting ${blockName}${opSuffix} config`
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
|
||||
async execute(args?: GetBlockConfigArgs): Promise<void> {
|
||||
const logger = createLogger('GetBlockConfigClientTool')
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
const { blockType, operation } = GetBlockConfigInput.parse(args || {})
|
||||
|
||||
const res = await fetch('/api/copilot/execute-copilot-server-tool', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ toolName: 'get_block_config', payload: { blockType, operation } }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => '')
|
||||
throw new Error(errorText || `Server error (${res.status})`)
|
||||
}
|
||||
const json = await res.json()
|
||||
const parsed = ExecuteResponseSuccessSchema.parse(json)
|
||||
const result = GetBlockConfigResult.parse(parsed.result)
|
||||
|
||||
const inputCount = Object.keys(result.inputs).length
|
||||
const outputCount = Object.keys(result.outputs).length
|
||||
await this.markToolComplete(200, { inputs: inputCount, outputs: outputCount }, result)
|
||||
this.setState(ClientToolCallState.success)
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logger.error('Execute failed', { message })
|
||||
await this.markToolComplete(500, message)
|
||||
this.setState(ClientToolCallState.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { ListFilter, Loader2, MinusCircle, XCircle } from 'lucide-react'
|
||||
import {
|
||||
BaseClientTool,
|
||||
type BaseClientToolMetadata,
|
||||
ClientToolCallState,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import {
|
||||
ExecuteResponseSuccessSchema,
|
||||
GetBlockOptionsInput,
|
||||
GetBlockOptionsResult,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
interface GetBlockOptionsArgs {
|
||||
blockId: string
|
||||
}
|
||||
|
||||
export class GetBlockOptionsClientTool extends BaseClientTool {
|
||||
static readonly id = 'get_block_options'
|
||||
|
||||
constructor(toolCallId: string) {
|
||||
super(toolCallId, GetBlockOptionsClientTool.id, GetBlockOptionsClientTool.metadata)
|
||||
}
|
||||
|
||||
static readonly metadata: BaseClientToolMetadata = {
|
||||
displayNames: {
|
||||
[ClientToolCallState.generating]: { text: 'Getting block options', icon: Loader2 },
|
||||
[ClientToolCallState.pending]: { text: 'Getting block options', icon: Loader2 },
|
||||
[ClientToolCallState.executing]: { text: 'Getting block options', icon: Loader2 },
|
||||
[ClientToolCallState.success]: { text: 'Got block options', icon: ListFilter },
|
||||
[ClientToolCallState.error]: { text: 'Failed to get block options', icon: XCircle },
|
||||
[ClientToolCallState.aborted]: { text: 'Aborted getting block options', icon: XCircle },
|
||||
[ClientToolCallState.rejected]: {
|
||||
text: 'Skipped getting block options',
|
||||
icon: MinusCircle,
|
||||
},
|
||||
},
|
||||
getDynamicText: (params, state) => {
|
||||
if (params?.blockId && typeof params.blockId === 'string') {
|
||||
const blockName = params.blockId.replace(/_/g, ' ')
|
||||
|
||||
switch (state) {
|
||||
case ClientToolCallState.success:
|
||||
return `Got ${blockName} options`
|
||||
case ClientToolCallState.executing:
|
||||
case ClientToolCallState.generating:
|
||||
case ClientToolCallState.pending:
|
||||
return `Getting ${blockName} options`
|
||||
case ClientToolCallState.error:
|
||||
return `Failed to get ${blockName} options`
|
||||
case ClientToolCallState.aborted:
|
||||
return `Aborted getting ${blockName} options`
|
||||
case ClientToolCallState.rejected:
|
||||
return `Skipped getting ${blockName} options`
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
|
||||
async execute(args?: GetBlockOptionsArgs): Promise<void> {
|
||||
const logger = createLogger('GetBlockOptionsClientTool')
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
const { blockId } = GetBlockOptionsInput.parse(args || {})
|
||||
|
||||
const res = await fetch('/api/copilot/execute-copilot-server-tool', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ toolName: 'get_block_options', payload: { blockId } }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => '')
|
||||
throw new Error(errorText || `Server error (${res.status})`)
|
||||
}
|
||||
const json = await res.json()
|
||||
const parsed = ExecuteResponseSuccessSchema.parse(json)
|
||||
const result = GetBlockOptionsResult.parse(parsed.result)
|
||||
|
||||
await this.markToolComplete(200, { operations: result.operations.length }, result)
|
||||
this.setState(ClientToolCallState.success)
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logger.error('Execute failed', { message })
|
||||
await this.markToolComplete(500, message)
|
||||
this.setState(ClientToolCallState.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
342
apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts
Normal file
342
apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import {
|
||||
type GetBlockConfigInputType,
|
||||
GetBlockConfigResult,
|
||||
type GetBlockConfigResultType,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { registry as blockRegistry } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||
import { tools as toolsRegistry } from '@/tools/registry'
|
||||
|
||||
interface InputFieldSchema {
|
||||
type: string
|
||||
description?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
options?: string[]
|
||||
default?: any
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available models from PROVIDER_DEFINITIONS as static options.
|
||||
* This provides fallback data when store state is not available server-side.
|
||||
*/
|
||||
function getStaticModelOptions(): string[] {
|
||||
const models: 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(model.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to call a dynamic options function with fallback data injected.
|
||||
*/
|
||||
function callOptionsWithFallback(optionsFn: () => any[]): any[] | undefined {
|
||||
const staticModels = getStaticModelOptions()
|
||||
|
||||
const mockProvidersState = {
|
||||
providers: {
|
||||
base: { models: staticModels },
|
||||
ollama: { models: [] },
|
||||
vllm: { models: [] },
|
||||
openrouter: { models: [] },
|
||||
},
|
||||
}
|
||||
|
||||
let originalGetState: (() => any) | undefined
|
||||
let store: any
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
store = require('@/stores/providers/store')
|
||||
if (store?.useProvidersStore?.getState) {
|
||||
originalGetState = store.useProvidersStore.getState
|
||||
store.useProvidersStore.getState = () => mockProvidersState
|
||||
}
|
||||
} catch {
|
||||
// Store module not available
|
||||
}
|
||||
|
||||
try {
|
||||
return optionsFn()
|
||||
} finally {
|
||||
if (store?.useProvidersStore && originalGetState) {
|
||||
store.useProvidersStore.getState = originalGetState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves options from a subBlock, handling both static arrays and dynamic functions
|
||||
*/
|
||||
function resolveSubBlockOptions(sb: SubBlockConfig): string[] | undefined {
|
||||
// Skip if subblock uses fetchOptions (async network calls)
|
||||
if (sb.fetchOptions) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let rawOptions: any[] | undefined
|
||||
|
||||
try {
|
||||
if (typeof sb.options === 'function') {
|
||||
rawOptions = callOptionsWithFallback(sb.options)
|
||||
} else {
|
||||
rawOptions = sb.options
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!Array.isArray(rawOptions) || rawOptions.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return rawOptions
|
||||
.map((opt: any) => {
|
||||
if (!opt) return undefined
|
||||
if (typeof opt === 'object') {
|
||||
return opt.label || opt.id
|
||||
}
|
||||
return String(opt)
|
||||
})
|
||||
.filter((o): o is string => o !== undefined)
|
||||
}
|
||||
|
||||
interface OutputFieldSchema {
|
||||
type: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the condition to check if it matches the given operation
|
||||
*/
|
||||
function matchesOperation(condition: any, operation: string): boolean {
|
||||
if (!condition) return false
|
||||
|
||||
const cond = typeof condition === 'function' ? condition() : condition
|
||||
if (!cond) return false
|
||||
|
||||
if (cond.field === 'operation' && !cond.not) {
|
||||
const values = Array.isArray(cond.value) ? cond.value : [cond.value]
|
||||
return values.includes(operation)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts input schema from subBlocks
|
||||
*/
|
||||
function extractInputsFromSubBlocks(
|
||||
subBlocks: SubBlockConfig[],
|
||||
operation?: string
|
||||
): Record<string, InputFieldSchema> {
|
||||
const inputs: Record<string, InputFieldSchema> = {}
|
||||
|
||||
for (const sb of subBlocks) {
|
||||
// Skip trigger-mode subBlocks
|
||||
if (sb.mode === 'trigger') continue
|
||||
|
||||
// Skip hidden subBlocks
|
||||
if (sb.hidden) continue
|
||||
|
||||
// If operation is specified, only include subBlocks that:
|
||||
// 1. Have no condition (common parameters)
|
||||
// 2. Have a condition matching the operation
|
||||
if (operation) {
|
||||
const condition = typeof sb.condition === 'function' ? sb.condition() : sb.condition
|
||||
if (condition) {
|
||||
if (condition.field === 'operation' && !condition.not) {
|
||||
// This is an operation-specific field
|
||||
const values = Array.isArray(condition.value) ? condition.value : [condition.value]
|
||||
if (!values.includes(operation)) {
|
||||
continue // Skip if doesn't match our operation
|
||||
}
|
||||
} else if (!matchesOperation(condition, operation)) {
|
||||
// Other condition that doesn't match
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const field: InputFieldSchema = {
|
||||
type: mapSubBlockTypeToSchemaType(sb.type),
|
||||
}
|
||||
|
||||
if (sb.description) field.description = sb.description
|
||||
if (sb.title && !sb.description) field.description = sb.title
|
||||
if (sb.placeholder) field.placeholder = sb.placeholder
|
||||
|
||||
// Handle required
|
||||
if (typeof sb.required === 'boolean') {
|
||||
field.required = sb.required
|
||||
} else if (typeof sb.required === 'object') {
|
||||
field.required = true // Has conditional requirement
|
||||
}
|
||||
|
||||
// Handle options using the resolver that handles dynamic model lists
|
||||
const resolvedOptions = resolveSubBlockOptions(sb)
|
||||
if (resolvedOptions && resolvedOptions.length > 0) {
|
||||
field.options = resolvedOptions
|
||||
}
|
||||
|
||||
// Handle default value
|
||||
if (sb.defaultValue !== undefined) {
|
||||
field.default = sb.defaultValue
|
||||
}
|
||||
|
||||
// Handle numeric constraints
|
||||
if (sb.min !== undefined) field.min = sb.min
|
||||
if (sb.max !== undefined) field.max = sb.max
|
||||
|
||||
inputs[sb.id] = field
|
||||
}
|
||||
|
||||
return inputs
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps subBlock type to a simplified schema type
|
||||
*/
|
||||
function mapSubBlockTypeToSchemaType(type: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
'short-input': 'string',
|
||||
'long-input': 'string',
|
||||
code: 'string',
|
||||
dropdown: 'string',
|
||||
combobox: 'string',
|
||||
slider: 'number',
|
||||
switch: 'boolean',
|
||||
'tool-input': 'json',
|
||||
'checkbox-list': 'array',
|
||||
'grouped-checkbox-list': 'array',
|
||||
'condition-input': 'json',
|
||||
'eval-input': 'json',
|
||||
'time-input': 'string',
|
||||
'oauth-input': 'credential',
|
||||
'file-selector': 'string',
|
||||
'project-selector': 'string',
|
||||
'channel-selector': 'string',
|
||||
'user-selector': 'string',
|
||||
'folder-selector': 'string',
|
||||
'knowledge-base-selector': 'string',
|
||||
'document-selector': 'string',
|
||||
'mcp-server-selector': 'string',
|
||||
'mcp-tool-selector': 'string',
|
||||
table: 'json',
|
||||
'file-upload': 'file',
|
||||
'messages-input': 'array',
|
||||
}
|
||||
|
||||
return typeMap[type] || 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts output schema from block config or tool
|
||||
*/
|
||||
function extractOutputs(blockConfig: any, operation?: string): Record<string, OutputFieldSchema> {
|
||||
const outputs: Record<string, OutputFieldSchema> = {}
|
||||
|
||||
// If operation is specified, try to get outputs from the specific tool
|
||||
if (operation) {
|
||||
try {
|
||||
const toolSelector = blockConfig.tools?.config?.tool
|
||||
if (typeof toolSelector === 'function') {
|
||||
const toolId = toolSelector({ operation })
|
||||
const tool = toolsRegistry[toolId]
|
||||
if (tool?.outputs) {
|
||||
for (const [key, def] of Object.entries(tool.outputs)) {
|
||||
const typedDef = def as { type: string; description?: string }
|
||||
outputs[key] = {
|
||||
type: typedDef.type || 'any',
|
||||
description: typedDef.description,
|
||||
}
|
||||
}
|
||||
return outputs
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to block-level outputs
|
||||
}
|
||||
}
|
||||
|
||||
// Use block-level outputs
|
||||
if (blockConfig.outputs) {
|
||||
for (const [key, def] of Object.entries(blockConfig.outputs)) {
|
||||
if (typeof def === 'string') {
|
||||
outputs[key] = { type: def }
|
||||
} else if (typeof def === 'object' && def !== null) {
|
||||
const typedDef = def as { type?: string; description?: string }
|
||||
outputs[key] = {
|
||||
type: typedDef.type || 'any',
|
||||
description: typedDef.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outputs
|
||||
}
|
||||
|
||||
export const getBlockConfigServerTool: BaseServerTool<
|
||||
GetBlockConfigInputType,
|
||||
GetBlockConfigResultType
|
||||
> = {
|
||||
name: 'get_block_config',
|
||||
async execute({
|
||||
blockType,
|
||||
operation,
|
||||
}: GetBlockConfigInputType): Promise<GetBlockConfigResultType> {
|
||||
const logger = createLogger('GetBlockConfigServerTool')
|
||||
logger.debug('Executing get_block_config', { blockType, operation })
|
||||
|
||||
const blockConfig = blockRegistry[blockType]
|
||||
if (!blockConfig) {
|
||||
throw new Error(`Block not found: ${blockType}`)
|
||||
}
|
||||
|
||||
// If operation is specified, validate it exists
|
||||
if (operation) {
|
||||
const operationSubBlock = blockConfig.subBlocks?.find((sb) => sb.id === 'operation')
|
||||
if (operationSubBlock && Array.isArray(operationSubBlock.options)) {
|
||||
const validOperations = operationSubBlock.options.map((o) =>
|
||||
typeof o === 'object' ? o.id : o
|
||||
)
|
||||
if (!validOperations.includes(operation)) {
|
||||
throw new Error(
|
||||
`Invalid operation "${operation}" for block "${blockType}". Valid operations: ${validOperations.join(', ')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subBlocks = Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks : []
|
||||
const inputs = extractInputsFromSubBlocks(subBlocks, operation)
|
||||
const outputs = extractOutputs(blockConfig, operation)
|
||||
|
||||
const result = {
|
||||
blockType,
|
||||
blockName: blockConfig.name,
|
||||
operation,
|
||||
inputs,
|
||||
outputs,
|
||||
}
|
||||
|
||||
return GetBlockConfigResult.parse(result)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import {
|
||||
type GetBlockOptionsInputType,
|
||||
GetBlockOptionsResult,
|
||||
type GetBlockOptionsResultType,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { registry as blockRegistry } from '@/blocks/registry'
|
||||
import { tools as toolsRegistry } from '@/tools/registry'
|
||||
|
||||
export const getBlockOptionsServerTool: BaseServerTool<
|
||||
GetBlockOptionsInputType,
|
||||
GetBlockOptionsResultType
|
||||
> = {
|
||||
name: 'get_block_options',
|
||||
async execute({ blockId }: GetBlockOptionsInputType): Promise<GetBlockOptionsResultType> {
|
||||
const logger = createLogger('GetBlockOptionsServerTool')
|
||||
logger.debug('Executing get_block_options', { blockId })
|
||||
|
||||
const blockConfig = blockRegistry[blockId]
|
||||
if (!blockConfig) {
|
||||
throw new Error(`Block not found: ${blockId}`)
|
||||
}
|
||||
|
||||
const operations: { id: string; name: string; description?: string }[] = []
|
||||
|
||||
// Check if block has an operation dropdown to determine available operations
|
||||
const operationSubBlock = blockConfig.subBlocks?.find((sb) => sb.id === 'operation')
|
||||
if (operationSubBlock && Array.isArray(operationSubBlock.options)) {
|
||||
// Block has operations - get tool info for each operation
|
||||
for (const option of operationSubBlock.options) {
|
||||
const opId = typeof option === 'object' ? option.id : option
|
||||
const opLabel = typeof option === 'object' ? option.label : option
|
||||
|
||||
// Try to resolve the tool for this operation
|
||||
let toolDescription: string | undefined
|
||||
try {
|
||||
const toolSelector = blockConfig.tools?.config?.tool
|
||||
if (typeof toolSelector === 'function') {
|
||||
const toolId = toolSelector({ operation: opId })
|
||||
const tool = toolsRegistry[toolId]
|
||||
if (tool) {
|
||||
toolDescription = tool.description
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Tool resolution failed, continue without description
|
||||
}
|
||||
|
||||
operations.push({
|
||||
id: opId,
|
||||
name: opLabel || opId,
|
||||
description: toolDescription,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// No operation dropdown - list all accessible tools
|
||||
const accessibleTools = blockConfig.tools?.access || []
|
||||
for (const toolId of accessibleTools) {
|
||||
const tool = toolsRegistry[toolId]
|
||||
if (tool) {
|
||||
operations.push({
|
||||
id: toolId,
|
||||
name: tool.name || toolId,
|
||||
description: tool.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
blockId,
|
||||
blockName: blockConfig.name,
|
||||
operations,
|
||||
}
|
||||
|
||||
return GetBlockOptionsResult.parse(result)
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { getBlockConfigServerTool } from '@/lib/copilot/tools/server/blocks/get-block-config'
|
||||
import { getBlockOptionsServerTool } from '@/lib/copilot/tools/server/blocks/get-block-options'
|
||||
import { getBlocksAndToolsServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-and-tools'
|
||||
import { getBlocksMetadataServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-metadata-tool'
|
||||
import { getTriggerBlocksServerTool } from '@/lib/copilot/tools/server/blocks/get-trigger-blocks'
|
||||
@@ -15,6 +17,10 @@ import { editWorkflowServerTool } from '@/lib/copilot/tools/server/workflow/edit
|
||||
import { getWorkflowConsoleServerTool } from '@/lib/copilot/tools/server/workflow/get-workflow-console'
|
||||
import {
|
||||
ExecuteResponseSuccessSchema,
|
||||
GetBlockConfigInput,
|
||||
GetBlockConfigResult,
|
||||
GetBlockOptionsInput,
|
||||
GetBlockOptionsResult,
|
||||
GetBlocksAndToolsInput,
|
||||
GetBlocksAndToolsResult,
|
||||
GetBlocksMetadataInput,
|
||||
@@ -35,6 +41,8 @@ const logger = createLogger('ServerToolRouter')
|
||||
// Register tools
|
||||
serverToolRegistry[getBlocksAndToolsServerTool.name] = getBlocksAndToolsServerTool
|
||||
serverToolRegistry[getBlocksMetadataServerTool.name] = getBlocksMetadataServerTool
|
||||
serverToolRegistry[getBlockOptionsServerTool.name] = getBlockOptionsServerTool
|
||||
serverToolRegistry[getBlockConfigServerTool.name] = getBlockConfigServerTool
|
||||
serverToolRegistry[getTriggerBlocksServerTool.name] = getTriggerBlocksServerTool
|
||||
serverToolRegistry[editWorkflowServerTool.name] = editWorkflowServerTool
|
||||
serverToolRegistry[getWorkflowConsoleServerTool.name] = getWorkflowConsoleServerTool
|
||||
@@ -72,6 +80,12 @@ export async function routeExecution(
|
||||
if (toolName === 'get_blocks_metadata') {
|
||||
args = GetBlocksMetadataInput.parse(args)
|
||||
}
|
||||
if (toolName === 'get_block_options') {
|
||||
args = GetBlockOptionsInput.parse(args)
|
||||
}
|
||||
if (toolName === 'get_block_config') {
|
||||
args = GetBlockConfigInput.parse(args)
|
||||
}
|
||||
if (toolName === 'get_trigger_blocks') {
|
||||
args = GetTriggerBlocksInput.parse(args)
|
||||
}
|
||||
@@ -87,6 +101,12 @@ export async function routeExecution(
|
||||
if (toolName === 'get_blocks_metadata') {
|
||||
return GetBlocksMetadataResult.parse(result)
|
||||
}
|
||||
if (toolName === 'get_block_options') {
|
||||
return GetBlockOptionsResult.parse(result)
|
||||
}
|
||||
if (toolName === 'get_block_config') {
|
||||
return GetBlockConfigResult.parse(result)
|
||||
}
|
||||
if (toolName === 'get_trigger_blocks') {
|
||||
return GetTriggerBlocksResult.parse(result)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,39 @@ export const GetTriggerBlocksResult = z.object({
|
||||
})
|
||||
export type GetTriggerBlocksResultType = z.infer<typeof GetTriggerBlocksResult>
|
||||
|
||||
// get_block_options
|
||||
export const GetBlockOptionsInput = z.object({
|
||||
blockId: z.string(),
|
||||
})
|
||||
export const GetBlockOptionsResult = z.object({
|
||||
blockId: z.string(),
|
||||
blockName: z.string(),
|
||||
operations: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
export type GetBlockOptionsInputType = z.infer<typeof GetBlockOptionsInput>
|
||||
export type GetBlockOptionsResultType = z.infer<typeof GetBlockOptionsResult>
|
||||
|
||||
// get_block_config
|
||||
export const GetBlockConfigInput = z.object({
|
||||
blockType: z.string(),
|
||||
operation: z.string().optional(),
|
||||
})
|
||||
export const GetBlockConfigResult = z.object({
|
||||
blockType: z.string(),
|
||||
blockName: z.string(),
|
||||
operation: z.string().optional(),
|
||||
inputs: z.record(z.any()),
|
||||
outputs: z.record(z.any()),
|
||||
})
|
||||
export type GetBlockConfigInputType = z.infer<typeof GetBlockConfigInput>
|
||||
export type GetBlockConfigResultType = z.infer<typeof GetBlockConfigResult>
|
||||
|
||||
// knowledge_base - shared schema used by client tool, server tool, and registry
|
||||
export const KnowledgeBaseArgsSchema = z.object({
|
||||
operation: z.enum(['create', 'list', 'get', 'query']),
|
||||
|
||||
@@ -8,6 +8,8 @@ import type {
|
||||
ClientToolDisplay,
|
||||
} from '@/lib/copilot/tools/client/base-tool'
|
||||
import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
|
||||
import { GetBlockConfigClientTool } from '@/lib/copilot/tools/client/blocks/get-block-config'
|
||||
import { GetBlockOptionsClientTool } from '@/lib/copilot/tools/client/blocks/get-block-options'
|
||||
import { GetBlocksAndToolsClientTool } from '@/lib/copilot/tools/client/blocks/get-blocks-and-tools'
|
||||
import { GetBlocksMetadataClientTool } from '@/lib/copilot/tools/client/blocks/get-blocks-metadata'
|
||||
import { GetTriggerBlocksClientTool } from '@/lib/copilot/tools/client/blocks/get-trigger-blocks'
|
||||
@@ -76,6 +78,8 @@ const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
|
||||
get_workflow_console: (id) => new GetWorkflowConsoleClientTool(id),
|
||||
get_blocks_and_tools: (id) => new GetBlocksAndToolsClientTool(id),
|
||||
get_blocks_metadata: (id) => new GetBlocksMetadataClientTool(id),
|
||||
get_block_options: (id) => new GetBlockOptionsClientTool(id),
|
||||
get_block_config: (id) => new GetBlockConfigClientTool(id),
|
||||
get_trigger_blocks: (id) => new GetTriggerBlocksClientTool(id),
|
||||
search_online: (id) => new SearchOnlineClientTool(id),
|
||||
search_documentation: (id) => new SearchDocumentationClientTool(id),
|
||||
@@ -114,6 +118,8 @@ export const CLASS_TOOL_METADATA: Record<string, BaseClientToolMetadata | undefi
|
||||
get_workflow_console: (GetWorkflowConsoleClientTool as any)?.metadata,
|
||||
get_blocks_and_tools: (GetBlocksAndToolsClientTool as any)?.metadata,
|
||||
get_blocks_metadata: (GetBlocksMetadataClientTool as any)?.metadata,
|
||||
get_block_options: (GetBlockOptionsClientTool as any)?.metadata,
|
||||
get_block_config: (GetBlockConfigClientTool as any)?.metadata,
|
||||
get_trigger_blocks: (GetTriggerBlocksClientTool as any)?.metadata,
|
||||
search_online: (SearchOnlineClientTool as any)?.metadata,
|
||||
search_documentation: (SearchDocumentationClientTool as any)?.metadata,
|
||||
|
||||
Reference in New Issue
Block a user