mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
Merge main HEAD (segment 5) into sandboxed-agent-rebase
Resolve 83 conflicts: 10 backend, 62 frontend, 11 config/lock files. Preserve sandbox/agent/collaboration features while adopting main's UI refactorings (Dialog/AlertDialog/Popover), model provider updates, and enterprise features. Made-with: Cursor
This commit is contained in:
@ -0,0 +1,194 @@
|
||||
import type { ToolNodeType } from '../../types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { VarType } from '../../types'
|
||||
import useConfig from '../use-config'
|
||||
|
||||
const mockSetInputs = vi.hoisted(() => vi.fn())
|
||||
const mockSetControlPromptEditorRerenderKey = vi.hoisted(() => vi.fn())
|
||||
const mockUseCurrentToolCollection = vi.hoisted(() => vi.fn())
|
||||
const mockGetConfiguredValue = vi.hoisted(() => vi.fn())
|
||||
const mockToolParametersToFormSchemas = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useLanguage: () => 'en_US',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useNodesReadOnly: () => ({ nodesReadOnly: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
|
||||
__esModule: true,
|
||||
default: (_id: string, data: ToolNodeType) => ({
|
||||
inputs: data,
|
||||
setInputs: mockSetInputs,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/tools/utils/to-form-schema', () => ({
|
||||
getConfiguredValue: (...args: unknown[]) => mockGetConfiguredValue(...args),
|
||||
toolParametersToFormSchemas: (...args: unknown[]) => mockToolParametersToFormSchemas(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/tools', () => ({
|
||||
updateBuiltInToolCredential: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setControlPromptEditorRerenderKey: mockSetControlPromptEditorRerenderKey,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../use-current-tool-collection', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockUseCurrentToolCollection(...args),
|
||||
}))
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
type: BlockEnum.Tool,
|
||||
provider_id: 'google_search',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_name: 'Google Search',
|
||||
tool_name: 'google_search',
|
||||
tool_label: 'Google Search',
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const currentTool = {
|
||||
name: 'google_search',
|
||||
parameters: [
|
||||
{
|
||||
variable: 'query',
|
||||
form: 'llm',
|
||||
label: { en_US: 'Query' },
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'default query',
|
||||
},
|
||||
{
|
||||
variable: 'api_key',
|
||||
form: 'credential',
|
||||
label: { en_US: 'API Key' },
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: 'default secret',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const currentToolWithoutDefaults = {
|
||||
name: 'google_search',
|
||||
parameters: [
|
||||
{
|
||||
variable: 'query',
|
||||
form: 'llm',
|
||||
label: { en_US: 'Query' },
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
variable: 'api_key',
|
||||
form: 'credential',
|
||||
label: { en_US: 'API Key' },
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const createToolVarInput = (value: string) => ({
|
||||
type: VarType.mixed,
|
||||
value,
|
||||
})
|
||||
|
||||
describe('useConfig', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
|
||||
mockUseCurrentToolCollection.mockReturnValue({
|
||||
currCollection: {
|
||||
name: 'google_search',
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
tools: [currentTool],
|
||||
},
|
||||
})
|
||||
|
||||
mockToolParametersToFormSchemas.mockImplementation(parameters => parameters)
|
||||
mockGetConfiguredValue.mockImplementation((_value, schema: Array<{ variable: string, default?: string }>) => {
|
||||
return schema.reduce<Record<string, ReturnType<typeof createToolVarInput>>>((acc, item) => {
|
||||
acc[item.variable] = createToolVarInput(item.default || '')
|
||||
return acc
|
||||
}, {} as Record<string, ReturnType<typeof createToolVarInput>>)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.runOnlyPendingTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('Default Value Sync', () => {
|
||||
it('should apply default values only once when the current payload is initially empty', () => {
|
||||
const emptyPayload = createNodeData()
|
||||
const syncedPayload = createNodeData({
|
||||
tool_parameters: { query: createToolVarInput('default query') },
|
||||
tool_configurations: { api_key: createToolVarInput('default secret') },
|
||||
})
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ payload }) => useConfig('tool-node-1', payload),
|
||||
{ initialProps: { payload: emptyPayload } },
|
||||
)
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
|
||||
tool_parameters: { query: createToolVarInput('default query') },
|
||||
tool_configurations: { api_key: createToolVarInput('default secret') },
|
||||
}))
|
||||
|
||||
rerender({ payload: syncedPayload })
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not update inputs when tool values are already populated on first render', () => {
|
||||
renderHook(() => useConfig('tool-node-1', createNodeData({
|
||||
tool_parameters: { query: createToolVarInput('existing query') },
|
||||
tool_configurations: { api_key: createToolVarInput('existing secret') },
|
||||
})))
|
||||
|
||||
expect(mockSetInputs).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not update inputs when empty schemas do not provide any default values', () => {
|
||||
mockUseCurrentToolCollection.mockReturnValue({
|
||||
currCollection: {
|
||||
name: 'google_search',
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
tools: [currentToolWithoutDefaults],
|
||||
},
|
||||
})
|
||||
mockGetConfiguredValue.mockReturnValue({})
|
||||
|
||||
renderHook(() => useConfig('tool-node-1', createNodeData()))
|
||||
|
||||
expect(mockSetInputs).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,84 @@
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import useCurrentToolCollection from '../use-current-tool-collection'
|
||||
|
||||
const mockUseAllBuiltInTools = vi.hoisted(() => vi.fn())
|
||||
const mockUseAllCustomTools = vi.hoisted(() => vi.fn())
|
||||
const mockUseAllWorkflowTools = vi.hoisted(() => vi.fn())
|
||||
const mockUseAllMCPTools = vi.hoisted(() => vi.fn())
|
||||
const mockCanFindTool = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useAllBuiltInTools: () => mockUseAllBuiltInTools(),
|
||||
useAllCustomTools: () => mockUseAllCustomTools(),
|
||||
useAllWorkflowTools: () => mockUseAllWorkflowTools(),
|
||||
useAllMCPTools: () => mockUseAllMCPTools(),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils', () => ({
|
||||
canFindTool: (...args: unknown[]) => mockCanFindTool(...args),
|
||||
}))
|
||||
|
||||
const createToolCollection = (overrides: Partial<ToolWithProvider> = {}): ToolWithProvider => ({
|
||||
id: 'builtin-search',
|
||||
name: 'Google Search',
|
||||
type: CollectionType.builtIn,
|
||||
label: { en_US: 'Google Search' },
|
||||
description: { en_US: 'Search provider' },
|
||||
icon: '',
|
||||
icon_dark: '',
|
||||
background: '',
|
||||
tags: [],
|
||||
tools: [],
|
||||
allow_delete: true,
|
||||
is_team_authorization: false,
|
||||
meta: {} as ToolWithProvider['meta'],
|
||||
...overrides,
|
||||
}) as ToolWithProvider
|
||||
|
||||
describe('useCurrentToolCollection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCanFindTool.mockImplementation((collectionId: string, providerId: string) => collectionId === providerId)
|
||||
mockUseAllBuiltInTools.mockReturnValue({ data: [] })
|
||||
mockUseAllCustomTools.mockReturnValue({ data: [] })
|
||||
mockUseAllWorkflowTools.mockReturnValue({ data: [] })
|
||||
mockUseAllMCPTools.mockReturnValue({ data: [] })
|
||||
})
|
||||
|
||||
it('should return the built-in collection list and matched provider for built-in tools', () => {
|
||||
const builtInCollection = createToolCollection({ id: 'builtin-search' })
|
||||
mockUseAllBuiltInTools.mockReturnValue({ data: [builtInCollection] })
|
||||
|
||||
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.builtIn, 'builtin-search'))
|
||||
|
||||
expect(result.current.currentTools).toEqual([builtInCollection])
|
||||
expect(result.current.currCollection).toBe(builtInCollection)
|
||||
expect(mockCanFindTool).toHaveBeenCalledWith('builtin-search', 'builtin-search')
|
||||
})
|
||||
|
||||
it('should select the custom tool collection when the provider type is custom', () => {
|
||||
const customCollection = createToolCollection({
|
||||
id: 'custom-search',
|
||||
type: CollectionType.custom,
|
||||
})
|
||||
mockUseAllCustomTools.mockReturnValue({ data: [customCollection] })
|
||||
|
||||
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.custom, 'custom-search'))
|
||||
|
||||
expect(result.current.currentTools).toEqual([customCollection])
|
||||
expect(result.current.currCollection).toBe(customCollection)
|
||||
})
|
||||
|
||||
it('should return undefined when no collection matches the provider id', () => {
|
||||
mockUseAllBuiltInTools.mockReturnValue({
|
||||
data: [createToolCollection({ id: 'another-tool' })],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useCurrentToolCollection(CollectionType.builtIn, 'builtin-search'))
|
||||
|
||||
expect(result.current.currentTools).toHaveLength(1)
|
||||
expect(result.current.currCollection).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,49 @@
|
||||
import type { ToolNodeType } from '../../types'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import useGetDataForCheckMore from '../use-get-data-for-check-more'
|
||||
|
||||
const mockUseConfig = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../use-config', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockUseConfig(...args),
|
||||
}))
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
type: BlockEnum.Tool,
|
||||
provider_id: 'google_search',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_name: 'Google Search',
|
||||
tool_name: 'google_search',
|
||||
tool_label: 'Google Search',
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('useGetDataForCheckMore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should expose the config hook validator as getData', () => {
|
||||
const getMoreDataForCheckValid = vi.fn(() => ({ provider: 'google' }))
|
||||
const payload = createNodeData()
|
||||
mockUseConfig.mockReturnValue({
|
||||
getMoreDataForCheckValid,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useGetDataForCheckMore({
|
||||
id: 'tool-node-1',
|
||||
payload,
|
||||
}))
|
||||
|
||||
expect(mockUseConfig).toHaveBeenCalledWith('tool-node-1', payload)
|
||||
expect(result.current.getData).toBe(getMoreDataForCheckValid)
|
||||
expect(result.current.getData()).toEqual({ provider: 'google' })
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,206 @@
|
||||
import type { ToolNodeType, VarType } from '../../types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
||||
import useSingleRunFormParams from '../use-single-run-form-params'
|
||||
|
||||
const mockUseToolIcon = vi.hoisted(() => vi.fn())
|
||||
const mockUseNodeCrud = vi.hoisted(() => vi.fn())
|
||||
const mockFormatToTracingNodeList = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useToolIcon: (...args: unknown[]) => mockUseToolIcon(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockUseNodeCrud(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/run/utils/format-log', () => ({
|
||||
__esModule: true,
|
||||
default: (...args: unknown[]) => mockFormatToTracingNodeList(...args),
|
||||
}))
|
||||
|
||||
const createNodeData = (overrides: Partial<ToolNodeType> = {}): ToolNodeType => ({
|
||||
title: 'Google Search',
|
||||
desc: '',
|
||||
type: BlockEnum.Tool,
|
||||
provider_id: 'google_search',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_name: 'Google Search',
|
||||
tool_name: 'google_search',
|
||||
tool_label: 'Google Search',
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createInputVar = (variable: InputVar['variable']): InputVar => ({
|
||||
type: InputVarType.textInput,
|
||||
label: typeof variable === 'string' ? variable : 'invalid',
|
||||
variable,
|
||||
required: false,
|
||||
})
|
||||
|
||||
const createRunResult = (): NodeTracing => ({
|
||||
id: 'trace-1',
|
||||
index: 0,
|
||||
predecessor_node_id: '',
|
||||
node_id: 'tool-node-1',
|
||||
node_type: BlockEnum.Tool,
|
||||
title: 'Google Search',
|
||||
inputs: {},
|
||||
inputs_truncated: false,
|
||||
process_data: {},
|
||||
process_data_truncated: false,
|
||||
outputs_truncated: false,
|
||||
status: 'succeeded',
|
||||
elapsed_time: 1,
|
||||
metadata: {
|
||||
iterator_length: 0,
|
||||
iterator_index: 0,
|
||||
loop_length: 0,
|
||||
loop_index: 0,
|
||||
},
|
||||
created_at: 0,
|
||||
created_by: {
|
||||
id: 'user-1',
|
||||
name: 'User',
|
||||
email: 'user@example.com',
|
||||
},
|
||||
finished_at: 1,
|
||||
})
|
||||
|
||||
describe('useSingleRunFormParams', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseToolIcon.mockReturnValue('tool-icon')
|
||||
mockFormatToTracingNodeList.mockReturnValue([{ id: 'formatted-node' }])
|
||||
mockUseNodeCrud.mockImplementation((_id: string, payload: ToolNodeType) => ({
|
||||
inputs: payload,
|
||||
}))
|
||||
})
|
||||
|
||||
describe('Variable Extraction', () => {
|
||||
it('should build form inputs from variable params and settings and expose dependent vars', () => {
|
||||
const payload = createNodeData({
|
||||
tool_parameters: {
|
||||
query: { type: 'variable' as VarType, value: ['start', 'query'] },
|
||||
legacy_query: { type: 'variable' as VarType, value: 'legacy.answer' },
|
||||
constant_query: { type: 'constant' as VarType, value: 'fixed' },
|
||||
},
|
||||
tool_configurations: {
|
||||
prompt: { type: 'mixed' as VarType, value: 'prefix {{#tool.result#}}' },
|
||||
api_key: { type: 'constant' as VarType, value: 'secret' },
|
||||
plainText: 'ignored',
|
||||
},
|
||||
})
|
||||
const getInputVars = vi.fn(() => [
|
||||
createInputVar('#start.query#'),
|
||||
createInputVar(undefined as unknown as string),
|
||||
createInputVar('#legacy.answer#'),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useSingleRunFormParams({
|
||||
id: 'tool-node-1',
|
||||
payload,
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
getInputVars,
|
||||
setRunInputData: vi.fn(),
|
||||
toVarInputs: vi.fn(),
|
||||
runResult: null as unknown as NodeTracing,
|
||||
}))
|
||||
|
||||
expect(getInputVars).toHaveBeenCalledWith([
|
||||
'{{#start.query#}}',
|
||||
'{{#legacy.answer#}}',
|
||||
'prefix {{#tool.result#}}',
|
||||
])
|
||||
expect(result.current.forms).toHaveLength(1)
|
||||
expect(result.current.forms[0].inputs).toEqual([
|
||||
createInputVar('#start.query#'),
|
||||
createInputVar(undefined as unknown as string),
|
||||
createInputVar('#legacy.answer#'),
|
||||
])
|
||||
expect(result.current.forms[0].values).toEqual({})
|
||||
expect(result.current.toolIcon).toBe('tool-icon')
|
||||
expect(result.current.getDependentVars()).toEqual([
|
||||
['start', 'query'],
|
||||
['legacy', 'answer'],
|
||||
])
|
||||
expect(result.current.nodeInfo).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Updates', () => {
|
||||
it('should update form values and forward run input data on change', () => {
|
||||
const payload = createNodeData({
|
||||
tool_parameters: {
|
||||
nullable_constant: { type: 'constant' as VarType, value: null },
|
||||
query: { type: 'variable' as VarType, value: ['start', 'query'] },
|
||||
},
|
||||
})
|
||||
const getInputVars = vi.fn(() => [createInputVar('#start.query#')])
|
||||
const setRunInputData = vi.fn()
|
||||
|
||||
const { result } = renderHook(() => useSingleRunFormParams({
|
||||
id: 'tool-node-1',
|
||||
payload,
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
getInputVars,
|
||||
setRunInputData,
|
||||
toVarInputs: vi.fn(),
|
||||
runResult: null as unknown as NodeTracing,
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.forms[0].onChange({
|
||||
query: 'weather',
|
||||
tool_parameters: {
|
||||
nullable_constant: 'temp-value',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
expect(setRunInputData).toHaveBeenCalledWith({
|
||||
query: 'weather',
|
||||
tool_parameters: {
|
||||
nullable_constant: 'temp-value',
|
||||
},
|
||||
})
|
||||
expect(result.current.forms[0].values).toEqual({
|
||||
query: 'weather',
|
||||
tool_parameters: {
|
||||
nullable_constant: 'temp-value',
|
||||
},
|
||||
nullable_constant: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tracing Data', () => {
|
||||
it('should format the latest run result into node info when a run result exists', () => {
|
||||
const payload = createNodeData()
|
||||
const runResult = createRunResult()
|
||||
|
||||
const { result } = renderHook(() => useSingleRunFormParams({
|
||||
id: 'tool-node-1',
|
||||
payload,
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
getInputVars: vi.fn(() => []),
|
||||
setRunInputData: vi.fn(),
|
||||
toVarInputs: vi.fn(),
|
||||
runResult,
|
||||
}))
|
||||
|
||||
expect(mockFormatToTracingNodeList).toHaveBeenCalledWith([runResult], expect.any(Function))
|
||||
expect(result.current.nodeInfo).toEqual({ id: 'formatted-node' })
|
||||
})
|
||||
})
|
||||
})
|
||||
287
web/app/components/workflow/nodes/tool/hooks/use-config.ts
Normal file
287
web/app/components/workflow/nodes/tool/hooks/use-config.ts
Normal file
@ -0,0 +1,287 @@
|
||||
import type { ToolNodeType, ToolVarInputs } from '../types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { capitalize } from 'es-toolkit/string'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import {
|
||||
getConfiguredValue,
|
||||
toolParametersToFormSchemas,
|
||||
} from '@/app/components/tools/utils/to-form-schema'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { updateBuiltInToolCredential } from '@/service/tools'
|
||||
import {
|
||||
useInvalidToolsByType,
|
||||
} from '@/service/use-tools'
|
||||
import { isToolAuthorizationRequired } from '../auth'
|
||||
import { normalizeJsonSchemaType } from '../output-schema-utils'
|
||||
import useCurrentToolCollection from './use-current-tool-collection'
|
||||
|
||||
const formatDisplayType = (output: Record<string, unknown>): string => {
|
||||
const normalizedType = normalizeJsonSchemaType(output) || 'Unknown'
|
||||
return capitalize(normalizedType)
|
||||
}
|
||||
|
||||
const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const language = useLanguage()
|
||||
const { inputs, setInputs: doSetInputs } = useNodeCrud<ToolNodeType>(
|
||||
id,
|
||||
payload,
|
||||
)
|
||||
/*
|
||||
* tool_configurations: tool setting, not dynamic setting (form type = form)
|
||||
* tool_parameters: tool dynamic setting(form type = llm)
|
||||
*/
|
||||
const {
|
||||
provider_id,
|
||||
provider_type,
|
||||
tool_name,
|
||||
tool_configurations,
|
||||
tool_parameters,
|
||||
} = inputs
|
||||
const isBuiltIn = provider_type === CollectionType.builtIn
|
||||
const { currCollection } = useCurrentToolCollection(provider_type, provider_id)
|
||||
|
||||
// Auth
|
||||
const isShowAuthBtn = isToolAuthorizationRequired(provider_type, currCollection)
|
||||
const [
|
||||
showSetAuth,
|
||||
{ setTrue: showSetAuthModal, setFalse: hideSetAuthModal },
|
||||
] = useBoolean(false)
|
||||
|
||||
const invalidToolsByType = useInvalidToolsByType(provider_type)
|
||||
const handleSaveAuth = useCallback(
|
||||
async (value: any) => {
|
||||
await updateBuiltInToolCredential(currCollection?.name as string, value)
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('api.actionSuccess', { ns: 'common' }),
|
||||
})
|
||||
invalidToolsByType()
|
||||
hideSetAuthModal()
|
||||
},
|
||||
[
|
||||
currCollection?.name,
|
||||
hideSetAuthModal,
|
||||
t,
|
||||
invalidToolsByType,
|
||||
],
|
||||
)
|
||||
|
||||
const currTool = useMemo(() => {
|
||||
return currCollection?.tools.find(tool => tool.name === tool_name)
|
||||
}, [currCollection, tool_name])
|
||||
const formSchemas = useMemo(() => {
|
||||
return currTool ? toolParametersToFormSchemas(currTool.parameters) : []
|
||||
}, [currTool])
|
||||
const toolInputVarSchema = useMemo(() => {
|
||||
return formSchemas.filter((item: any) => item.form === 'llm')
|
||||
}, [formSchemas])
|
||||
// use setting
|
||||
const toolSettingSchema = useMemo(() => {
|
||||
return formSchemas.filter((item: any) => item.form !== 'llm')
|
||||
}, [formSchemas])
|
||||
const hasShouldTransferTypeSettingInput = toolSettingSchema.some(
|
||||
item => item.type === 'boolean' || item.type === 'number-input',
|
||||
)
|
||||
|
||||
const setInputs = useCallback(
|
||||
(value: ToolNodeType) => {
|
||||
if (!hasShouldTransferTypeSettingInput) {
|
||||
doSetInputs(value)
|
||||
return
|
||||
}
|
||||
const newInputs = produce(value, (draft) => {
|
||||
const newConfig = { ...draft.tool_configurations }
|
||||
Object.keys(draft.tool_configurations).forEach((key) => {
|
||||
const schema = formSchemas.find(item => item.variable === key)
|
||||
const value = newConfig[key]
|
||||
if (schema?.type === 'boolean') {
|
||||
if (typeof value === 'string')
|
||||
newConfig[key] = value === 'true' || value === '1'
|
||||
|
||||
if (typeof value === 'number')
|
||||
newConfig[key] = value === 1
|
||||
}
|
||||
|
||||
if (schema?.type === 'number-input') {
|
||||
if (typeof value === 'string' && value !== '')
|
||||
newConfig[key] = Number.parseFloat(value)
|
||||
}
|
||||
})
|
||||
draft.tool_configurations = newConfig
|
||||
})
|
||||
doSetInputs(newInputs)
|
||||
},
|
||||
[doSetInputs, formSchemas, hasShouldTransferTypeSettingInput],
|
||||
)
|
||||
const [notSetDefaultValue, setNotSetDefaultValue] = useState(false)
|
||||
const toolSettingValue = useMemo(() => {
|
||||
if (notSetDefaultValue)
|
||||
return tool_configurations
|
||||
return getConfiguredValue(tool_configurations, toolSettingSchema)
|
||||
}, [notSetDefaultValue, toolSettingSchema, tool_configurations])
|
||||
const setToolSettingValue = useCallback(
|
||||
(value: Record<string, any>) => {
|
||||
setNotSetDefaultValue(true)
|
||||
setInputs({
|
||||
...inputs,
|
||||
tool_configurations: value,
|
||||
})
|
||||
},
|
||||
[inputs, setInputs],
|
||||
)
|
||||
|
||||
const formattingParameters = useCallback(() => {
|
||||
const inputsWithDefaultValue = produce(inputs, (draft) => {
|
||||
if (
|
||||
!draft.tool_configurations
|
||||
|| Object.keys(draft.tool_configurations).length === 0
|
||||
) {
|
||||
const configuredToolSettings = getConfiguredValue(
|
||||
tool_configurations,
|
||||
toolSettingSchema,
|
||||
) as ToolVarInputs
|
||||
if (Object.keys(configuredToolSettings).length > 0)
|
||||
draft.tool_configurations = configuredToolSettings
|
||||
}
|
||||
if (
|
||||
!draft.tool_parameters
|
||||
|| Object.keys(draft.tool_parameters).length === 0
|
||||
) {
|
||||
const configuredToolParameters = getConfiguredValue(
|
||||
tool_parameters,
|
||||
toolInputVarSchema,
|
||||
) as ToolVarInputs
|
||||
if (Object.keys(configuredToolParameters).length > 0)
|
||||
draft.tool_parameters = configuredToolParameters
|
||||
}
|
||||
})
|
||||
return inputsWithDefaultValue
|
||||
}, [inputs, toolInputVarSchema, toolSettingSchema, tool_configurations, tool_parameters])
|
||||
|
||||
useEffect(() => {
|
||||
if (!currTool)
|
||||
return
|
||||
const inputsWithDefaultValue = formattingParameters()
|
||||
if (inputsWithDefaultValue === inputs)
|
||||
return
|
||||
|
||||
const { setControlPromptEditorRerenderKey } = workflowStore.getState()
|
||||
setInputs(inputsWithDefaultValue)
|
||||
const rerenderTimeout = setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
||||
|
||||
return () => {
|
||||
clearTimeout(rerenderTimeout)
|
||||
}
|
||||
}, [currTool, formattingParameters, inputs, setInputs, workflowStore])
|
||||
|
||||
// setting when call
|
||||
const setInputVar = useCallback(
|
||||
(value: ToolVarInputs) => {
|
||||
setInputs({
|
||||
...inputs,
|
||||
tool_parameters: value,
|
||||
})
|
||||
},
|
||||
[inputs, setInputs],
|
||||
)
|
||||
|
||||
const isLoading = currTool && (isBuiltIn ? !currCollection : false)
|
||||
|
||||
const getMoreDataForCheckValid = () => {
|
||||
return {
|
||||
toolInputsSchema: (() => {
|
||||
const formInputs: InputVar[] = []
|
||||
toolInputVarSchema.forEach((item: any) => {
|
||||
formInputs.push({
|
||||
label: item.label[language] || item.label.en_US,
|
||||
variable: item.variable,
|
||||
type: item.type,
|
||||
required: item.required,
|
||||
})
|
||||
})
|
||||
return formInputs
|
||||
})(),
|
||||
notAuthed: isShowAuthBtn,
|
||||
toolSettingSchema,
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
const outputSchema = useMemo(() => {
|
||||
const res: any[] = []
|
||||
const output_schema = currTool?.output_schema
|
||||
if (!output_schema || !output_schema.properties)
|
||||
return res
|
||||
|
||||
Object.keys(output_schema.properties).forEach((outputKey) => {
|
||||
const output = output_schema.properties[outputKey]
|
||||
const type = output.type
|
||||
if (type === 'object') {
|
||||
res.push({
|
||||
name: outputKey,
|
||||
value: output,
|
||||
})
|
||||
}
|
||||
else {
|
||||
const normalizedType = normalizeJsonSchemaType(output)
|
||||
res.push({
|
||||
name: outputKey,
|
||||
type:
|
||||
normalizedType === 'array'
|
||||
? `Array[${output.items ? formatDisplayType(output.items) : 'Unknown'}]`
|
||||
: formatDisplayType(output),
|
||||
description: output.description,
|
||||
})
|
||||
}
|
||||
})
|
||||
return res
|
||||
}, [currTool])
|
||||
|
||||
const hasObjectOutput = useMemo(() => {
|
||||
const output_schema = currTool?.output_schema
|
||||
if (!output_schema || !output_schema.properties)
|
||||
return false
|
||||
const properties = output_schema.properties
|
||||
return Object.keys(properties).some(
|
||||
key => properties[key].type === 'object',
|
||||
)
|
||||
}, [currTool])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
currTool,
|
||||
toolSettingSchema,
|
||||
toolSettingValue,
|
||||
setToolSettingValue,
|
||||
toolInputVarSchema,
|
||||
setInputVar,
|
||||
currCollection,
|
||||
isShowAuthBtn,
|
||||
showSetAuth,
|
||||
showSetAuthModal,
|
||||
hideSetAuthModal,
|
||||
handleSaveAuth,
|
||||
isLoading,
|
||||
outputSchema,
|
||||
hasObjectOutput,
|
||||
getMoreDataForCheckValid,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@ -0,0 +1,47 @@
|
||||
import type { ToolNodeType } from '../types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { useMemo } from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
const useCurrentToolCollection = (
|
||||
providerType: ToolNodeType['provider_type'],
|
||||
providerId: string,
|
||||
) => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
const currentTools = useMemo<ToolWithProvider[]>(() => {
|
||||
switch (providerType) {
|
||||
case CollectionType.builtIn:
|
||||
return buildInTools || []
|
||||
case CollectionType.custom:
|
||||
return customTools || []
|
||||
case CollectionType.workflow:
|
||||
return workflowTools || []
|
||||
case CollectionType.mcp:
|
||||
return mcpTools || []
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}, [buildInTools, customTools, mcpTools, providerType, workflowTools])
|
||||
|
||||
const currCollection = useMemo(() => {
|
||||
return currentTools.find(item => canFindTool(item.id, providerId))
|
||||
}, [currentTools, providerId])
|
||||
|
||||
return {
|
||||
currentTools,
|
||||
currCollection,
|
||||
}
|
||||
}
|
||||
|
||||
export default useCurrentToolCollection
|
||||
@ -0,0 +1,20 @@
|
||||
import type { ToolNodeType } from '../types'
|
||||
import useConfig from './use-config'
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
payload: ToolNodeType
|
||||
}
|
||||
|
||||
const useGetDataForCheckMore = ({
|
||||
id,
|
||||
payload,
|
||||
}: Params) => {
|
||||
const { getMoreDataForCheckValid } = useConfig(id, payload)
|
||||
|
||||
return {
|
||||
getData: getMoreDataForCheckValid,
|
||||
}
|
||||
}
|
||||
|
||||
export default useGetDataForCheckMore
|
||||
@ -0,0 +1,171 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { ToolNodeType } from '../types'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToolIcon } from '@/app/components/workflow/hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
|
||||
import { AGENT_CONTEXT_VAR_PATTERN } from '@/app/components/workflow/utils/agent-context'
|
||||
import { VarType } from '../types'
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
payload: ToolNodeType
|
||||
runInputData: Record<string, any>
|
||||
runInputDataRef: RefObject<Record<string, any>>
|
||||
getInputVars: (textList: string[]) => InputVar[]
|
||||
setRunInputData: (data: Record<string, any>) => void
|
||||
toVarInputs: (variables: Variable[]) => InputVar[]
|
||||
runResult: NodeTracing
|
||||
}
|
||||
type NestedNodeParam = {
|
||||
type?: VarType
|
||||
value?: unknown
|
||||
nested_node_config?: {
|
||||
extractor_node_id?: string
|
||||
output_selector?: unknown
|
||||
}
|
||||
}
|
||||
const useSingleRunFormParams = ({
|
||||
id,
|
||||
payload,
|
||||
getInputVars,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
}: Params) => {
|
||||
const { t } = useTranslation()
|
||||
const { inputs } = useNodeCrud<ToolNodeType>(id, payload)
|
||||
|
||||
const hadVarParams = Object.keys(inputs.tool_parameters)
|
||||
.filter(key => ![VarType.constant, VarType.nested_node].includes(inputs.tool_parameters[key].type))
|
||||
.map(k => inputs.tool_parameters[k])
|
||||
|
||||
const hadVarSettings = Object.keys(inputs.tool_configurations)
|
||||
.filter(key => typeof inputs.tool_configurations[key] === 'object' && inputs.tool_configurations[key].type && inputs.tool_configurations[key].type !== VarType.constant)
|
||||
.map(k => inputs.tool_configurations[k])
|
||||
|
||||
const varInputs = getInputVars([...hadVarParams, ...hadVarSettings].map((p) => {
|
||||
if (p.type === VarType.variable) {
|
||||
// handle the old wrong value not crash the page
|
||||
if (!(p.value as any).join)
|
||||
return `{{#${p.value}#}}`
|
||||
|
||||
return `{{#${(p.value as ValueSelector).join('.')}#}}`
|
||||
}
|
||||
|
||||
return p.value as string
|
||||
}))
|
||||
const [inputVarValues, setInputVarValues] = useState<Record<string, any>>({})
|
||||
const handleInputVarValuesChange = useCallback((value: Record<string, any>) => {
|
||||
setInputVarValues(value)
|
||||
setRunInputData(value)
|
||||
}, [setRunInputData])
|
||||
|
||||
const inputVarValuesWithConstantValue = useCallback(() => {
|
||||
const res = produce(inputVarValues, (draft) => {
|
||||
Object.keys(inputs.tool_parameters).forEach((key: string) => {
|
||||
const { type, value } = inputs.tool_parameters[key]
|
||||
if (type === VarType.constant && (value === undefined || value === null)) {
|
||||
if (!draft.tool_parameters || !draft.tool_parameters[key])
|
||||
return
|
||||
draft[key] = value
|
||||
}
|
||||
})
|
||||
})
|
||||
return res
|
||||
}, [inputs.tool_parameters, inputVarValues])
|
||||
|
||||
const forms = useMemo(() => {
|
||||
const forms: FormProps[] = [{
|
||||
inputs: varInputs,
|
||||
values: inputVarValuesWithConstantValue(),
|
||||
onChange: handleInputVarValuesChange,
|
||||
}]
|
||||
return forms
|
||||
}, [handleInputVarValuesChange, inputVarValuesWithConstantValue, varInputs])
|
||||
|
||||
const nodeInfo = useMemo(() => {
|
||||
if (!runResult)
|
||||
return null
|
||||
return formatToTracingNodeList([runResult], t)[0]
|
||||
}, [runResult, t])
|
||||
|
||||
const toolIcon = useToolIcon(payload)
|
||||
|
||||
const resolveOutputSelector = (extractorNodeId: string, rawSelector?: unknown) => {
|
||||
if (!Array.isArray(rawSelector))
|
||||
return [] as string[]
|
||||
if (rawSelector[0] === extractorNodeId)
|
||||
return rawSelector.slice(1) as string[]
|
||||
return rawSelector as string[]
|
||||
}
|
||||
|
||||
const getDefaultNestedOutputSelector = (paramKey: string, value?: unknown) => {
|
||||
if (typeof value === 'string') {
|
||||
const matches = Array.from(value.matchAll(AGENT_CONTEXT_VAR_PATTERN))
|
||||
if (matches.length > 0)
|
||||
return ['structured_output', paramKey]
|
||||
}
|
||||
return ['result']
|
||||
}
|
||||
|
||||
const collectNestedNodeSelectors = (params: Record<string, NestedNodeParam> = {}) => {
|
||||
return Object.entries(params).flatMap(([paramKey, param]) => {
|
||||
if (!param || param.type !== VarType.nested_node)
|
||||
return [] as ValueSelector[]
|
||||
|
||||
const nestedConfig = param.nested_node_config
|
||||
const extractorNodeId = nestedConfig?.extractor_node_id || `${id}_ext_${paramKey}`
|
||||
const rawSelector = nestedConfig?.output_selector
|
||||
const resolvedOutputSelector = resolveOutputSelector(extractorNodeId, rawSelector)
|
||||
const outputSelector = resolvedOutputSelector.length > 0
|
||||
? resolvedOutputSelector
|
||||
: getDefaultNestedOutputSelector(paramKey, param.value)
|
||||
|
||||
return outputSelector.length > 0
|
||||
? [[extractorNodeId, ...outputSelector]]
|
||||
: []
|
||||
})
|
||||
}
|
||||
|
||||
const getDependentVars = () => {
|
||||
const selectorList: ValueSelector[] = []
|
||||
|
||||
varInputs.forEach((item) => {
|
||||
// Guard against null/undefined variable to prevent app crash
|
||||
if (!item.variable || typeof item.variable !== 'string')
|
||||
return
|
||||
const selector = item.variable.slice(1, -1).split('.')
|
||||
if (selector.length > 0)
|
||||
selectorList.push(selector)
|
||||
})
|
||||
|
||||
const nestedSelectors = [
|
||||
...collectNestedNodeSelectors(inputs.tool_parameters as Record<string, NestedNodeParam>),
|
||||
...collectNestedNodeSelectors(inputs.tool_configurations as Record<string, NestedNodeParam>),
|
||||
]
|
||||
selectorList.push(...nestedSelectors)
|
||||
|
||||
const seen = new Set<string>()
|
||||
return selectorList.filter((selector) => {
|
||||
const key = selector.join('.')
|
||||
if (seen.has(key))
|
||||
return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
forms,
|
||||
nodeInfo,
|
||||
toolIcon,
|
||||
getDependentVars,
|
||||
}
|
||||
}
|
||||
|
||||
export default useSingleRunFormParams
|
||||
Reference in New Issue
Block a user