diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input.tsx new file mode 100644 index 0000000000..733487ad37 --- /dev/null +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input.tsx @@ -0,0 +1,304 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { PlusIcon, XIcon } from 'lucide-react' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { useSubBlockValue } from '../hooks/use-sub-block-value' +import { getAllBlocks } from '@/blocks' +import { cn } from '@/lib/utils' +import { getTool } from '@/tools' +import { ShortInput } from './short-input' + +interface ToolInputProps { + blockId: string + subBlockId: string +} + +// State interface - only what we need to store +interface StoredTool { + type: string + title: string + params: Record +} + +// UI interface - includes display properties +interface ToolDisplay { + type: string + icon: any + title: string + bgColor: string + requiredParams: ToolParam[] +} + +interface SelectedTool { + type: string + icon: any + title: string + bgColor: string + requiredParams?: ToolParam[] + params?: Record +} + +interface ToolParam { + id: string + type: string + description?: string + requiredForToolCall: boolean +} + +// Assumes the first tool in the access array is the tool to be used +// TODO: Switch to getting tools instead of tool blocks once we switch to providers +const getToolIdFromBlock = (blockType: string): string | undefined => { + const block = getAllBlocks().find((block) => block.type === blockType) + return block?.tools.access[0] +} + +const getRequiredToolParams = (toolId: string): ToolParam[] => { + const tool = getTool(toolId) + if (!tool) return [] + + return Object.entries(tool.params) + .filter(([_, param]) => param.requiredForToolCall) + .map(([paramId, param]) => ({ + id: paramId, + type: param.type, + description: param.description, + requiredForToolCall: param.requiredForToolCall ?? false, + })) +} + +export function ToolInput({ blockId, subBlockId }: ToolInputProps) { + const [value, setValue] = useSubBlockValue(blockId, subBlockId) + const [open, setOpen] = useState(false) + + const toolBlocks = getAllBlocks().filter( + (block) => block.toolbar.category === 'tools' + ) + + const selectedTools: StoredTool[] = + Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' + ? (value as unknown as StoredTool[]) + : [] + + const handleSelectTool = (toolBlock: (typeof toolBlocks)[0]) => { + const toolId = getToolIdFromBlock(toolBlock.type) + + // Only store essential data + const newTool: StoredTool = { + type: toolBlock.type, + title: toolBlock.toolbar.title, + params: {}, + } + + if (!selectedTools.some((tool) => tool.type === newTool.type)) { + setValue([...selectedTools, newTool]) + } + + setOpen(false) + } + + const handleRemoveTool = (toolType: string) => { + setValue(selectedTools.filter((tool) => tool.type !== toolType)) + } + + const handleParamChange = ( + toolType: string, + paramId: string, + paramValue: string + ) => { + setValue( + selectedTools.map((tool) => { + if (tool.type === toolType) { + return { + ...tool, + params: { + ...tool.params, + [paramId]: paramValue, + }, + } + } + return tool + }) + ) + } + + const IconComponent = ({ + icon: Icon, + className, + }: { + icon: any + className?: string + }) => { + if (!Icon) return null + return + } + + // Helper function to get the icon component for a tool type + const getToolIcon = (type: string) => { + const toolBlock = toolBlocks.find((block) => block.type === type) + return toolBlock?.toolbar.icon + } + + return ( +
+ {selectedTools.length === 0 ? ( + + +
+
+ + Add Tool +
+
+
+ + + + + No tools found. + + {toolBlocks.map((block) => ( + handleSelectTool(block)} + className="flex items-center gap-2 cursor-pointer" + > +
+ +
+ {block.toolbar.title} +
+ ))} +
+
+
+
+
+ ) : ( +
+ {selectedTools.map((tool) => { + // Get UI properties from toolBlocks for display + const toolBlock = toolBlocks.find( + (block) => block.type === tool.type + ) + const toolId = getToolIdFromBlock(tool.type) + const requiredParams = toolId ? getRequiredToolParams(toolId) : [] + + return ( +
+
+
+
+
+ +
+ {tool.title} +
+ +
+ + {requiredParams.length > 0 && ( +
+ {requiredParams.map((param) => ( +
+
+ {param.id} +
+
+ + handleParamChange( + tool.type, + param.id, + e.target.value + ) + } + placeholder={param.description} + className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" + /> +
+
+ ))} +
+ )} +
+
+ ) + })} + + + + + + + + + No tools found. + + {toolBlocks.map((block) => ( + handleSelectTool(block)} + className="flex items-center gap-2 cursor-pointer" + > +
+ +
+ {block.toolbar.title} +
+ ))} +
+
+
+
+
+
+ )} +
+ ) +} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index 605174a267..181a7d7261 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -7,6 +7,7 @@ import { SliderInput } from './components/slider-input' import { Table } from './components/table' import { Code } from './components/code' import { Switch } from './components/switch' +import { ToolInput } from './components/tool-input' interface SubBlockProps { blockId: string @@ -92,6 +93,8 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) { title={config.title} /> ) + case 'tool-input': + return default: return null } diff --git a/blocks/blocks/agent.ts b/blocks/blocks/agent.ts index 790e9b628b..71c8b33c75 100644 --- a/blocks/blocks/agent.ts +++ b/blocks/blocks/agent.ts @@ -38,7 +38,8 @@ export const AgentBlock: BlockConfig = { context: { type: 'string', required: false }, apiKey: { type: 'string', required: true }, responseFormat: { type: 'json', required: false }, - temperature: { type: 'number', required: false } + temperature: { type: 'number', required: false }, + tools: { type: 'json', required: false } }, outputs: { response: { @@ -89,12 +90,18 @@ export const AgentBlock: BlockConfig = { password: true, connectionDroppable: false }, + { + id: 'tools', + title: 'Tools', + type: 'tool-input', + layout: 'full' + }, { id: 'responseFormat', title: 'Response Format', type: 'code', layout: 'full' - } + }, ] } } \ No newline at end of file diff --git a/blocks/index.ts b/blocks/index.ts index 4165a835d6..e94076303b 100644 --- a/blocks/index.ts +++ b/blocks/index.ts @@ -49,7 +49,6 @@ const toolToBlockType = Object.entries(blocks).reduce((acc, [blockType, config]) export const getBlock = (type: string): BlockConfig | undefined => blocks[type] - export const getBlocksByCategory = (category: 'blocks' | 'tools'): BlockConfig[] => Object.values(blocks).filter(block => block.toolbar.category === category) diff --git a/blocks/types.ts b/blocks/types.ts index 7147ab70f9..da9a46457c 100644 --- a/blocks/types.ts +++ b/blocks/types.ts @@ -15,7 +15,7 @@ export type BlockOutput = export type ParamType = 'string' | 'number' | 'boolean' | 'json' -export type SubBlockType = 'short-input' | 'long-input' | 'dropdown' | 'slider' | 'table' | 'code' | 'switch' +export type SubBlockType = 'short-input' | 'long-input' | 'dropdown' | 'slider' | 'table' | 'code' | 'switch' | 'tool-input' export type SubBlockLayout = 'full' | 'half' export interface OutputConfig { diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 0000000000..c15ae751e7 --- /dev/null +++ b/components/ui/command.tsx @@ -0,0 +1,169 @@ +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck +'use client' + +import * as React from 'react' +import { type DialogProps } from '@radix-ui/react-dialog' +import { Command as CommandPrimitive } from 'cmdk' +import { Search } from 'lucide-react' + +import { cn } from '@/lib/utils' +import { Dialog, DialogContent } from '@/components/ui/dialog' + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + children?: React.ReactNode + } +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + placeholder?: string + } +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + children?: React.ReactNode + } +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + children?: React.ReactNode + } +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + children?: React.ReactNode + } +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + children?: React.ReactNode + onSelect?: () => void + className?: string + } +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = 'CommandShortcut' + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx new file mode 100644 index 0000000000..a0ec48beee --- /dev/null +++ b/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/package-lock.json b/package-lock.json index 955d87b1bd..6ad6592966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "0.1.0", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.5", - "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slider": "^1.2.2", @@ -2362,15 +2362,15 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.4.tgz", - "integrity": "sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.5.tgz", + "integrity": "sha512-YXkTAftOIW2Bt3qKH8vYr6n9gCkVrvyvfiTObVjoHVTHnNj26rmvO87IKa3VgtgCjb8FAQ6qOjNViwl+9iIzlg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", @@ -2380,8 +2380,35 @@ "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz", + "integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", diff --git a/package.json b/package.json index 3d0116e4f8..a61c18b7a8 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.5", - "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slider": "^1.2.2", diff --git a/tools/crewai/vision.ts b/tools/crewai/vision.ts index ed000e645f..0aa72a0a96 100644 --- a/tools/crewai/vision.ts +++ b/tools/crewai/vision.ts @@ -25,6 +25,7 @@ export const visionTool: ToolConfig = { apiKey: { type: 'string', required: true, + requiredForToolCall: true, description: 'API key for the selected model provider' }, imageUrl: { diff --git a/tools/firecrawl/scrape.ts b/tools/firecrawl/scrape.ts index 7d2dfe0246..eff0549556 100644 --- a/tools/firecrawl/scrape.ts +++ b/tools/firecrawl/scrape.ts @@ -41,6 +41,7 @@ export const scrapeTool: ToolConfig = { apiKey: { type: 'string', required: true, + requiredForToolCall: true, description: 'Firecrawl API key' }, url: { diff --git a/tools/github/repo.ts b/tools/github/repo.ts index 95c751527e..e5148da327 100644 --- a/tools/github/repo.ts +++ b/tools/github/repo.ts @@ -36,6 +36,7 @@ export const repoInfoTool: ToolConfig = { }, apiKey: { type: 'string', + requiredForToolCall: true, description: 'GitHub Personal Access Token' } }, diff --git a/tools/hubspot/contacts.ts b/tools/hubspot/contacts.ts index 89039da710..ed968d656b 100644 --- a/tools/hubspot/contacts.ts +++ b/tools/hubspot/contacts.ts @@ -36,6 +36,7 @@ export const contactsTool: ToolConfig = { apiKey: { type: 'string', required: true, + requiredForToolCall: true, description: 'HubSpot API key' }, email: { diff --git a/tools/salesforce/opportunities.ts b/tools/salesforce/opportunities.ts index 64e5974b1d..33ae45d4a7 100644 --- a/tools/salesforce/opportunities.ts +++ b/tools/salesforce/opportunities.ts @@ -37,6 +37,7 @@ export const opportunitiesTool: ToolConfig { // Basic tool identification id: string - name: string + name: string description: string version: string @@ -17,6 +17,7 @@ export interface ToolConfig

{ params: Record