feat: llm node support tools

This commit is contained in:
zxhlyh 2026-01-04 18:03:47 +08:00
parent e83635ee5a
commit ececc5ec2c
6 changed files with 172 additions and 1 deletions

View File

@ -21,6 +21,7 @@ import BasicContent from './basic-content'
import More from './more'
import Operation from './operation'
import SuggestedQuestions from './suggested-questions'
import ToolCalls from './tool-calls'
import WorkflowProcessItem from './workflow-process'
type AnswerProps = {
@ -61,6 +62,7 @@ const Answer: FC<AnswerProps> = ({
workflowProcess,
allFiles,
message_files,
toolCalls,
} = item
const hasAgentThoughts = !!agent_thoughts?.length
@ -154,6 +156,11 @@ const Answer: FC<AnswerProps> = ({
/>
)
}
{
!!toolCalls?.length && (
<ToolCalls toolCalls={toolCalls} />
)
}
{
responding && contentIsEmpty && !hasAgentThoughts && (
<div className="flex h-5 w-6 items-center justify-center">

View File

@ -0,0 +1,19 @@
import type { ToolCallItem } from '../../type'
import ToolCallsItem from './item'
type ToolCallsProps = {
toolCalls: ToolCallItem[]
}
const ToolCalls = ({
toolCalls,
}: ToolCallsProps) => {
return (
<div>
{toolCalls.map((toolCall: ToolCallItem) => (
<ToolCallsItem key={toolCall.tool_call_id} payload={toolCall} />
))}
</div>
)
}
export default ToolCalls

View File

@ -0,0 +1,75 @@
import type { ToolCallItem } from '../../type'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
type ToolCallsItemProps = {
payload: ToolCallItem
}
const ToolCallsItem = ({
payload,
}: ToolCallsItemProps) => {
const { t } = useTranslation()
const [expand, setExpand] = useState(false)
return (
<div
className="rounded-xl bg-background-gradient-bg-fill-chat-bubble-bg-1 px-2 pb-1 pt-2"
>
<div className="mb-1 flex cursor-pointer items-center hover:bg-background-gradient-bg-fill-chat-bubble-bg-2" onClick={() => setExpand(!expand)}>
<div className="mr-1 h-5 w-5 grow truncate" title={payload.tool_name}>{payload.tool_name}</div>
{
!!payload.tool_elapsed_time && (
<div className="system-xs-regular mr-1 shrink-0 text-text-tertiary">
{payload.tool_elapsed_time?.toFixed(3)}
s
</div>
)
}
<RiArrowDownSLine className="h-4 w-4 shrink-0" />
</div>
{
expand && (
<div className="relative px-2 pl-9">
<div className="absolute bottom-1 left-2 top-1 w-[1px] bg-divider-regular"></div>
{
payload.is_thought && (
<div className="body-sm-medium text-text-tertiary">{payload.tool_output}</div>
)
}
{
!payload.is_thought && (
<CodeEditor
readOnly
title={<div>{t('common.input', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={JSON.parse(payload.tool_arguments || '{}')}
isJSONStringifyBeauty
/>
)
}
{
!payload.is_thought && (
<CodeEditor
readOnly
className="mt-1"
title={<div>{t('common.output', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={{
answer: payload.tool_output,
}}
isJSONStringifyBeauty
/>
)
}
</div>
)
}
</div>
)
}
export default ToolCallsItem

View File

@ -64,6 +64,17 @@ export type CitationItem = {
word_count: number
}
export type ToolCallItem = {
is_thought?: boolean
tool_call_id?: string
tool_name?: string
tool_arguments?: string
tool_files?: string[]
tool_error?: string
tool_output?: string
tool_elapsed_time?: number
}
export type IChatItem = {
id: string
content: string
@ -104,6 +115,7 @@ export type IChatItem = {
siblingIndex?: number
prevSibling?: string
nextSibling?: string
toolCalls?: ToolCallItem[]
}
export type Metadata = {

View File

@ -270,9 +270,53 @@ export const useChat = (
handleRun(
bodyParams,
{
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
onData: (message: string, isFirstMessage: boolean, {
conversationId: newConversationId,
messageId,
taskId,
chunk_type,
tool_call_id,
tool_name,
tool_arguments,
tool_files,
tool_error,
tool_elapsed_time,
}: any) => {
responseItem.content = responseItem.content + message
if (chunk_type === 'tool_call') {
if (!responseItem.toolCalls)
responseItem.toolCalls = []
responseItem.toolCalls?.push({
tool_call_id,
tool_name,
tool_arguments,
tool_files,
tool_error,
tool_elapsed_time,
})
}
if (chunk_type === 'tool_result') {
const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.tool_call_id === tool_call_id) ?? -1
if (currentToolCallIndex > -1)
responseItem.toolCalls![currentToolCallIndex].tool_output = message
}
if (chunk_type === 'thought_start') {
responseItem.toolCalls?.push({
is_thought: true,
tool_elapsed_time,
})
}
if (chunk_type === 'thought') {
const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.is_thought) ?? -1
if (currentThoughtIndex > -1)
responseItem.toolCalls![currentThoughtIndex].tool_output = message
}
if (messageId && !hasSetResponseId) {
questionItem.id = `question-${messageId}`
responseItem.id = messageId

View File

@ -40,6 +40,13 @@ export type IOnDataMoreInfo = {
messageId: string
errorMessage?: string
errorCode?: string
chunk_type?: 'text' | 'tool_call' | 'tool_result' | 'thought' | 'thought_start'
tool_call_id?: string
tool_name?: string
tool_arguments?: string
tool_files?: string[]
tool_error?: string
tool_elapsed_time?: number
}
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
@ -234,6 +241,13 @@ export const handleStream = (
conversationId: bufferObj.conversation_id,
taskId: bufferObj.task_id,
messageId: bufferObj.id,
chunk_type: bufferObj.chunk_type,
tool_call_id: bufferObj.tool_call_id,
tool_name: bufferObj.tool_name,
tool_arguments: bufferObj.tool_arguments,
tool_files: bufferObj.tool_files,
tool_error: bufferObj.tool_error,
tool_elapsed_time: bufferObj.tool_elapsed_time,
})
isFirstMessage = false
}