feat: llm node support tools

This commit is contained in:
zxhlyh
2026-01-08 14:27:37 +08:00
parent 70149ea05e
commit c323028179
11 changed files with 229 additions and 65 deletions

View File

@ -9,9 +9,9 @@ const ToolCalls = ({
}: ToolCallsProps) => {
return (
<div className="my-1 space-y-1">
{toolCalls.map((toolCall: ToolCallItem) => (
{toolCalls.map((toolCall: ToolCallItem, index: number) => (
<ToolCallItemComponent
key={toolCall.tool_call_id}
key={index}
payload={toolCall}
className="bg-background-gradient-bg-fill-chat-bubble-bg-2 shadow-none"
/>

View File

@ -319,6 +319,9 @@ export const useChat = (
return player
}
let toolCallId = ''
let thoughtId = ''
ssePost(
url,
{
@ -326,7 +329,19 @@ export const useChat = (
},
{
isPublicAPI,
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
onData: (message: string, isFirstMessage: boolean, {
conversationId: newConversationId,
messageId,
taskId,
chunk_type,
tool_icon,
tool_icon_dark,
tool_name,
tool_arguments,
tool_files,
tool_error,
tool_elapsed_time,
}: any) => {
if (!isAgentMode) {
responseItem.content = responseItem.content + message
}
@ -336,6 +351,57 @@ export const useChat = (
lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
}
if (chunk_type === 'tool_call') {
if (!responseItem.toolCalls)
responseItem.toolCalls = []
toolCallId = uuidV4()
responseItem.toolCalls?.push({
id: toolCallId,
type: 'tool',
toolName: tool_name,
toolArguments: tool_arguments,
toolIcon: tool_icon,
toolIconDark: tool_icon_dark,
})
}
if (chunk_type === 'tool_result') {
const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.id === toolCallId) ?? -1
if (currentToolCallIndex > -1) {
responseItem.toolCalls![currentToolCallIndex].toolError = tool_error
responseItem.toolCalls![currentToolCallIndex].toolDuration = tool_elapsed_time
responseItem.toolCalls![currentToolCallIndex].toolFiles = tool_files
responseItem.toolCalls![currentToolCallIndex].toolOutput = message
}
}
if (chunk_type === 'thought_start') {
if (!responseItem.toolCalls)
responseItem.toolCalls = []
thoughtId = uuidV4()
responseItem.toolCalls.push({
id: thoughtId,
type: 'thought',
thoughtOutput: '',
})
}
if (chunk_type === 'thought') {
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
if (currentThoughtIndex > -1) {
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
}
}
if (chunk_type === 'thought_end') {
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
if (currentThoughtIndex > -1) {
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
responseItem.toolCalls![currentThoughtIndex].thoughtCompleted = true
}
}
if (messageId && !hasSetResponseId) {
questionItem.id = `question-${messageId}`
responseItem.id = messageId

View File

@ -64,11 +64,6 @@ export type CitationItem = {
word_count: number
}
export type IconObject = {
background: string
content: string
}
export type IChatItem = {
id: string
content: string

View File

@ -150,6 +150,10 @@ export const LLM_OUTPUT_STRUCT: Var[] = [
variable: 'usage',
type: VarType.object,
},
{
variable: 'generation',
type: VarType.object,
},
]
export const KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT: Var[] = [

View File

@ -315,6 +315,11 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
type="object"
description={t(`${i18nPrefix}.outputVars.usage`, { ns: 'workflow' })}
/>
<VarItem
name="generation"
type="object"
description={t(`${i18nPrefix}.outputVars.generation`, { ns: 'workflow' })}
/>
{inputs.structured_output_enabled && (
<>
<Split className="mt-3" />

View File

@ -15,6 +15,7 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { v4 as uuidV4 } from 'uuid'
import {
getProcessedInputs,
processOpeningStatement,
@ -266,6 +267,8 @@ export const useChat = (
}
let hasSetResponseId = false
let toolCallId = ''
let thoughtId = ''
handleRun(
bodyParams,
@ -277,7 +280,6 @@ export const useChat = (
chunk_type,
tool_icon,
tool_icon_dark,
tool_call_id,
tool_name,
tool_arguments,
tool_files,
@ -289,43 +291,52 @@ export const useChat = (
if (chunk_type === 'tool_call') {
if (!responseItem.toolCalls)
responseItem.toolCalls = []
toolCallId = uuidV4()
responseItem.toolCalls?.push({
id: toolCallId,
type: 'tool',
tool_call_id,
tool_name,
tool_arguments,
tool_icon,
tool_icon_dark,
tool_files,
tool_error,
tool_elapsed_time,
toolName: tool_name,
toolArguments: tool_arguments,
toolIcon: tool_icon,
toolIconDark: tool_icon_dark,
})
}
if (chunk_type === 'tool_result') {
const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.tool_call_id === tool_call_id) ?? -1
const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.id === toolCallId) ?? -1
if (currentToolCallIndex > -1)
responseItem.toolCalls![currentToolCallIndex].tool_output = message
if (currentToolCallIndex > -1) {
responseItem.toolCalls![currentToolCallIndex].toolError = tool_error
responseItem.toolCalls![currentToolCallIndex].toolDuration = tool_elapsed_time
responseItem.toolCalls![currentToolCallIndex].toolFiles = tool_files
responseItem.toolCalls![currentToolCallIndex].toolOutput = message
}
}
if (chunk_type === 'thought_start') {
console.log(message, 'xx1')
responseItem.toolCalls?.push({
if (!responseItem.toolCalls)
responseItem.toolCalls = []
thoughtId = uuidV4()
responseItem.toolCalls.push({
id: thoughtId,
type: 'thought',
tool_elapsed_time,
thoughtOutput: '',
})
}
if (chunk_type === 'thought_end') {
console.log(message, 'xx2')
// const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.is_thought) ?? -1
// if (currentThoughtIndex > -1)
// responseItem.toolCalls![currentThoughtIndex].tool_output = message
if (chunk_type === 'thought') {
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
if (currentThoughtIndex > -1) {
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
}
}
if (chunk_type === 'thought') {
console.log(message, 'xx3')
if (chunk_type === 'thought_end') {
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1
if (currentThoughtIndex > -1) {
responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message
responseItem.toolCalls![currentThoughtIndex].thoughtCompleted = true
}
}
if (messageId && !hasSetResponseId) {

View File

@ -22,18 +22,30 @@ const LLMResultPanel: FC<Props> = ({
onBack,
}) => {
const { t } = useTranslation()
const formattedList = list.map(item => ({
type: item.type,
tool_call_id: item.provider,
tool_name: item.name,
tool_arguments: item.type === 'tool' ? item.output.arguments : undefined,
tool_icon: item.icon,
tool_icon_dark: item.icon_dark,
tool_files: [],
tool_error: item.error,
tool_output: item.type === 'tool' ? item.output.output : item.output,
tool_elapsed_time: item.duration,
}))
const formattedList = list.map((item) => {
if (item.type === 'tool') {
return {
type: 'tool',
toolName: item.name,
toolProvider: item.provider,
toolIcon: item.icon,
toolIconDark: item.icon_dark,
toolArguments: item.output.arguments,
toolOutput: item.output.output,
toolDuration: item.duration,
}
}
return {
type: 'model',
modelName: item.name,
modelProvider: item.provider,
modelIcon: item.icon,
modelIconDark: item.icon_dark,
modelOutput: item.output,
modelDuration: item.duration,
}
})
return (
<div>

View File

@ -4,6 +4,8 @@ import {
} from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { Thinking } from '@/app/components/base/icons/src/vender/workflow'
import BlockIcon from '@/app/components/workflow/block-icon'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -24,17 +26,73 @@ const ToolCallItemComponent = ({
<div
className={cn('rounded-[10px] border-[0.5px] border-components-panel-border bg-background-default-subtle px-2 pb-1 pt-2 shadow-xs', className)}
>
<div className="mb-1 flex cursor-pointer items-center hover:bg-background-gradient-bg-fill-chat-bubble-bg-2" onClick={() => setExpand(!expand)}>
<BlockIcon
type={BlockEnum.Tool}
toolIcon={payload.tool_icon}
className="mr-1 h-4 w-4 shrink-0"
/>
<div className="mr-1 grow truncate" title={payload.tool_name}>{payload.tool_name}</div>
<div
className="mb-1 flex cursor-pointer items-center hover:bg-background-gradient-bg-fill-chat-bubble-bg-2"
onClick={() => {
setExpand(!expand)
}}
>
{
!!payload.tool_elapsed_time && (
payload.type === 'thought' && (
<Thinking className="mr-1 h-4 w-4 shrink-0" />
)
}
{
payload.type === 'tool' && (
<BlockIcon
type={BlockEnum.Tool}
toolIcon={payload.toolIcon}
className="mr-1 h-4 w-4 shrink-0"
/>
)
}
{
payload.type === 'model' && (
<AppIcon
iconType={typeof payload.modelIcon === 'string' ? 'image' : undefined}
imageUrl={typeof payload.modelIcon === 'string' ? payload.modelIcon : undefined}
background={typeof payload.modelIcon === 'object' ? payload.modelIcon.background : undefined}
className="mr-1 h-4 w-4 shrink-0"
/>
)
}
{
payload.type === 'thought' && (
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.thoughtOutput}>
{
payload.thoughtCompleted && !expand && (payload.thoughtOutput || '') as string
}
{
payload.thoughtCompleted && expand && 'THOUGHT'
}
{
!payload.thoughtCompleted && 'THINKING...'
}
</div>
)
}
{
payload.type === 'tool' && (
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.toolName}>{payload.toolName}</div>
)
}
{
payload.type === 'model' && (
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.modelName}>{payload.modelName}</div>
)
}
{
!!payload.toolDuration && (
<div className="system-xs-regular mr-1 shrink-0 text-text-tertiary">
{payload.tool_elapsed_time?.toFixed(1)}
{payload.toolDuration?.toFixed(1)}
s
</div>
)
}
{
!!payload.modelDuration && (
<div className="system-xs-regular mr-1 shrink-0 text-text-tertiary">
{payload.modelDuration?.toFixed(1)}
s
</div>
)
@ -46,8 +104,8 @@ const ToolCallItemComponent = ({
<div className="relative px-2 pl-9">
<div className="absolute bottom-1 left-2 top-1 w-[1px] bg-divider-regular"></div>
{
payload.type === 'thought' && typeof payload.tool_output === 'string' && (
<div className="body-sm-medium text-text-tertiary">{payload.tool_output}</div>
payload.type === 'thought' && typeof payload.thoughtOutput === 'string' && (
<div className="body-sm-medium text-text-tertiary">{payload.thoughtOutput}</div>
)
}
{
@ -56,7 +114,7 @@ const ToolCallItemComponent = ({
readOnly
title={<div>{t('common.data', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.tool_output}
value={payload.modelOutput}
isJSONStringifyBeauty
/>
)
@ -67,7 +125,7 @@ const ToolCallItemComponent = ({
readOnly
title={<div>{t('common.input', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.tool_arguments}
value={payload.toolArguments}
isJSONStringifyBeauty
/>
)
@ -79,7 +137,7 @@ const ToolCallItemComponent = ({
className="mt-1"
title={<div>{t('common.output', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.tool_output}
value={payload.toolOutput}
isJSONStringifyBeauty
/>
)

View File

@ -651,6 +651,7 @@
"nodes.llm.jsonSchema.warningTips.saveSchema": "Please finish editing the current field before saving the schema",
"nodes.llm.model": "model",
"nodes.llm.notSetContextInPromptTip": "To enable the context feature, please fill in the context variable in PROMPT.",
"nodes.llm.outputVars.generation": "Generation Information",
"nodes.llm.outputVars.output": "Generate content",
"nodes.llm.outputVars.reasoning_content": "Reasoning Content",
"nodes.llm.outputVars.usage": "Model Usage Information",

View File

@ -651,6 +651,7 @@
"nodes.llm.jsonSchema.warningTips.saveSchema": "请先完成当前字段的编辑",
"nodes.llm.model": "模型",
"nodes.llm.notSetContextInPromptTip": "要启用上下文功能,请在提示中填写上下文变量。",
"nodes.llm.outputVars.generation": "生成信息",
"nodes.llm.outputVars.output": "生成内容",
"nodes.llm.outputVars.reasoning_content": "推理内容",
"nodes.llm.outputVars.usage": "模型用量信息",

View File

@ -34,16 +34,27 @@ export type IconObject = {
}
export type ToolCallItem = {
id: string
type: 'model' | 'tool' | 'thought'
tool_call_id?: string
tool_name?: string
tool_arguments?: string
tool_icon?: string | IconObject
tool_icon_dark?: string | IconObject
tool_files?: string[]
tool_error?: string
tool_output?: Record<string, any> | string
tool_elapsed_time?: number
thoughtCompleted?: boolean
thoughtOutput?: string
toolName?: string
toolProvider?: string
toolIcon?: string | IconObject
toolIconDark?: string | IconObject
toolArguments?: string
toolOutput?: Record<string, any> | string
toolFiles?: string[]
toolError?: string
toolDuration?: number
modelName?: string
modelProvider?: string
modelOutput?: Record<string, any> | string
modelDuration?: number
modelIcon?: string | IconObject
modelIconDark?: string | IconObject
}
export type ToolCallDetail = {