feat(optional-tool-input): added ability to add optional params (#285)

* feat(optional-tool-input): added ability to add optional params to a tool call

* fix(tests): added testing for optional param
This commit is contained in:
Emir Karabeg
2025-04-20 12:08:51 -07:00
committed by GitHub
parent 613ce55e1e
commit 2de8f37aa2
5 changed files with 83 additions and 29 deletions
@@ -51,6 +51,7 @@ interface ToolParam {
type: string
description?: string
requiredForToolCall: boolean
optionalToolInput?: boolean
}
// Assumes the first tool in the access array is the tool to be used
@@ -60,17 +61,35 @@ const getToolIdFromBlock = (blockType: string): string | undefined => {
return block?.tools.access[0]
}
const getRequiredToolParams = (toolId: string): ToolParam[] => {
// Get parameters that need to be displayed in the tool input UI
const getToolDisplayParams = (toolId: string): ToolParam[] => {
const tool = getTool(toolId)
if (!tool) return []
return Object.entries(tool.params)
.filter(([_, param]) => param.requiredForToolCall)
.filter(([_, param]) => param.requiredForToolCall || param.optionalToolInput)
.map(([paramId, param]) => ({
id: paramId,
type: param.type,
description: param.description,
requiredForToolCall: param.requiredForToolCall ?? false,
optionalToolInput: param.optionalToolInput ?? false,
}))
}
// Keep this for backward compatibility - only get strictly required parameters
const getRequiredToolParams = (toolId: string): ToolParam[] => {
const tool = getTool(toolId)
if (!tool) return []
return Object.entries(tool.params)
.filter(([_, param]) => param.requiredForToolCall || param.optionalToolInput)
.map(([paramId, param]) => ({
id: paramId,
type: param.type,
description: param.description,
requiredForToolCall: param.requiredForToolCall ?? false,
optionalToolInput: param.optionalToolInput ?? false,
}))
}
@@ -86,12 +105,14 @@ const getCustomToolParams = (schema: any): ToolParam[] => {
const properties = schema.function.parameters.properties
const required = schema.function.parameters.required || []
const optionalInputs = schema.function.parameters.optionalToolInputs || []
return Object.entries(properties).map(([paramId, param]: [string, any]) => ({
id: paramId,
type: param.type || 'string',
description: param.description || '',
requiredForToolCall: required.includes(paramId),
optionalToolInput: optionalInputs.includes(paramId),
}))
}
@@ -188,29 +209,33 @@ const formatParamId = (paramId: string): string => {
// Special case for common parameter names
if (paramId === 'apiKey') return 'API Key'
if (paramId === 'apiVersion') return 'API Version'
// Handle underscore and hyphen separated words
if (paramId.includes('_') || paramId.includes('-')) {
return paramId
.split(/[-_]/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
// Handle single character parameters
if (paramId.length === 1) return paramId.toUpperCase()
// Handle camelCase
if (/[A-Z]/.test(paramId)) {
const result = paramId.replace(/([A-Z])/g, ' $1')
return result.charAt(0).toUpperCase() + result.slice(1)
.replace(/ Api/g, ' API')
.replace(/ Id/g, ' ID')
.replace(/ Url/g, ' URL')
.replace(/ Uri/g, ' URI')
.replace(/ Ui/g, ' UI')
return (
result.charAt(0).toUpperCase() +
result
.slice(1)
.replace(/ Api/g, ' API')
.replace(/ Id/g, ' ID')
.replace(/ Url/g, ' URL')
.replace(/ Uri/g, ' URI')
.replace(/ Ui/g, ' UI')
)
}
// Simple case - just capitalize first letter
return paramId.charAt(0).toUpperCase() + paramId.slice(1)
}
@@ -265,12 +290,12 @@ export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
const defaultOperation = operationOptions.length > 0 ? operationOptions[0].id : undefined
const toolId = getToolIdFromBlock(toolBlock.type) || toolBlock.type
const requiredParams = toolId ? getRequiredToolParams(toolId) : []
const displayParams = toolId ? getToolDisplayParams(toolId) : []
// Use the helper function to initialize parameters with blockId as instanceId
const initialParams = initializeToolParams(
toolId,
requiredParams,
displayParams,
subBlockStore,
isAutoFillEnvVarsEnabled,
blockId
@@ -399,19 +424,22 @@ export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
// New handler for when a custom tool is completely deleted from the store
const handleDeleteTool = (toolId: string) => {
// Find any instances of this tool in the current workflow and remove them
const updatedTools = selectedTools.filter(tool => {
const updatedTools = selectedTools.filter((tool) => {
// For custom tools, we need to check if it matches the deleted tool
if (tool.type === 'custom-tool' &&
tool.schema?.function?.name &&
customTools.some(customTool =>
customTool.id === toolId &&
if (
tool.type === 'custom-tool' &&
tool.schema?.function?.name &&
customTools.some(
(customTool) =>
customTool.id === toolId &&
customTool.schema.function.name === tool.schema.function.name
)) {
)
) {
return false
}
return true
})
// Update the workflow value if any tools were removed
if (updatedTools.length !== selectedTools.length) {
setValue(updatedTools)
@@ -832,8 +860,13 @@ export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
{/* Existing parameters */}
{requiredParams.map((param) => (
<div key={param.id} className="space-y-1.5 relative">
<div className="text-xs font-medium text-muted-foreground">
<div className="text-xs font-medium text-muted-foreground flex items-center">
{formatParamId(param.id)}
{param.optionalToolInput && !param.requiredForToolCall && (
<span className="ml-1 text-xs text-muted-foreground/60">
(Optional)
</span>
)}
</div>
<div className="relative">
<ShortInput
@@ -995,8 +1028,10 @@ export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
initialValues={
editingToolIndex !== null && selectedTools[editingToolIndex]?.type === 'custom-tool'
? {
id: customTools.find(tool =>
tool.schema.function.name === selectedTools[editingToolIndex].schema.function.name
id: customTools.find(
(tool) =>
tool.schema.function.name ===
selectedTools[editingToolIndex].schema.function.name
)?.id,
schema: selectedTools[editingToolIndex].schema,
code: selectedTools[editingToolIndex].code || '',
+1
View File
@@ -48,6 +48,7 @@ export const scrapeTool: ToolConfig<ScrapeParams, ScrapeResponse> = {
url: {
type: 'string',
required: true,
optionalToolInput: true,
description: 'The URL to scrape content from',
},
scrapeOptions: {
+1
View File
@@ -31,6 +31,7 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
type: 'string',
required: true,
description: 'Target Slack channel (e.g., #general)',
optionalToolInput: true,
},
text: {
type: 'string',
+1
View File
@@ -33,6 +33,7 @@ export interface ToolConfig<P = any, R = any> {
type: string
required?: boolean
requiredForToolCall?: boolean
optionalToolInput?: boolean
default?: any
description?: string
}
+20 -4
View File
@@ -156,6 +156,7 @@ export function validateToolRequest(
}
// Ensure all required parameters for tool call are provided
// Note: optionalToolInput parameters are not checked here as they're optional
for (const [paramName, paramConfig] of Object.entries(tool.params)) {
if (paramConfig.requiredForToolCall && !(paramName in params)) {
throw new Error(`Parameter "${paramName}" is required for ${toolId} but was not provided`)
@@ -180,13 +181,28 @@ export function createParamSchema(customTool: any): Record<string, any> {
const params: Record<string, any> = {}
if (customTool.schema.function?.parameters?.properties) {
Object.entries(customTool.schema.function.parameters.properties).forEach(([key, config]: [string, any]) => {
params[key] = {
const properties = customTool.schema.function.parameters.properties;
const required = customTool.schema.function.parameters.required || [];
const optionalToolInputs = customTool.schema.function.parameters.optionalToolInputs || [];
Object.entries(properties).forEach(([key, config]: [string, any]) => {
const isRequired = required.includes(key);
const isOptionalInput = optionalToolInputs.includes(key);
// Create the base parameter configuration
const paramConfig: Record<string, any> = {
type: config.type || 'string',
required: customTool.schema.function.parameters.required?.includes(key) || false,
requiredForToolCall: customTool.schema.function.parameters.required?.includes(key) || false,
required: isRequired,
requiredForToolCall: isRequired,
description: config.description || '',
};
// Only add optionalToolInput if it's true to maintain backward compatibility with tests
if (isOptionalInput) {
paramConfig.optionalToolInput = true;
}
params[key] = paramConfig;
})
}