mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
feat(agent): added workflow, kb, and function as a tool for agent block, fix keyboard nav in tool input (#2107)
* feat(agent): added workflow, kb, and function as a tool for agent block * fix keyboard nav and keyboard selection in tool-inp * ack PR comments * remove custom tool changes * fixed kb tools for agent * cleanup
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import type React from 'react'
|
||||
import {
|
||||
import React, {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
@@ -19,6 +18,7 @@ type CommandContextType = {
|
||||
registerItem: (id: string) => void
|
||||
unregisterItem: (id: string) => void
|
||||
selectItem: (id: string) => void
|
||||
handleKeyDown: (e: React.KeyboardEvent) => void
|
||||
}
|
||||
|
||||
const CommandContext = createContext<CommandContextType | undefined>(undefined)
|
||||
@@ -31,6 +31,11 @@ const useCommandContext = () => {
|
||||
return context
|
||||
}
|
||||
|
||||
export const useCommandKeyDown = () => {
|
||||
const context = useContext(CommandContext)
|
||||
return context?.handleKeyDown
|
||||
}
|
||||
|
||||
interface CommandProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
@@ -71,7 +76,6 @@ export function Command({
|
||||
const [items, setItems] = useState<string[]>([])
|
||||
const [filteredItems, setFilteredItems] = useState<string[]>([])
|
||||
|
||||
// Use external searchQuery if provided, otherwise use internal state
|
||||
const searchQuery = externalSearchQuery ?? internalSearchQuery
|
||||
|
||||
const registerItem = useCallback((id: string) => {
|
||||
@@ -101,19 +105,27 @@ export function Command({
|
||||
return
|
||||
}
|
||||
|
||||
const filtered = items
|
||||
.map((item) => {
|
||||
const score = filter ? filter(item, searchQuery) : defaultFilter(item, searchQuery)
|
||||
return { item, score }
|
||||
})
|
||||
.filter((item) => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map((item) => item.item)
|
||||
const filtered = items.filter((item) => {
|
||||
const score = filter ? filter(item, searchQuery) : defaultFilter(item, searchQuery)
|
||||
return score > 0
|
||||
})
|
||||
|
||||
setFilteredItems(filtered)
|
||||
setActiveIndex(filtered.length > 0 ? 0 : -1)
|
||||
}, [searchQuery, items, filter])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex >= 0 && filteredItems[activeIndex]) {
|
||||
const activeElement = document.getElementById(filteredItems[activeIndex])
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [activeIndex, filteredItems])
|
||||
|
||||
const defaultFilter = useCallback((value: string, search: string): number => {
|
||||
const normalizedValue = value.toLowerCase()
|
||||
const normalizedSearch = search.toLowerCase()
|
||||
@@ -158,15 +170,22 @@ export function Command({
|
||||
registerItem,
|
||||
unregisterItem,
|
||||
selectItem,
|
||||
handleKeyDown,
|
||||
}),
|
||||
[searchQuery, activeIndex, filteredItems, registerItem, unregisterItem, selectItem]
|
||||
[
|
||||
searchQuery,
|
||||
activeIndex,
|
||||
filteredItems,
|
||||
registerItem,
|
||||
unregisterItem,
|
||||
selectItem,
|
||||
handleKeyDown,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<CommandContext.Provider value={contextValue}>
|
||||
<div className={cn('flex w-full flex-col', className)} onKeyDown={handleKeyDown}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={cn('flex w-full flex-col', className)}>{children}</div>
|
||||
</CommandContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Check, ChevronDown, ExternalLink, Plus, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@/components/emcn/components/button/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { Button, Combobox } from '@/components/emcn/components'
|
||||
import {
|
||||
getCanonicalScopesForProvider,
|
||||
getProviderIdFromServiceId,
|
||||
getServiceIdFromScopes,
|
||||
OAUTH_PROVIDERS,
|
||||
type OAuthProvider,
|
||||
type OAuthService,
|
||||
@@ -23,16 +15,14 @@ import { useOAuthCredentialDetail, useOAuthCredentials } from '@/hooks/queries/o
|
||||
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('ToolCredentialSelector')
|
||||
|
||||
const getProviderIcon = (providerName: OAuthProvider) => {
|
||||
const { baseProvider } = parseProvider(providerName)
|
||||
const baseProviderConfig = OAUTH_PROVIDERS[baseProvider]
|
||||
|
||||
if (!baseProviderConfig) {
|
||||
return <ExternalLink className='h-4 w-4' />
|
||||
return <ExternalLink className='h-3 w-3' />
|
||||
}
|
||||
return baseProviderConfig.icon({ className: 'h-4 w-4' })
|
||||
return baseProviderConfig.icon({ className: 'h-3 w-3' })
|
||||
}
|
||||
|
||||
const getProviderName = (providerName: OAuthProvider) => {
|
||||
@@ -68,192 +58,237 @@ export function ToolCredentialSelector({
|
||||
serviceId,
|
||||
disabled = false,
|
||||
}: ToolCredentialSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState('')
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedId(value)
|
||||
}, [value])
|
||||
const selectedId = value || ''
|
||||
|
||||
const effectiveServiceId = useMemo(
|
||||
() => serviceId || getServiceIdFromScopes(provider, requiredScopes),
|
||||
[provider, requiredScopes, serviceId]
|
||||
)
|
||||
|
||||
const effectiveProviderId = useMemo(
|
||||
() => getProviderIdFromServiceId(effectiveServiceId),
|
||||
[effectiveServiceId]
|
||||
)
|
||||
|
||||
const {
|
||||
data: fetchedCredentials = [],
|
||||
data: credentials = [],
|
||||
isFetching: credentialsLoading,
|
||||
refetch: refetchCredentials,
|
||||
} = useOAuthCredentials(provider, true)
|
||||
} = useOAuthCredentials(effectiveProviderId, Boolean(effectiveProviderId))
|
||||
|
||||
const shouldFetchDetail =
|
||||
Boolean(value) &&
|
||||
!fetchedCredentials.some((cred) => cred.id === value) &&
|
||||
Boolean(activeWorkflowId)
|
||||
const selectedCredential = useMemo(
|
||||
() => credentials.find((cred) => cred.id === selectedId),
|
||||
[credentials, selectedId]
|
||||
)
|
||||
|
||||
const { data: collaboratorCredentials = [], isFetching: collaboratorLoading } =
|
||||
const shouldFetchForeignMeta =
|
||||
Boolean(selectedId) &&
|
||||
!selectedCredential &&
|
||||
Boolean(activeWorkflowId) &&
|
||||
Boolean(effectiveProviderId)
|
||||
|
||||
const { data: foreignCredentials = [], isFetching: foreignMetaLoading } =
|
||||
useOAuthCredentialDetail(
|
||||
shouldFetchDetail ? value : undefined,
|
||||
shouldFetchForeignMeta ? selectedId : undefined,
|
||||
activeWorkflowId || undefined,
|
||||
shouldFetchDetail
|
||||
shouldFetchForeignMeta
|
||||
)
|
||||
|
||||
const credentials = useMemo(() => {
|
||||
if (collaboratorCredentials.length === 0) {
|
||||
return fetchedCredentials
|
||||
}
|
||||
const collaborator = collaboratorCredentials[0]
|
||||
if (!collaborator) {
|
||||
return fetchedCredentials
|
||||
}
|
||||
const alreadyIncluded = fetchedCredentials.some((cred) => cred.id === collaborator.id)
|
||||
if (alreadyIncluded) {
|
||||
return fetchedCredentials
|
||||
}
|
||||
return [collaborator, ...fetchedCredentials]
|
||||
}, [fetchedCredentials, collaboratorCredentials])
|
||||
const hasForeignMeta = foreignCredentials.length > 0
|
||||
const isForeign = Boolean(selectedId && !selectedCredential && hasForeignMeta)
|
||||
|
||||
const resolvedLabel = useMemo(() => {
|
||||
if (selectedCredential) return selectedCredential.name
|
||||
if (isForeign) return 'Saved by collaborator'
|
||||
return ''
|
||||
}, [selectedCredential, isForeign])
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
if (!isEditing) {
|
||||
setInputValue(resolvedLabel)
|
||||
}
|
||||
}, [resolvedLabel, isEditing])
|
||||
|
||||
const invalidSelection =
|
||||
Boolean(selectedId) &&
|
||||
!selectedCredential &&
|
||||
!hasForeignMeta &&
|
||||
!credentialsLoading &&
|
||||
!foreignMetaLoading
|
||||
|
||||
useEffect(() => {
|
||||
if (!invalidSelection) return
|
||||
onChange('')
|
||||
}, [invalidSelection, onChange])
|
||||
|
||||
useCredentialRefreshTriggers(refetchCredentials, effectiveProviderId, provider)
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
void refetchCredentials()
|
||||
}
|
||||
}
|
||||
},
|
||||
[refetchCredentials]
|
||||
)
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
}, [refetchCredentials])
|
||||
|
||||
const handleSelect = (credentialId: string) => {
|
||||
setSelectedId(credentialId)
|
||||
onChange(credentialId)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleOAuthClose = () => {
|
||||
setShowOAuthModal(false)
|
||||
void refetchCredentials()
|
||||
}
|
||||
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
setOpen(isOpen)
|
||||
if (isOpen) {
|
||||
void refetchCredentials()
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCredential = credentials.find((cred) => cred.id === selectedId)
|
||||
const isForeign = !!(selectedId && !selectedCredential)
|
||||
|
||||
// Determine if additional permissions are required for the selected credential
|
||||
const hasSelection = !!selectedCredential
|
||||
const hasSelection = Boolean(selectedCredential)
|
||||
const missingRequiredScopes = hasSelection
|
||||
? getMissingRequiredScopes(selectedCredential, requiredScopes || [])
|
||||
? getMissingRequiredScopes(selectedCredential!, requiredScopes || [])
|
||||
: []
|
||||
|
||||
const needsUpdate =
|
||||
hasSelection && missingRequiredScopes.length > 0 && !disabled && !credentialsLoading
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(credentialId: string) => {
|
||||
onChange(credentialId)
|
||||
setIsEditing(false)
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
||||
const handleAddCredential = useCallback(() => {
|
||||
setShowOAuthModal(true)
|
||||
}, [])
|
||||
|
||||
const comboboxOptions = useMemo(() => {
|
||||
const options = credentials.map((cred) => ({
|
||||
label: cred.name,
|
||||
value: cred.id,
|
||||
}))
|
||||
|
||||
if (credentials.length === 0) {
|
||||
options.push({
|
||||
label: `Connect ${getProviderName(provider)} account`,
|
||||
value: '__connect_account__',
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}, [credentials, provider])
|
||||
|
||||
const selectedCredentialProvider = selectedCredential?.provider ?? provider
|
||||
|
||||
const overlayContent = useMemo(() => {
|
||||
if (!inputValue) return null
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center truncate'>
|
||||
<div className='mr-2 flex-shrink-0 opacity-90'>
|
||||
{getProviderIcon(selectedCredentialProvider)}
|
||||
</div>
|
||||
<span className='truncate'>{inputValue}</span>
|
||||
</div>
|
||||
)
|
||||
}, [inputValue, selectedCredentialProvider])
|
||||
|
||||
const handleComboboxChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (newValue === '__connect_account__') {
|
||||
handleAddCredential()
|
||||
return
|
||||
}
|
||||
|
||||
const matchedCred = credentials.find((c) => c.id === newValue)
|
||||
if (matchedCred) {
|
||||
setInputValue(matchedCred.name)
|
||||
handleSelect(newValue)
|
||||
return
|
||||
}
|
||||
|
||||
setIsEditing(true)
|
||||
setInputValue(newValue)
|
||||
},
|
||||
[credentials, handleAddCredential, handleSelect]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
aria-expanded={open}
|
||||
className='h-10 w-full min-w-0 justify-between'
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{getProviderIcon(provider)}
|
||||
<span
|
||||
className={
|
||||
selectedCredential ? 'truncate font-normal' : 'truncate text-muted-foreground'
|
||||
}
|
||||
>
|
||||
{selectedCredential
|
||||
? selectedCredential.name
|
||||
: isForeign
|
||||
? 'Saved by collaborator'
|
||||
: label}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[300px] p-0' align='start'>
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{credentialsLoading || collaboratorLoading ? (
|
||||
<div className='flex items-center justify-center p-4'>
|
||||
<RefreshCw className='h-4 w-4 animate-spin' />
|
||||
<span className='ml-2'>Loading...</span>
|
||||
</div>
|
||||
) : credentials.length === 0 ? (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='font-medium text-sm'>No accounts connected.</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Connect a {getProviderName(provider)} account to continue.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className='p-4 text-center'>
|
||||
<p className='font-medium text-sm'>No accounts found.</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
|
||||
{credentials.length > 0 && (
|
||||
<CommandGroup>
|
||||
{credentials.map((credential) => (
|
||||
<CommandItem
|
||||
key={credential.id}
|
||||
value={credential.id}
|
||||
onSelect={() => handleSelect(credential.id)}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
{getProviderIcon(credential.provider)}
|
||||
<span className='font-normal'>{credential.name}</span>
|
||||
</div>
|
||||
{credential.id === selectedId && <Check className='ml-auto h-4 w-4' />}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
<CommandGroup>
|
||||
<CommandItem onSelect={() => setShowOAuthModal(true)}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Plus className='h-4 w-4' />
|
||||
<span className='font-normal'>Connect {getProviderName(provider)} account</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Combobox
|
||||
options={comboboxOptions}
|
||||
value={inputValue}
|
||||
selectedValue={selectedId}
|
||||
onChange={handleComboboxChange}
|
||||
onOpenChange={handleOpenChange}
|
||||
placeholder={label}
|
||||
disabled={disabled}
|
||||
editable={true}
|
||||
filterOptions={true}
|
||||
isLoading={credentialsLoading}
|
||||
overlayContent={overlayContent}
|
||||
className={selectedId ? 'pl-[28px]' : ''}
|
||||
/>
|
||||
|
||||
{needsUpdate && (
|
||||
<div className='mt-2 flex items-center justify-between rounded-[6px] border border-amber-300/40 bg-amber-50/60 px-2 py-1 font-medium text-[12px] transition-colors dark:bg-amber-950/10'>
|
||||
<span>Additional permissions required</span>
|
||||
{/* We don't have reliable foreign detection context here; always show CTA */}
|
||||
<Button onClick={() => setShowOAuthModal(true)}>Update access</Button>
|
||||
{!isForeign && <Button onClick={() => setShowOAuthModal(true)}>Update access</Button>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<OAuthRequiredModal
|
||||
isOpen={showOAuthModal}
|
||||
onClose={handleOAuthClose}
|
||||
provider={provider}
|
||||
toolName={label}
|
||||
requiredScopes={getCanonicalScopesForProvider(
|
||||
serviceId ? getProviderIdFromServiceId(serviceId) : (provider as string)
|
||||
)}
|
||||
newScopes={missingRequiredScopes}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
{showOAuthModal && (
|
||||
<OAuthRequiredModal
|
||||
isOpen={showOAuthModal}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
provider={provider}
|
||||
toolName={getProviderName(provider)}
|
||||
requiredScopes={getCanonicalScopesForProvider(effectiveProviderId)}
|
||||
newScopes={missingRequiredScopes}
|
||||
serviceId={effectiveServiceId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function useCredentialRefreshTriggers(
|
||||
refetchCredentials: () => Promise<unknown>,
|
||||
effectiveProviderId?: string,
|
||||
provider?: OAuthProvider
|
||||
) {
|
||||
useEffect(() => {
|
||||
const refresh = () => {
|
||||
void refetchCredentials()
|
||||
}
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageShow = (event: Event) => {
|
||||
if ('persisted' in event && (event as PageTransitionEvent).persisted) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
const handleCredentialDisconnected = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ providerId?: string }>
|
||||
const providerId = customEvent.detail?.providerId
|
||||
|
||||
if (
|
||||
providerId &&
|
||||
(providerId === effectiveProviderId || (provider && providerId.startsWith(provider)))
|
||||
) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.addEventListener('pageshow', handlePageShow)
|
||||
window.addEventListener('credential-disconnected', handleCredentialDisconnected)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.removeEventListener('pageshow', handlePageShow)
|
||||
window.removeEventListener('credential-disconnected', handleCredentialDisconnected)
|
||||
}
|
||||
}, [refetchCredentials, effectiveProviderId, provider])
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,6 +74,8 @@ export type SubBlockType =
|
||||
| 'input-mapping' // Map parent variables to child workflow input schema
|
||||
| 'variables-input' // Variable assignments for updating workflow variables
|
||||
| 'messages-input' // Multiple message inputs with role and content for LLM message history
|
||||
| 'workflow-selector' // Workflow selector for agent tools
|
||||
| 'workflow-input-mapper' // Dynamic workflow input mapper based on selected workflow
|
||||
| 'text' // Read-only text display
|
||||
|
||||
/**
|
||||
@@ -87,6 +89,7 @@ export const SELECTOR_TYPES_HYDRATION_REQUIRED: SubBlockType[] = [
|
||||
'folder-selector',
|
||||
'project-selector',
|
||||
'knowledge-base-selector',
|
||||
'workflow-selector',
|
||||
'document-selector',
|
||||
'variables-input',
|
||||
'mcp-server-selector',
|
||||
|
||||
@@ -42,7 +42,8 @@ async function fetchWorkflows(workspaceId: string): Promise<WorkflowMetadata[]>
|
||||
return data.map(mapWorkflow)
|
||||
}
|
||||
|
||||
export function useWorkflows(workspaceId?: string) {
|
||||
export function useWorkflows(workspaceId?: string, options?: { syncRegistry?: boolean }) {
|
||||
const { syncRegistry = true } = options || {}
|
||||
const beginMetadataLoad = useWorkflowRegistry((state) => state.beginMetadataLoad)
|
||||
const completeMetadataLoad = useWorkflowRegistry((state) => state.completeMetadataLoad)
|
||||
const failMetadataLoad = useWorkflowRegistry((state) => state.failMetadataLoad)
|
||||
@@ -56,24 +57,24 @@ export function useWorkflows(workspaceId?: string) {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceId && query.status === 'pending') {
|
||||
if (syncRegistry && workspaceId && query.status === 'pending') {
|
||||
beginMetadataLoad(workspaceId)
|
||||
}
|
||||
}, [workspaceId, query.status, beginMetadataLoad])
|
||||
}, [syncRegistry, workspaceId, query.status, beginMetadataLoad])
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceId && query.status === 'success' && query.data) {
|
||||
if (syncRegistry && workspaceId && query.status === 'success' && query.data) {
|
||||
completeMetadataLoad(workspaceId, query.data)
|
||||
}
|
||||
}, [workspaceId, query.status, query.data, completeMetadataLoad])
|
||||
}, [syncRegistry, workspaceId, query.status, query.data, completeMetadataLoad])
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceId && query.status === 'error') {
|
||||
if (syncRegistry && workspaceId && query.status === 'error') {
|
||||
const message =
|
||||
query.error instanceof Error ? query.error.message : 'Failed to fetch workflows'
|
||||
failMetadataLoad(workspaceId, message)
|
||||
}
|
||||
}, [workspaceId, query.status, query.error, failMetadataLoad])
|
||||
}, [syncRegistry, workspaceId, query.status, query.error, failMetadataLoad])
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Sandbox } from '@e2b/code-interpreter'
|
||||
import { env } from '@/lib/env'
|
||||
import { CodeLanguage } from '@/lib/execution/languages'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -20,13 +21,7 @@ const logger = createLogger('E2BExecution')
|
||||
export async function executeInE2B(req: E2BExecutionRequest): Promise<E2BExecutionResult> {
|
||||
const { code, language, timeoutMs } = req
|
||||
|
||||
logger.info(`Executing code in E2B`, {
|
||||
code,
|
||||
language,
|
||||
timeoutMs,
|
||||
})
|
||||
|
||||
const apiKey = process.env.E2B_API_KEY
|
||||
const apiKey = env.E2B_API_KEY
|
||||
if (!apiKey) {
|
||||
throw new Error('E2B_API_KEY is required when E2B is enabled')
|
||||
}
|
||||
@@ -42,7 +37,6 @@ export async function executeInE2B(req: E2BExecutionRequest): Promise<E2BExecuti
|
||||
timeoutMs,
|
||||
})
|
||||
|
||||
// Check for execution errors
|
||||
if (execution.error) {
|
||||
const errorMessage = `${execution.error.name}: ${execution.error.value}`
|
||||
logger.error(`E2B execution error`, {
|
||||
@@ -51,7 +45,6 @@ export async function executeInE2B(req: E2BExecutionRequest): Promise<E2BExecuti
|
||||
errorMessage,
|
||||
})
|
||||
|
||||
// Include error traceback in stdout if available
|
||||
const errorOutput = execution.error.traceback || errorMessage
|
||||
return {
|
||||
result: null,
|
||||
@@ -61,7 +54,6 @@ export async function executeInE2B(req: E2BExecutionRequest): Promise<E2BExecuti
|
||||
}
|
||||
}
|
||||
|
||||
// Get output from execution
|
||||
if (execution.text) {
|
||||
stdoutChunks.push(execution.text)
|
||||
}
|
||||
|
||||
@@ -491,7 +491,7 @@ export async function transformBlockTool(
|
||||
const userProvidedParams = block.params || {}
|
||||
|
||||
// Create LLM schema that excludes user-provided parameters
|
||||
const llmSchema = createLLMToolSchema(toolConfig, userProvidedParams)
|
||||
const llmSchema = await createLLMToolSchema(toolConfig, userProvidedParams)
|
||||
|
||||
// Return formatted tool config
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,7 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
id: 'function_execute',
|
||||
name: 'Function Execute',
|
||||
description:
|
||||
'Execute JavaScript code in a secure, sandboxed environment with proper isolation and resource limits.',
|
||||
'Execute JavaScript code. fetch() is available. Code runs in async IIFE wrapper automatically. CRITICAL: Write plain statements with await/return, NOT wrapped in functions. Example for API call: const res = await fetch(url); const data = await res.json(); return data;',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
@@ -15,7 +15,8 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The code to execute',
|
||||
description:
|
||||
'Raw JavaScript statements (NOT a function). Code is auto-wrapped in async context. MUST use fetch() for HTTP (NOT xhr/axios/request libs). Write like: await fetch(url) then return result. NO import/require statements.',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
@@ -27,35 +28,35 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'hidden',
|
||||
description: 'Execution timeout in milliseconds',
|
||||
default: DEFAULT_EXECUTION_TIMEOUT_MS,
|
||||
},
|
||||
envVars: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'hidden',
|
||||
description: 'Environment variables to make available during execution',
|
||||
default: {},
|
||||
},
|
||||
blockData: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'hidden',
|
||||
description: 'Block output data for variable resolution',
|
||||
default: {},
|
||||
},
|
||||
blockNameMapping: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'hidden',
|
||||
description: 'Mapping of block names to block IDs',
|
||||
default: {},
|
||||
},
|
||||
workflowVariables: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
visibility: 'hidden',
|
||||
description: 'Workflow variables for <variable.name> resolution',
|
||||
default: {},
|
||||
},
|
||||
|
||||
@@ -11,57 +11,76 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base containing the document',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the document',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content of the document',
|
||||
},
|
||||
tag1: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 1 value for the document',
|
||||
},
|
||||
tag2: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 2 value for the document',
|
||||
},
|
||||
tag3: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 3 value for the document',
|
||||
},
|
||||
tag4: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 4 value for the document',
|
||||
},
|
||||
tag5: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 5 value for the document',
|
||||
},
|
||||
tag6: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 6 value for the document',
|
||||
},
|
||||
tag7: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag 7 value for the document',
|
||||
},
|
||||
documentTagsData: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Structured tag data with names, types, and values',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tagName: { type: 'string' },
|
||||
tagValue: { type: 'string' },
|
||||
tagType: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -11,22 +11,33 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base to search in',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query text (optional when using tag filters)',
|
||||
},
|
||||
topK: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of most similar results to return (1-100)',
|
||||
},
|
||||
tagFilters: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of tag filters with tagName and tagValue properties',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tagName: { type: 'string' },
|
||||
tagValue: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -11,16 +11,19 @@ export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkRespo
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the knowledge base containing the document',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the document to upload the chunk to',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Content of the chunk to upload',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -82,13 +82,13 @@ describe('Tool Parameters Utils', () => {
|
||||
})
|
||||
|
||||
describe('createLLMToolSchema', () => {
|
||||
it.concurrent('should create schema excluding user-provided parameters', () => {
|
||||
it.concurrent('should create schema excluding user-provided parameters', async () => {
|
||||
const userProvidedParams = {
|
||||
apiKey: 'user-provided-key',
|
||||
channel: '#general',
|
||||
}
|
||||
|
||||
const schema = createLLMToolSchema(mockToolConfig, userProvidedParams)
|
||||
const schema = await createLLMToolSchema(mockToolConfig, userProvidedParams)
|
||||
|
||||
expect(schema.properties).not.toHaveProperty('apiKey') // user-only, excluded
|
||||
expect(schema.properties).not.toHaveProperty('channel') // user-provided, excluded
|
||||
@@ -98,8 +98,8 @@ describe('Tool Parameters Utils', () => {
|
||||
expect(schema.required).not.toContain('apiKey') // user-only, never required for LLM
|
||||
})
|
||||
|
||||
it.concurrent('should include all parameters when none are user-provided', () => {
|
||||
const schema = createLLMToolSchema(mockToolConfig, {})
|
||||
it.concurrent('should include all parameters when none are user-provided', async () => {
|
||||
const schema = await createLLMToolSchema(mockToolConfig, {})
|
||||
|
||||
expect(schema.properties).not.toHaveProperty('apiKey') // user-only, never shown to LLM
|
||||
expect(schema.properties).toHaveProperty('message') // user-or-llm, shown to LLM
|
||||
@@ -226,8 +226,8 @@ describe('Tool Parameters Utils', () => {
|
||||
})
|
||||
|
||||
describe('Type Interface Validation', () => {
|
||||
it.concurrent('should have properly typed ToolSchema', () => {
|
||||
const schema: ToolSchema = createLLMToolSchema(mockToolConfig, {})
|
||||
it.concurrent('should have properly typed ToolSchema', async () => {
|
||||
const schema: ToolSchema = await createLLMToolSchema(mockToolConfig, {})
|
||||
|
||||
expect(schema.type).toBe('object')
|
||||
expect(typeof schema.properties).toBe('object')
|
||||
|
||||
@@ -11,7 +11,8 @@ export interface Option {
|
||||
|
||||
export interface ComponentCondition {
|
||||
field: string
|
||||
value: string
|
||||
value: string | number | boolean | Array<string | number | boolean>
|
||||
not?: boolean
|
||||
}
|
||||
|
||||
export interface UIComponentConfig {
|
||||
@@ -35,6 +36,7 @@ export interface UIComponentConfig {
|
||||
generationType?: string
|
||||
acceptedTypes?: string[]
|
||||
multiple?: boolean
|
||||
multiSelect?: boolean
|
||||
maxSize?: number
|
||||
dependsOn?: string[]
|
||||
}
|
||||
@@ -108,6 +110,12 @@ export interface ToolWithParameters {
|
||||
|
||||
let blockConfigCache: Record<string, BlockConfig> | null = null
|
||||
|
||||
const workflowInputFieldsCache = new Map<
|
||||
string,
|
||||
{ fields: Array<{ name: string; type: string }>; timestamp: number }
|
||||
>()
|
||||
const WORKFLOW_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
function getBlockConfigurations(): Record<string, BlockConfig> {
|
||||
if (!blockConfigCache) {
|
||||
try {
|
||||
@@ -146,6 +154,51 @@ export function getToolParametersConfig(
|
||||
return null
|
||||
}
|
||||
|
||||
// Special handling for workflow_executor tool
|
||||
if (toolId === 'workflow_executor') {
|
||||
const parameters: ToolParameterConfig[] = [
|
||||
{
|
||||
id: 'workflowId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the workflow to execute',
|
||||
uiComponent: {
|
||||
type: 'workflow-selector',
|
||||
placeholder: 'Select workflow to execute',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inputMapping',
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Map inputs to the selected workflow',
|
||||
uiComponent: {
|
||||
type: 'workflow-input-mapper',
|
||||
title: 'Workflow Inputs',
|
||||
condition: {
|
||||
field: 'workflowId',
|
||||
value: '',
|
||||
not: true, // Show when workflowId is not empty
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
toolConfig,
|
||||
allParameters: parameters,
|
||||
userInputParameters: parameters.filter(
|
||||
(param) => param.visibility === 'user-or-llm' || param.visibility === 'user-only'
|
||||
),
|
||||
requiredParameters: parameters.filter((param) => param.required),
|
||||
optionalParameters: parameters.filter(
|
||||
(param) => param.visibility === 'user-only' && !param.required
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Get block configuration for UI component information
|
||||
let blockConfig: BlockConfig | null = null
|
||||
if (blockType) {
|
||||
@@ -276,10 +329,10 @@ export function getToolParametersConfig(
|
||||
/**
|
||||
* Creates a tool schema for LLM with user-provided parameters excluded
|
||||
*/
|
||||
export function createLLMToolSchema(
|
||||
export async function createLLMToolSchema(
|
||||
toolConfig: ToolConfig,
|
||||
userProvidedParams: Record<string, unknown>
|
||||
): ToolSchema {
|
||||
): Promise<ToolSchema> {
|
||||
const schema: ToolSchema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -287,7 +340,7 @@ export function createLLMToolSchema(
|
||||
}
|
||||
|
||||
// Only include parameters that the LLM should/can provide
|
||||
Object.entries(toolConfig.params).forEach(([paramId, param]) => {
|
||||
for (const [paramId, param] of Object.entries(toolConfig.params)) {
|
||||
const isUserProvided =
|
||||
userProvidedParams[paramId] !== undefined &&
|
||||
userProvidedParams[paramId] !== null &&
|
||||
@@ -295,17 +348,17 @@ export function createLLMToolSchema(
|
||||
|
||||
// Skip parameters that user has already provided
|
||||
if (isUserProvided) {
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip parameters that are user-only (never shown to LLM)
|
||||
if (param.visibility === 'user-only') {
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip hidden parameters
|
||||
if (param.visibility === 'hidden') {
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
// Add parameter to LLM schema
|
||||
@@ -314,20 +367,140 @@ export function createLLMToolSchema(
|
||||
schemaType = 'object'
|
||||
}
|
||||
|
||||
schema.properties[paramId] = {
|
||||
const propertySchema: any = {
|
||||
type: schemaType,
|
||||
description: param.description || '',
|
||||
}
|
||||
|
||||
// Include items property for arrays
|
||||
if (param.type === 'array' && param.items) {
|
||||
propertySchema.items = {
|
||||
...param.items,
|
||||
...(param.items.properties && {
|
||||
properties: { ...param.items.properties },
|
||||
}),
|
||||
}
|
||||
} else if (param.items) {
|
||||
logger.warn(
|
||||
`items property ignored for non-array param "${paramId}" in tool "${toolConfig.id}"`
|
||||
)
|
||||
}
|
||||
|
||||
// Special handling for workflow_executor's inputMapping parameter
|
||||
if (toolConfig.id === 'workflow_executor' && paramId === 'inputMapping') {
|
||||
const workflowId = userProvidedParams.workflowId as string
|
||||
if (workflowId) {
|
||||
await applyDynamicSchemaForWorkflow(propertySchema, workflowId)
|
||||
}
|
||||
}
|
||||
|
||||
schema.properties[paramId] = propertySchema
|
||||
|
||||
// Add to required if LLM must provide it and it's originally required
|
||||
if ((param.visibility === 'user-or-llm' || param.visibility === 'llm-only') && param.required) {
|
||||
schema.required.push(paramId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply dynamic schema enrichment for workflow_executor's inputMapping parameter
|
||||
*/
|
||||
async function applyDynamicSchemaForWorkflow(
|
||||
propertySchema: any,
|
||||
workflowId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const workflowInputFields = await fetchWorkflowInputFields(workflowId)
|
||||
|
||||
if (workflowInputFields && workflowInputFields.length > 0) {
|
||||
propertySchema.type = 'object'
|
||||
propertySchema.properties = {}
|
||||
propertySchema.required = []
|
||||
|
||||
// Convert workflow input fields to JSON schema properties
|
||||
for (const field of workflowInputFields) {
|
||||
propertySchema.properties[field.name] = {
|
||||
type: field.type || 'string',
|
||||
description: `Input field: ${field.name}`,
|
||||
}
|
||||
propertySchema.required.push(field.name)
|
||||
}
|
||||
|
||||
// Update description to be more specific
|
||||
propertySchema.description = `Input values for the workflow. Required fields: ${workflowInputFields.map((f) => f.name).join(', ')}`
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch workflow input fields for LLM schema:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to fetch workflow input fields with caching
|
||||
*/
|
||||
async function fetchWorkflowInputFields(
|
||||
workflowId: string
|
||||
): Promise<Array<{ name: string; type: string }>> {
|
||||
// Check cache first
|
||||
const cached = workflowInputFieldsCache.get(workflowId)
|
||||
const now = Date.now()
|
||||
|
||||
if (cached && now - cached.timestamp < WORKFLOW_CACHE_TTL) {
|
||||
return cached.fields
|
||||
}
|
||||
|
||||
try {
|
||||
const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http')
|
||||
|
||||
const headers = await buildAuthHeaders()
|
||||
const url = buildAPIUrl(`/api/workflows/${workflowId}`)
|
||||
|
||||
const response = await fetch(url.toString(), { headers })
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch workflow')
|
||||
}
|
||||
|
||||
const { data } = await response.json()
|
||||
if (!data?.state?.blocks) {
|
||||
return []
|
||||
}
|
||||
|
||||
const blocks = data.state.blocks as Record<string, any>
|
||||
const triggerEntry = Object.entries(blocks).find(
|
||||
([, block]) =>
|
||||
block.type === 'start_trigger' || block.type === 'input_trigger' || block.type === 'starter'
|
||||
)
|
||||
|
||||
if (!triggerEntry) {
|
||||
return []
|
||||
}
|
||||
|
||||
const triggerBlock = triggerEntry[1]
|
||||
const inputFormat = triggerBlock.subBlocks?.inputFormat?.value
|
||||
|
||||
let fields: Array<{ name: string; type: string }> = []
|
||||
|
||||
if (Array.isArray(inputFormat)) {
|
||||
fields = inputFormat
|
||||
.filter((field: any) => field.name && typeof field.name === 'string')
|
||||
.map((field: any) => ({
|
||||
name: field.name,
|
||||
type: field.type || 'string',
|
||||
}))
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
workflowInputFieldsCache.set(workflowId, { fields, timestamp: now })
|
||||
|
||||
return fields
|
||||
} catch (error) {
|
||||
logger.error('Error fetching workflow input fields:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete tool schema for execution with all parameters
|
||||
*/
|
||||
@@ -339,11 +512,27 @@ export function createExecutionToolSchema(toolConfig: ToolConfig): ToolSchema {
|
||||
}
|
||||
|
||||
Object.entries(toolConfig.params).forEach(([paramId, param]) => {
|
||||
schema.properties[paramId] = {
|
||||
const propertySchema: any = {
|
||||
type: param.type === 'json' ? 'object' : param.type,
|
||||
description: param.description || '',
|
||||
}
|
||||
|
||||
// Include items property for arrays
|
||||
if (param.type === 'array' && param.items) {
|
||||
propertySchema.items = {
|
||||
...param.items,
|
||||
...(param.items.properties && {
|
||||
properties: { ...param.items.properties },
|
||||
}),
|
||||
}
|
||||
} else if (param.items) {
|
||||
logger.warn(
|
||||
`items property ignored for non-array param "${paramId}" in tool "${toolConfig.id}"`
|
||||
)
|
||||
}
|
||||
|
||||
schema.properties[paramId] = propertySchema
|
||||
|
||||
if (param.required) {
|
||||
schema.required.push(paramId)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@ export interface ToolConfig<P = any, R = any> {
|
||||
visibility?: ParameterVisibility
|
||||
default?: any
|
||||
description?: string
|
||||
items?: {
|
||||
type: string
|
||||
description?: string
|
||||
properties?: Record<string, { type: string; description?: string }>
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { WorkflowExecutorParams, WorkflowExecutorResponse } from '@/tools/workflow/types'
|
||||
|
||||
const logger = createLogger('WorkflowExecutorTool')
|
||||
|
||||
/**
|
||||
* Tool for executing workflows as blocks within other workflows.
|
||||
* This tool is used by the WorkflowBlockHandler to provide the execution capability.
|
||||
@@ -14,38 +11,46 @@ export const workflowExecutorTool: ToolConfig<
|
||||
> = {
|
||||
id: 'workflow_executor',
|
||||
name: 'Workflow Executor',
|
||||
description: 'Execute another workflow inline as a block',
|
||||
description:
|
||||
'Execute another workflow as a sub-workflow. Pass inputs as a JSON object with field names matching the child workflow\'s input format. Example: if child expects "name" and "email", pass {"name": "John", "email": "john@example.com"}',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the workflow to execute',
|
||||
},
|
||||
inputMapping: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
description: 'JSON object mapping parent data to child workflow inputs',
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON object with keys matching the child workflow\'s input field names. Each key should map to the value you want to pass for that input field. Example: {"fieldName": "value", "otherField": 123}',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/workflow-executor',
|
||||
url: (params: WorkflowExecutorParams) => `/api/workflows/${params.workflowId}/execute`,
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
body: (params) => params,
|
||||
body: (params: WorkflowExecutorParams) => ({
|
||||
input: params.inputMapping || {},
|
||||
triggerType: 'api',
|
||||
useDraftState: false,
|
||||
}),
|
||||
},
|
||||
transformResponse: async (response: any) => {
|
||||
logger.info('Workflow executor tool response received', { response })
|
||||
|
||||
// Extract success state from response, default to false if not present
|
||||
const success = response?.success ?? false
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const outputData = data?.output ?? {}
|
||||
|
||||
return {
|
||||
success,
|
||||
duration: response?.duration ?? 0,
|
||||
childWorkflowId: response?.childWorkflowId ?? '',
|
||||
childWorkflowName: response?.childWorkflowName ?? '',
|
||||
...response,
|
||||
success: data?.success ?? false,
|
||||
duration: data?.metadata?.duration ?? 0,
|
||||
childWorkflowId: data?.workflowId ?? '',
|
||||
childWorkflowName: data?.workflowName ?? '',
|
||||
output: outputData, // For OpenAI provider
|
||||
result: outputData, // For backwards compatibility
|
||||
error: data?.error,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user