feat: llm node support tools

This commit is contained in:
zxhlyh 2025-12-29 14:55:26 +08:00
parent 0cff94d90e
commit d60348572e
9 changed files with 162 additions and 17 deletions

View File

@ -6,17 +6,20 @@ export type BoxProps = {
className?: string
children?: ReactNode
withBorderBottom?: boolean
withBorderTop?: boolean
}
export const Box = memo(({
className,
children,
withBorderBottom,
withBorderTop,
}: BoxProps) => {
return (
<div
className={cn(
'py-2',
withBorderBottom && 'border-b border-divider-subtle',
withBorderTop && 'border-t border-divider-subtle',
className,
)}
>

View File

@ -9,6 +9,7 @@ import { cn } from '@/utils/classnames'
export type FieldTitleProps = {
title?: string
className?: string
operation?: ReactNode
subTitle?: string | ReactNode
tooltip?: string
@ -19,6 +20,7 @@ export type FieldTitleProps = {
}
export const FieldTitle = memo(({
title,
className,
operation,
subTitle,
tooltip,
@ -31,7 +33,7 @@ export const FieldTitle = memo(({
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
return (
<div className={cn('mb-0.5', !!subTitle && 'mb-1')}>
<div className={cn('mb-0.5', !!subTitle && 'mb-1', className)}>
<div
className="group/collapse flex items-center justify-between py-1"
onClick={() => {

View File

@ -6,17 +6,20 @@ export type GroupProps = {
className?: string
children?: ReactNode
withBorderBottom?: boolean
withBorderTop?: boolean
}
export const Group = memo(({
className,
children,
withBorderBottom,
withBorderTop,
}: GroupProps) => {
return (
<div
className={cn(
'px-4 py-2',
withBorderBottom && 'border-b border-divider-subtle',
withBorderTop && 'border-t border-divider-subtle',
className,
)}
>

View File

@ -1,4 +1,6 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
@ -18,3 +20,27 @@ const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
}
export default useNodeCrud
export const useNodeCurdKit = <T>(id: string) => {
const store = useStoreApi()
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const getNodeData = useCallback(() => {
const { getNodes } = store.getState()
const nodes = getNodes()
return nodes.find(node => node.id === id)
}, [store, id])
const handleNodeDataUpdate = useCallback((data: Partial<CommonNodeType<T>>) => {
handleNodeDataUpdateWithSyncDraft({
id,
data,
})
}, [id, handleNodeDataUpdateWithSyncDraft])
return {
getNodeData,
handleNodeDataUpdate,
}
}

View File

@ -1,28 +1,54 @@
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import { BoxGroup } from '@/app/components/workflow/nodes/_base/components/layout'
import MaxIterations from './max-iterations'
import { useNodeTools } from './use-node-tools'
const i18nPrefix = 'workflow.nodes.llm'
const Tools = () => {
type ToolsProps = {
nodeId: string
tools?: ToolValue[]
maxIterations?: number
}
const Tools = ({
nodeId,
tools = [],
maxIterations = 10,
}: ToolsProps) => {
const { t } = useTranslation()
const {
handleToolsChange,
handleMaxIterationsChange,
} = useNodeTools(nodeId)
return (
<Field
title={t(`${i18nPrefix}.tools.title`)}
tooltip={t('appDebug.vision.description')!}
operations={(
<Tooltip
popupContent={t('appDebug.vision.onlySupportVisionModelTip')!}
>
</Tooltip>
)}
<BoxGroup
boxProps={{
withBorderBottom: true,
withBorderTop: true,
}}
groupProps={{
className: 'px-0',
}}
>
<div>
<div>Tools</div>
</div>
</Field>
<MultipleToolSelector
nodeId={nodeId}
nodeOutputVars={[]}
availableNodes={[]}
value={tools}
label={t(`${i18nPrefix}.tools.title`)}
tooltip={t(`${i18nPrefix}.tools.title`)}
onChange={handleToolsChange}
supportCollapse
/>
<MaxIterations
value={maxIterations}
onChange={handleMaxIterationsChange}
/>
</BoxGroup>
)
}

View File

@ -0,0 +1,40 @@
import { memo } from 'react'
import { InputNumber } from '@/app/components/base/input-number'
import Slider from '@/app/components/base/slider'
import Tooltip from '@/app/components/base/tooltip'
type MaxIterationsProps = {
value?: number
onChange?: (value: number) => void
}
const MaxIterations = ({ value = 10, onChange }: MaxIterationsProps) => {
return (
<div className="mt-3 flex h-10 items-center">
<div className="system-sm-semibold mr-0.5 truncate uppercase text-text-secondary">Max Iterations</div>
<Tooltip
popupContent="Max Iterations is the maximum number of iterations to run the tool."
triggerClassName="shrink-0 w-4 h-4"
/>
<div className="mr-3 flex grow items-center justify-end">
<Slider
className="w-[124px]"
value={value}
onChange={onChange ?? (() => {})}
min={1}
max={99}
step={1}
/>
</div>
<InputNumber
className="w-10 shrink-0"
value={value}
onChange={onChange ?? (() => {})}
min={1}
max={99}
step={1}
/>
</div>
)
}
export default memo(MaxIterations)

View File

@ -0,0 +1,23 @@
import type { LLMNodeType } from '../../types'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import { useNodeCurdKit } from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
export const useNodeTools = (nodeId: string) => {
const { handleNodeDataUpdate } = useNodeCurdKit<LLMNodeType>(nodeId)
const handleToolsChange = (tools: ToolValue[]) => {
handleNodeDataUpdate({
tools,
})
}
const handleMaxIterationsChange = (maxIterations: number) => {
handleNodeDataUpdate({
max_iterations: maxIterations,
})
}
return {
handleToolsChange,
handleMaxIterationsChange,
}
}

View File

@ -22,6 +22,7 @@ import VarReferencePicker from '../_base/components/variable/var-reference-picke
import ConfigPrompt from './components/config-prompt'
import ReasoningFormatConfig from './components/reasoning-format-config'
import StructureOutput from './components/structure-output'
import Tools from './components/tools'
import useConfig from './use-config'
const i18nPrefix = 'workflow.nodes.llm'
@ -233,6 +234,12 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
</>
)}
<Tools
nodeId={id}
tools={inputs.tools}
maxIterations={inputs.max_iterations}
/>
{/* Vision: GPT4-vision and so on */}
<ConfigVision
nodeId={id}

View File

@ -1,5 +1,18 @@
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Variable, VisionSetting } from '@/app/components/workflow/types'
export type Tool = {
enabled: boolean
type: string
provider_name: 'plugin' | 'builtin' | 'api' | 'workflow' | 'app' | 'dataset-retrieval'
tool_name: string
plugin_unique_identifier?: string
credential_id?: string
parameters?: Record<string, any>
settings?: Record<string, any>
extra?: Record<string, any>
}
export type LLMNodeType = CommonNodeType & {
model: ModelConfig
prompt_template: PromptItem[] | PromptItem
@ -18,6 +31,8 @@ export type LLMNodeType = CommonNodeType & {
structured_output_enabled?: boolean
structured_output?: StructuredOutput
reasoning_format?: 'tagged' | 'separated'
tools?: ToolValue[]
max_iterations?: number
}
export enum Type {