Compare commits

...

15 Commits

Author SHA1 Message Date
zxhlyh
1db995be0d Merge branch 'main' into feat/llm-support-tools 2026-01-13 16:46:03 +08:00
zxhlyh
aa5e37f2db Merge branch 'main' into feat/llm-support-tools 2026-01-12 13:42:58 +08:00
zxhlyh
4d3d8b35d9 Merge branch 'main' into feat/llm-node-support-tools 2026-01-08 14:28:13 +08:00
zxhlyh
c323028179 feat: llm node support tools 2026-01-08 14:27:37 +08:00
zxhlyh
70149ea05e Merge branch 'main' into feat/llm-node-support-tools 2026-01-07 16:29:47 +08:00
zxhlyh
1d93f41fcf feat: llm node support tools 2026-01-07 16:28:41 +08:00
zxhlyh
04f40303fd Merge branch 'main' into feat/llm-node-support-tools 2026-01-04 18:04:42 +08:00
zxhlyh
ececc5ec2c feat: llm node support tools 2026-01-04 18:03:47 +08:00
zxhlyh
e83635ee5a Merge branch 'main' into feat/llm-node-support-tools 2025-12-30 11:47:54 +08:00
zxhlyh
d79372a46d Merge branch 'main' into feat/llm-node-support-tools 2025-12-30 11:47:26 +08:00
zxhlyh
bbd11c9e89 feat: llm node support tools 2025-12-30 10:40:01 +08:00
zxhlyh
d132abcdb4 merge main 2025-12-29 15:55:45 +08:00
zxhlyh
d60348572e feat: llm node support tools 2025-12-29 14:55:26 +08:00
zxhlyh
0cff94d90e Merge branch 'main' into feat/llm-node-support-tools 2025-12-25 13:45:49 +08:00
zxhlyh
a7859de625 feat: llm node support tools 2025-12-24 14:15:55 +08:00
36 changed files with 880 additions and 20 deletions

View File

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

View File

@@ -0,0 +1,23 @@
import type { ToolCallItem } from '@/types/workflow'
import ToolCallItemComponent from '@/app/components/workflow/run/llm-log/tool-call-item'
type ToolCallsProps = {
toolCalls: ToolCallItem[]
}
const ToolCalls = ({
toolCalls,
}: ToolCallsProps) => {
return (
<div className="my-1 space-y-1">
{toolCalls.map((toolCall: ToolCallItem, index: number) => (
<ToolCallItemComponent
key={index}
payload={toolCall}
className="bg-background-gradient-bg-fill-chat-bubble-bg-2 shadow-none"
/>
))}
</div>
)
}
export default ToolCalls

View File

@@ -45,7 +45,7 @@ const WorkflowProcessItem = ({
return ( return (
<div <div
className={cn( className={cn(
'-mx-1 rounded-xl px-2.5', 'rounded-xl px-2.5',
collapse ? 'border-l-[0.25px] border-components-panel-border py-[7px]' : 'border-[0.5px] border-components-panel-border-subtle px-1 pb-1 pt-[7px]', collapse ? 'border-l-[0.25px] border-components-panel-border py-[7px]' : 'border-[0.5px] border-components-panel-border-subtle px-1 pb-1 pt-[7px]',
running && !collapse && 'bg-background-section-burn', running && !collapse && 'bg-background-section-burn',
succeeded && !collapse && 'bg-state-success-hover', succeeded && !collapse && 'bg-state-success-hover',

View File

@@ -319,6 +319,9 @@ export const useChat = (
return player return player
} }
let toolCallId = ''
let thoughtId = ''
ssePost( ssePost(
url, url,
{ {
@@ -326,7 +329,19 @@ export const useChat = (
}, },
{ {
isPublicAPI, 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) { if (!isAgentMode) {
responseItem.content = responseItem.content + message responseItem.content = responseItem.content + message
} }
@@ -336,6 +351,57 @@ export const useChat = (
lastThought.thought = lastThought.thought + message // need immer setAutoFreeze 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) { if (messageId && !hasSetResponseId) {
questionItem.id = `question-${messageId}` questionItem.id = `question-${messageId}`
responseItem.id = messageId responseItem.id = messageId

View File

@@ -2,7 +2,7 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { InputVarType } from '@/app/components/workflow/types' import type { InputVarType } from '@/app/components/workflow/types'
import type { Annotation, MessageRating } from '@/models/log' import type { Annotation, MessageRating } from '@/models/log'
import type { FileResponse } from '@/types/workflow' import type { FileResponse, ToolCallItem } from '@/types/workflow'
export type MessageMore = { export type MessageMore = {
time: string time: string
@@ -104,6 +104,7 @@ export type IChatItem = {
siblingIndex?: number siblingIndex?: number
prevSibling?: string prevSibling?: string
nextSibling?: string nextSibling?: string
toolCalls?: ToolCallItem[]
} }
export type Metadata = { export type Metadata = {

View File

@@ -0,0 +1,4 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 9.49479C0.782372 8.51826 0 7.01768 0 5.33333C0 2.38782 2.38782 0 5.33333 0C8.20841 0 10.5503 2.27504 10.6608 5.12305L11.888 6.96354C12.0843 7.25794 12.0161 7.65424 11.7331 7.86654L10.6667 8.66602V10C10.6667 10.7364 10.0697 11.3333 9.33333 11.3333H8V13.3333H6.66667V10.6667C6.66667 10.2985 6.96514 10 7.33333 10H9.33333V8.33333C9.33333 8.12349 9.43239 7.92603 9.60026 7.80013L10.4284 7.17838L9.44531 5.70312C9.3723 5.59361 9.33333 5.46495 9.33333 5.33333C9.33333 3.1242 7.54248 1.33333 5.33333 1.33333C3.1242 1.33333 1.33333 3.1242 1.33333 5.33333C1.33333 6.69202 2.0103 7.89261 3.04818 8.61654C3.2269 8.74119 3.33329 8.94552 3.33333 9.16341V13.3333H2V9.49479Z" fill="#354052"/>
<path d="M6.04367 4.24012L5.6504 3.21778C5.59993 3.08657 5.47393 3 5.33333 3C5.19273 3 5.06673 3.08657 5.01627 3.21778L4.62303 4.24012C4.55531 4.41618 4.41618 4.55531 4.24012 4.62303L3.21778 5.01624C3.08657 5.0667 3 5.19276 3 5.33333C3 5.47393 3.08657 5.59993 3.21778 5.6504L4.24012 6.04367C4.41618 6.11133 4.55531 6.25047 4.62303 6.42653L5.01627 7.44887C5.06673 7.58007 5.19273 7.66667 5.33333 7.66667C5.47393 7.66667 5.59993 7.58007 5.6504 7.44887L6.04367 6.42653C6.11133 6.25047 6.25047 6.11133 6.42653 6.04367L7.44887 5.6504C7.58007 5.59993 7.66667 5.47393 7.66667 5.33333C7.66667 5.19276 7.58007 5.0667 7.44887 5.01624L6.42653 4.62303C6.25047 4.55531 6.11133 4.41618 6.04367 4.24012Z" fill="#354052"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,35 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "12",
"height": "14",
"viewBox": "0 0 12 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M2 9.49479C0.782372 8.51826 0 7.01768 0 5.33333C0 2.38782 2.38782 0 5.33333 0C8.20841 0 10.5503 2.27504 10.6608 5.12305L11.888 6.96354C12.0843 7.25794 12.0161 7.65424 11.7331 7.86654L10.6667 8.66602V10C10.6667 10.7364 10.0697 11.3333 9.33333 11.3333H8V13.3333H6.66667V10.6667C6.66667 10.2985 6.96514 10 7.33333 10H9.33333V8.33333C9.33333 8.12349 9.43239 7.92603 9.60026 7.80013L10.4284 7.17838L9.44531 5.70312C9.3723 5.59361 9.33333 5.46495 9.33333 5.33333C9.33333 3.1242 7.54248 1.33333 5.33333 1.33333C3.1242 1.33333 1.33333 3.1242 1.33333 5.33333C1.33333 6.69202 2.0103 7.89261 3.04818 8.61654C3.2269 8.74119 3.33329 8.94552 3.33333 9.16341V13.3333H2V9.49479Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M6.04367 4.24012L5.6504 3.21778C5.59993 3.08657 5.47393 3 5.33333 3C5.19273 3 5.06673 3.08657 5.01627 3.21778L4.62303 4.24012C4.55531 4.41618 4.41618 4.55531 4.24012 4.62303L3.21778 5.01624C3.08657 5.0667 3 5.19276 3 5.33333C3 5.47393 3.08657 5.59993 3.21778 5.6504L4.24012 6.04367C4.41618 6.11133 4.55531 6.25047 4.62303 6.42653L5.01627 7.44887C5.06673 7.58007 5.19273 7.66667 5.33333 7.66667C5.47393 7.66667 5.59993 7.58007 5.6504 7.44887L6.04367 6.42653C6.11133 6.25047 6.25047 6.11133 6.42653 6.04367L7.44887 5.6504C7.58007 5.59993 7.66667 5.47393 7.66667 5.33333C7.66667 5.19276 7.58007 5.0667 7.44887 5.01624L6.42653 4.62303C6.25047 4.55531 6.11133 4.41618 6.04367 4.24012Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "Thinking"
}

View File

@@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './Thinking.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Thinking'
export default Icon

View File

@@ -24,6 +24,7 @@ export { default as ParameterExtractor } from './ParameterExtractor'
export { default as QuestionClassifier } from './QuestionClassifier' export { default as QuestionClassifier } from './QuestionClassifier'
export { default as Schedule } from './Schedule' export { default as Schedule } from './Schedule'
export { default as TemplatingTransform } from './TemplatingTransform' export { default as TemplatingTransform } from './TemplatingTransform'
export { default as Thinking } from './Thinking'
export { default as TriggerAll } from './TriggerAll' export { default as TriggerAll } from './TriggerAll'
export { default as VariableX } from './VariableX' export { default as VariableX } from './VariableX'
export { default as WebhookLine } from './WebhookLine' export { default as WebhookLine } from './WebhookLine'

View File

@@ -2,6 +2,7 @@ import type { FC } from 'react'
import { import {
RiFileTextLine, RiFileTextLine,
RiFilmAiLine, RiFilmAiLine,
RiHammerLine,
RiImageCircleAiLine, RiImageCircleAiLine,
RiVoiceAiFill, RiVoiceAiFill,
} from '@remixicon/react' } from '@remixicon/react'
@@ -38,17 +39,33 @@ const FeatureIcon: FC<FeatureIconProps> = ({
// ) // )
// } // }
// if (feature === ModelFeatureEnum.toolCall) { if (feature === ModelFeatureEnum.toolCall) {
// return ( if (showFeaturesLabel) {
// <Tooltip return (
// popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.toolCall })} <ModelBadge className={cn('gap-x-0.5', className)}>
// > <RiHammerLine className="size-3" />
// <ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}> <span>{ModelFeatureTextEnum.toolCall}</span>
// <MagicWand className='w-3 h-3' /> </ModelBadge>
// </ModelBadge> )
// </Tooltip> }
// )
// } return (
<Tooltip
popupContent={t('modelProvider.featureSupported', { ns: 'common', feature: ModelFeatureTextEnum.toolCall })}
>
<div className="inline-block cursor-help">
<ModelBadge
className={cn(
'w-[18px] justify-center !px-0',
className,
)}
>
<RiHammerLine className="size-3" />
</ModelBadge>
</div>
</Tooltip>
)
}
// if (feature === ModelFeatureEnum.multiToolCall) { // if (feature === ModelFeatureEnum.multiToolCall) {
// return ( // return (

View File

@@ -96,6 +96,14 @@ const PopupItem: FC<PopupItemProps> = ({
<div className='text-text-tertiary system-xs-regular'>{currentProvider?.description?.[language] || currentProvider?.description?.en_US}</div> <div className='text-text-tertiary system-xs-regular'>{currentProvider?.description?.[language] || currentProvider?.description?.en_US}</div>
)} */} )} */}
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{
modelItem.features?.includes(ModelFeatureEnum.toolCall) && (
<FeatureIcon
feature={ModelFeatureEnum.toolCall}
showFeaturesLabel
/>
)
}
{modelItem.model_type && ( {modelItem.model_type && (
<ModelBadge> <ModelBadge>
{modelTypeFormat(modelItem.model_type)} {modelTypeFormat(modelItem.model_type)}
@@ -118,7 +126,7 @@ const PopupItem: FC<PopupItemProps> = ({
<div className="pt-2"> <div className="pt-2">
<div className="system-2xs-medium-uppercase mb-1 text-text-tertiary">{t('model.capabilities', { ns: 'common' })}</div> <div className="system-2xs-medium-uppercase mb-1 text-text-tertiary">{t('model.capabilities', { ns: 'common' })}</div>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{modelItem.features?.map(feature => ( {modelItem.features?.filter(feature => feature !== ModelFeatureEnum.toolCall).map(feature => (
<FeatureIcon <FeatureIcon
key={feature} key={feature}
feature={feature} feature={feature}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
import type { CommonNodeType } from '@/app/components/workflow/types' import type { CommonNodeType } from '@/app/components/workflow/types'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks' import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => { const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
@@ -18,3 +20,27 @@ const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
} }
export default useNodeCrud 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

@@ -0,0 +1,53 @@
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
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'
type ToolsProps = {
nodeId: string
tools?: ToolValue[]
maxIterations?: number
}
const Tools = ({
nodeId,
tools = [],
maxIterations = 10,
}: ToolsProps) => {
const { t } = useTranslation()
const {
handleToolsChange,
handleMaxIterationsChange,
} = useNodeTools(nodeId)
return (
<BoxGroup
boxProps={{
withBorderBottom: true,
withBorderTop: true,
}}
groupProps={{
className: 'px-0',
}}
>
<MultipleToolSelector
nodeId={nodeId}
nodeOutputVars={[]}
availableNodes={[]}
value={tools}
label={t(`nodes.llm.tools.title`, { ns: 'workflow' })}
tooltip={t(`nodes.llm.tools.title`, { ns: 'workflow' })}
onChange={handleToolsChange}
supportCollapse
/>
<MaxIterations
value={maxIterations}
onChange={handleMaxIterationsChange}
/>
</BoxGroup>
)
}
export default memo(Tools)

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 ConfigPrompt from './components/config-prompt'
import ReasoningFormatConfig from './components/reasoning-format-config' import ReasoningFormatConfig from './components/reasoning-format-config'
import StructureOutput from './components/structure-output' import StructureOutput from './components/structure-output'
import Tools from './components/tools'
import useConfig from './use-config' import useConfig from './use-config'
const i18nPrefix = 'nodes.llm' const i18nPrefix = '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 */} {/* Vision: GPT4-vision and so on */}
<ConfigVision <ConfigVision
nodeId={id} nodeId={id}
@@ -308,6 +315,11 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
type="object" type="object"
description={t(`${i18nPrefix}.outputVars.usage`, { ns: 'workflow' })} description={t(`${i18nPrefix}.outputVars.usage`, { ns: 'workflow' })}
/> />
<VarItem
name="generation"
type="object"
description={t(`${i18nPrefix}.outputVars.generation`, { ns: 'workflow' })}
/>
{inputs.structured_output_enabled && ( {inputs.structured_output_enabled && (
<> <>
<Split className="mt-3" /> <Split className="mt-3" />

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' 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 & { export type LLMNodeType = CommonNodeType & {
model: ModelConfig model: ModelConfig
prompt_template: PromptItem[] | PromptItem prompt_template: PromptItem[] | PromptItem
@@ -18,6 +31,8 @@ export type LLMNodeType = CommonNodeType & {
structured_output_enabled?: boolean structured_output_enabled?: boolean
structured_output?: StructuredOutput structured_output?: StructuredOutput
reasoning_format?: 'tagged' | 'separated' reasoning_format?: 'tagged' | 'separated'
tools?: ToolValue[]
max_iterations?: number
} }
export enum Type { export enum Type {

View File

@@ -15,6 +15,7 @@ import {
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { v4 as uuidV4 } from 'uuid'
import { import {
getProcessedInputs, getProcessedInputs,
processOpeningStatement, processOpeningStatement,
@@ -266,13 +267,78 @@ export const useChat = (
} }
let hasSetResponseId = false let hasSetResponseId = false
let toolCallId = ''
let thoughtId = ''
handleRun( handleRun(
bodyParams, bodyParams,
{ {
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) => {
responseItem.content = responseItem.content + message responseItem.content = responseItem.content + message
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) { if (messageId && !hasSetResponseId) {
questionItem.id = `question-${messageId}` questionItem.id = `question-${messageId}`
responseItem.id = messageId responseItem.id = messageId

View File

@@ -1,6 +1,7 @@
import type { import type {
AgentLogItemWithChildren, AgentLogItemWithChildren,
IterationDurationMap, IterationDurationMap,
LLMTraceItem,
LoopDurationMap, LoopDurationMap,
LoopVariableMap, LoopVariableMap,
NodeTracing, NodeTracing,
@@ -79,8 +80,18 @@ export const useLogs = () => {
} }
}, [setAgentOrToolLogItemStack, setAgentOrToolLogListMap]) }, [setAgentOrToolLogItemStack, setAgentOrToolLogListMap])
const [showLLMDetail, {
setTrue: setShowLLMDetailTrue,
setFalse: setShowLLMDetailFalse,
}] = useBoolean(false)
const [llmResultList, setLLMResultList] = useState<LLMTraceItem[]>([])
const handleShowLLMDetail = useCallback((detail: LLMTraceItem[]) => {
setShowLLMDetailTrue()
setLLMResultList(detail)
}, [setShowLLMDetailTrue, setLLMResultList])
return { return {
showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length, showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length || showLLMDetail,
showRetryDetail, showRetryDetail,
setShowRetryDetailTrue, setShowRetryDetailTrue,
setShowRetryDetailFalse, setShowRetryDetailFalse,
@@ -111,5 +122,12 @@ export const useLogs = () => {
agentOrToolLogItemStack, agentOrToolLogItemStack,
agentOrToolLogListMap, agentOrToolLogListMap,
handleShowAgentOrToolLog, handleShowAgentOrToolLog,
showLLMDetail,
setShowLLMDetailTrue,
setShowLLMDetailFalse,
llmResultList,
setLLMResultList,
handleShowLLMDetail,
} }
} }

View File

@@ -153,7 +153,7 @@ const RunPanel: FC<RunProps> = ({
</div> </div>
</div> </div>
{/* panel detail */} {/* panel detail */}
<div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-components-panel-bg')}> <div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-background-section')}>
{loading && ( {loading && (
<div className="flex h-full items-center justify-center bg-components-panel-bg"> <div className="flex h-full items-center justify-center bg-components-panel-bg">
<Loading /> <Loading />
@@ -192,7 +192,7 @@ const RunPanel: FC<RunProps> = ({
)} )}
{!loading && currentTab === 'TRACING' && ( {!loading && currentTab === 'TRACING' && (
<TracingPanel <TracingPanel
className="bg-background-section-burn" className="bg-background-section"
list={list} list={list}
/> />
)} )}

View File

@@ -0,0 +1,2 @@
export { default as LLMLogTrigger } from './llm-log-trigger'
export { default as LLMResultPanel } from './llm-result-panel'

View File

@@ -0,0 +1,41 @@
import type { LLMTraceItem, NodeTracing } from '@/types/workflow'
import {
RiArrowRightSLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Thinking } from '@/app/components/base/icons/src/vender/workflow'
type LLMLogTriggerProps = {
nodeInfo: NodeTracing
onShowLLMDetail: (detail: LLMTraceItem[]) => void
}
const LLMLogTrigger = ({
nodeInfo,
onShowLLMDetail,
}: LLMLogTriggerProps) => {
const { t } = useTranslation()
const llmTrace = nodeInfo?.execution_metadata?.llm_trace || []
const handleShowLLMDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowLLMDetail(llmTrace || [])
}
return (
<Button
className="mb-1 flex w-full items-center justify-between"
variant="tertiary"
onClick={handleShowLLMDetail}
>
<div className="flex items-center">
<Thinking className="mr-[5px] h-4 w-4 shrink-0 text-components-button-tertiary-text" />
{t('detail', { ns: 'runLog' })}
</div>
<RiArrowRightSLine className="h-4 w-4 shrink-0 text-components-button-tertiary-text" />
</Button>
)
}
export default LLMLogTrigger

View File

@@ -0,0 +1,73 @@
'use client'
import type { FC } from 'react'
import type {
LLMTraceItem,
ToolCallItem,
} from '@/types/workflow'
import {
RiArrowLeftLine,
} from '@remixicon/react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import ToolCallItemComponent from '@/app/components/workflow/run/llm-log/tool-call-item'
type Props = {
list: LLMTraceItem[]
onBack: () => void
}
const LLMResultPanel: FC<Props> = ({
list,
onBack,
}) => {
const { t } = useTranslation()
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>
<div
className="system-sm-medium flex h-8 cursor-pointer items-center bg-components-panel-bg px-4 text-text-accent-secondary"
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onBack()
}}
>
<RiArrowLeftLine className="mr-1 h-4 w-4" />
{t('singleRun.back', { ns: 'workflow' })}
</div>
<div className="space-y-1 p-2">
{
formattedList.map((item, index) => (
<ToolCallItemComponent key={index} payload={item as ToolCallItem} />
))
}
</div>
</div>
)
}
export default memo(LLMResultPanel)

View File

@@ -0,0 +1,152 @@
import type { ToolCallItem } from '@/types/workflow'
import {
RiArrowDownSLine,
} 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'
import { BlockEnum } from '@/app/components/workflow/types'
import { cn } from '@/utils/classnames'
type ToolCallItemComponentProps = {
className?: string
payload: ToolCallItem
}
const ToolCallItemComponent = ({
className,
payload,
}: ToolCallItemComponentProps) => {
const { t } = useTranslation()
const [expand, setExpand] = useState(false)
return (
<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)
}}
>
{
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.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>
)
}
<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.type === 'thought' && typeof payload.thoughtOutput === 'string' && (
<div className="body-sm-medium text-text-tertiary">{payload.thoughtOutput}</div>
)
}
{
payload.type === 'model' && (
<CodeEditor
readOnly
title={<div>{t('common.data', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.modelOutput}
isJSONStringifyBeauty
/>
)
}
{
payload.type === 'tool' && (
<CodeEditor
readOnly
title={<div>{t('common.input', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.toolArguments}
isJSONStringifyBeauty
/>
)
}
{
payload.type === 'tool' && (
<CodeEditor
readOnly
className="mt-1"
title={<div>{t('common.output', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.toolOutput}
isJSONStringifyBeauty
/>
)
}
</div>
)
}
</div>
)
}
export default ToolCallItemComponent

View File

@@ -3,6 +3,7 @@ import type { FC } from 'react'
import type { import type {
AgentLogItemWithChildren, AgentLogItemWithChildren,
IterationDurationMap, IterationDurationMap,
LLMTraceItem,
LoopDurationMap, LoopDurationMap,
LoopVariableMap, LoopVariableMap,
NodeTracing, NodeTracing,
@@ -29,6 +30,7 @@ import { BlockEnum } from '../types'
import LargeDataAlert from '../variable-inspect/large-data-alert' import LargeDataAlert from '../variable-inspect/large-data-alert'
import { AgentLogTrigger } from './agent-log' import { AgentLogTrigger } from './agent-log'
import { IterationLogTrigger } from './iteration-log' import { IterationLogTrigger } from './iteration-log'
import { LLMLogTrigger } from './llm-log'
import { LoopLogTrigger } from './loop-log' import { LoopLogTrigger } from './loop-log'
import { RetryLogTrigger } from './retry-log' import { RetryLogTrigger } from './retry-log'
@@ -43,6 +45,7 @@ type Props = {
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void onShowRetryDetail?: (detail: NodeTracing[]) => void
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
onShowLLMDetail?: (detail: LLMTraceItem[]) => void
notShowIterationNav?: boolean notShowIterationNav?: boolean
notShowLoopNav?: boolean notShowLoopNav?: boolean
} }
@@ -58,6 +61,7 @@ const NodePanel: FC<Props> = ({
onShowLoopDetail, onShowLoopDetail,
onShowRetryDetail, onShowRetryDetail,
onShowAgentOrToolLog, onShowAgentOrToolLog,
onShowLLMDetail,
notShowIterationNav, notShowIterationNav,
notShowLoopNav, notShowLoopNav,
}) => { }) => {
@@ -96,6 +100,7 @@ const NodePanel: FC<Props> = ({
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
const isLLMNode = nodeInfo.node_type === BlockEnum.LLM && !!nodeInfo.execution_metadata?.llm_trace?.length
const inputsTitle = useMemo(() => { const inputsTitle = useMemo(() => {
let text = t('common.input', { ns: 'workflow' }) let text = t('common.input', { ns: 'workflow' })
@@ -193,6 +198,12 @@ const NodePanel: FC<Props> = ({
onShowRetryResultList={onShowRetryDetail} onShowRetryResultList={onShowRetryDetail}
/> />
)} )}
{isLLMNode && onShowLLMDetail && (
<LLMLogTrigger
nodeInfo={nodeInfo}
onShowLLMDetail={onShowLLMDetail}
/>
)}
{ {
(isAgentNode || isToolNode) && onShowAgentOrToolLog && ( (isAgentNode || isToolNode) && onShowAgentOrToolLog && (
<AgentLogTrigger <AgentLogTrigger

View File

@@ -2,6 +2,7 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { import type {
AgentLogItemWithChildren, AgentLogItemWithChildren,
LLMTraceItem,
NodeTracing, NodeTracing,
} from '@/types/workflow' } from '@/types/workflow'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -15,6 +16,7 @@ import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
import { hasRetryNode } from '@/app/components/workflow/utils' import { hasRetryNode } from '@/app/components/workflow/utils'
import LargeDataAlert from '../variable-inspect/large-data-alert' import LargeDataAlert from '../variable-inspect/large-data-alert'
import { LLMLogTrigger } from './llm-log'
import MetaData from './meta' import MetaData from './meta'
import StatusPanel from './status' import StatusPanel from './status'
@@ -45,6 +47,7 @@ export type ResultPanelProps = {
handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void onShowRetryDetail?: (detail: NodeTracing[]) => void
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
onShowLLMDetail?: (detail: LLMTraceItem[]) => void
} }
const ResultPanel: FC<ResultPanelProps> = ({ const ResultPanel: FC<ResultPanelProps> = ({
@@ -71,6 +74,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
handleShowLoopResultList, handleShowLoopResultList,
onShowRetryDetail, onShowRetryDetail,
handleShowAgentOrToolLog, handleShowAgentOrToolLog,
onShowLLMDetail,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length
@@ -78,6 +82,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length
const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length
const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length
const isLLMNode = nodeInfo?.node_type === BlockEnum.LLM && !!nodeInfo?.execution_metadata?.llm_trace?.length
return ( return (
<div className="bg-components-panel-bg py-2"> <div className="bg-components-panel-bg py-2">
@@ -116,6 +121,14 @@ const ResultPanel: FC<ResultPanelProps> = ({
/> />
) )
} }
{
isLLMNode && onShowLLMDetail && (
<LLMLogTrigger
nodeInfo={nodeInfo}
onShowLLMDetail={onShowLLMDetail}
/>
)
}
{ {
(isAgentNode || isToolNode) && handleShowAgentOrToolLog && ( (isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
<AgentLogTrigger <AgentLogTrigger

View File

@@ -1,12 +1,14 @@
import type { import type {
AgentLogItemWithChildren, AgentLogItemWithChildren,
IterationDurationMap, IterationDurationMap,
LLMTraceItem,
LoopDurationMap, LoopDurationMap,
LoopVariableMap, LoopVariableMap,
NodeTracing, NodeTracing,
} from '@/types/workflow' } from '@/types/workflow'
import { AgentResultPanel } from './agent-log' import { AgentResultPanel } from './agent-log'
import { IterationResultPanel } from './iteration-log' import { IterationResultPanel } from './iteration-log'
import { LLMResultPanel } from './llm-log'
import { LoopResultPanel } from './loop-log' import { LoopResultPanel } from './loop-log'
import { RetryResultPanel } from './retry-log' import { RetryResultPanel } from './retry-log'
@@ -29,6 +31,10 @@ export type SpecialResultPanelProps = {
agentOrToolLogItemStack?: AgentLogItemWithChildren[] agentOrToolLogItemStack?: AgentLogItemWithChildren[]
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]> agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
showLLMDetail?: boolean
setShowLLMDetailFalse?: () => void
llmResultList?: LLMTraceItem[]
} }
const SpecialResultPanel = ({ const SpecialResultPanel = ({
showRetryDetail, showRetryDetail,
@@ -49,6 +55,10 @@ const SpecialResultPanel = ({
agentOrToolLogItemStack, agentOrToolLogItemStack,
agentOrToolLogListMap, agentOrToolLogListMap,
handleShowAgentOrToolLog, handleShowAgentOrToolLog,
showLLMDetail,
setShowLLMDetailFalse,
llmResultList,
}: SpecialResultPanelProps) => { }: SpecialResultPanelProps) => {
return ( return (
<div onClick={(e) => { <div onClick={(e) => {
@@ -64,6 +74,14 @@ const SpecialResultPanel = ({
/> />
) )
} }
{
!!showLLMDetail && !!llmResultList?.length && setShowLLMDetailFalse && (
<LLMResultPanel
list={llmResultList}
onBack={setShowLLMDetailFalse}
/>
)
}
{ {
showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && ( showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && (
<IterationResultPanel <IterationResultPanel

View File

@@ -91,6 +91,11 @@ const TracingPanel: FC<TracingPanelProps> = ({
agentOrToolLogItemStack, agentOrToolLogItemStack,
agentOrToolLogListMap, agentOrToolLogListMap,
handleShowAgentOrToolLog, handleShowAgentOrToolLog,
showLLMDetail,
setShowLLMDetailFalse,
llmResultList,
handleShowLLMDetail,
} = useLogs() } = useLogs()
const renderNode = (node: NodeTracing) => { const renderNode = (node: NodeTracing) => {
@@ -153,6 +158,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
onShowLoopDetail={handleShowLoopResultList} onShowLoopDetail={handleShowLoopResultList}
onShowRetryDetail={handleShowRetryResultList} onShowRetryDetail={handleShowRetryResultList}
onShowAgentOrToolLog={handleShowAgentOrToolLog} onShowAgentOrToolLog={handleShowAgentOrToolLog}
onShowLLMDetail={handleShowLLMDetail}
hideInfo={hideNodeInfo} hideInfo={hideNodeInfo}
hideProcessDetail={hideNodeProcessDetail} hideProcessDetail={hideNodeProcessDetail}
/> />
@@ -182,6 +188,10 @@ const TracingPanel: FC<TracingPanelProps> = ({
agentOrToolLogItemStack={agentOrToolLogItemStack} agentOrToolLogItemStack={agentOrToolLogItemStack}
agentOrToolLogListMap={agentOrToolLogListMap} agentOrToolLogListMap={agentOrToolLogListMap}
handleShowAgentOrToolLog={handleShowAgentOrToolLog} handleShowAgentOrToolLog={handleShowAgentOrToolLog}
showLLMDetail={showLLMDetail}
setShowLLMDetailFalse={setShowLLMDetailFalse}
llmResultList={llmResultList}
/> />
) )
} }

View File

@@ -126,6 +126,7 @@
"common.currentDraftUnpublished": "Current Draft Unpublished", "common.currentDraftUnpublished": "Current Draft Unpublished",
"common.currentView": "Current View", "common.currentView": "Current View",
"common.currentWorkflow": "Current Workflow", "common.currentWorkflow": "Current Workflow",
"common.data": "Data",
"common.debugAndPreview": "Preview", "common.debugAndPreview": "Preview",
"common.disconnect": "Disconnect", "common.disconnect": "Disconnect",
"common.duplicate": "Duplicate", "common.duplicate": "Duplicate",
@@ -650,6 +651,7 @@
"nodes.llm.jsonSchema.warningTips.saveSchema": "Please finish editing the current field before saving the schema", "nodes.llm.jsonSchema.warningTips.saveSchema": "Please finish editing the current field before saving the schema",
"nodes.llm.model": "model", "nodes.llm.model": "model",
"nodes.llm.notSetContextInPromptTip": "To enable the context feature, please fill in the context variable in PROMPT.", "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.output": "Generate content",
"nodes.llm.outputVars.reasoning_content": "Reasoning Content", "nodes.llm.outputVars.reasoning_content": "Reasoning Content",
"nodes.llm.outputVars.usage": "Model Usage Information", "nodes.llm.outputVars.usage": "Model Usage Information",
@@ -666,6 +668,7 @@
"nodes.llm.roleDescription.user": "Provide instructions, queries, or any text-based input to the model", "nodes.llm.roleDescription.user": "Provide instructions, queries, or any text-based input to the model",
"nodes.llm.singleRun.variable": "Variable", "nodes.llm.singleRun.variable": "Variable",
"nodes.llm.sysQueryInUser": "sys.query in user message is required", "nodes.llm.sysQueryInUser": "sys.query in user message is required",
"nodes.llm.tools.title": "Tools",
"nodes.llm.variables": "variables", "nodes.llm.variables": "variables",
"nodes.llm.vision": "vision", "nodes.llm.vision": "vision",
"nodes.loop.ErrorMethod.continueOnError": "Continue on Error", "nodes.loop.ErrorMethod.continueOnError": "Continue on Error",

View File

@@ -126,6 +126,7 @@
"common.currentDraftUnpublished": "当前草稿未发布", "common.currentDraftUnpublished": "当前草稿未发布",
"common.currentView": "当前视图", "common.currentView": "当前视图",
"common.currentWorkflow": "整个工作流", "common.currentWorkflow": "整个工作流",
"common.data": "数据",
"common.debugAndPreview": "预览", "common.debugAndPreview": "预览",
"common.disconnect": "断开连接", "common.disconnect": "断开连接",
"common.duplicate": "复制", "common.duplicate": "复制",
@@ -650,6 +651,7 @@
"nodes.llm.jsonSchema.warningTips.saveSchema": "请先完成当前字段的编辑", "nodes.llm.jsonSchema.warningTips.saveSchema": "请先完成当前字段的编辑",
"nodes.llm.model": "模型", "nodes.llm.model": "模型",
"nodes.llm.notSetContextInPromptTip": "要启用上下文功能,请在提示中填写上下文变量。", "nodes.llm.notSetContextInPromptTip": "要启用上下文功能,请在提示中填写上下文变量。",
"nodes.llm.outputVars.generation": "生成信息",
"nodes.llm.outputVars.output": "生成内容", "nodes.llm.outputVars.output": "生成内容",
"nodes.llm.outputVars.reasoning_content": "推理内容", "nodes.llm.outputVars.reasoning_content": "推理内容",
"nodes.llm.outputVars.usage": "模型用量信息", "nodes.llm.outputVars.usage": "模型用量信息",

View File

@@ -34,12 +34,27 @@ import { getWebAppPassport } from './webapp-auth'
const TIME_OUT = 100000 const TIME_OUT = 100000
export type IconObject = {
background: string
content: string
}
export type IOnDataMoreInfo = { export type IOnDataMoreInfo = {
conversationId?: string conversationId?: string
taskId?: string taskId?: string
messageId: string messageId: string
errorMessage?: string errorMessage?: string
errorCode?: string errorCode?: string
chunk_type?: 'text' | 'tool_call' | 'tool_result' | 'thought' | 'thought_start' | 'thought_end'
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_elapsed_time?: number
} }
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
@@ -245,6 +260,15 @@ export const handleStream = (
conversationId: bufferObj.conversation_id, conversationId: bufferObj.conversation_id,
taskId: bufferObj.task_id, taskId: bufferObj.task_id,
messageId: bufferObj.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_icon: bufferObj.tool_icon,
tool_icon_dark: bufferObj.tool_icon_dark,
tool_files: bufferObj.tool_files,
tool_error: bufferObj.tool_error,
tool_elapsed_time: bufferObj.tool_elapsed_time,
}) })
isFirstMessage = false isFirstMessage = false
} }

View File

@@ -28,6 +28,68 @@ export type AgentLogItemWithChildren = AgentLogItem & {
children: AgentLogItemWithChildren[] children: AgentLogItemWithChildren[]
} }
export type IconObject = {
background: string
content: string
}
export type ToolCallItem = {
id: string
type: 'model' | 'tool' | 'thought'
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 = {
id: string
name: string
arguments: string
output: string
files: string[]
error: string
elapsed_time?: number
status: string
}
export type SequenceSegment
= | { type: 'context', start: number, end: number }
| { type: 'reasoning', index: number }
| { type: 'tool_call', index: number }
export type LLMLogItem = {
reasoning_content: string[]
tool_calls: ToolCallDetail[]
sequence: SequenceSegment[]
}
export type LLMTraceItem = {
type: 'model' | 'tool'
duration: number
output: Record<string, any>
provider?: string
name: string
icon?: string | IconObject
icon_dark?: string | IconObject
error?: string
status?: 'success' | 'error'
}
export type NodeTracing = { export type NodeTracing = {
id: string id: string
index: number index: number
@@ -72,6 +134,7 @@ export type NodeTracing = {
icon?: string icon?: string
} }
loop_variable_map?: Record<string, any> loop_variable_map?: Record<string, any>
llm_trace?: LLMTraceItem[]
} }
metadata: { metadata: {
iterator_length: number iterator_length: number
@@ -104,6 +167,7 @@ export type NodeTracing = {
parent_parallel_id?: string parent_parallel_id?: string
parent_parallel_start_node_id?: string parent_parallel_start_node_id?: string
agentLog?: AgentLogItemWithChildren[] // agent log agentLog?: AgentLogItemWithChildren[] // agent log
generation_detail?: LLMLogItem
} }
export type FetchWorkflowDraftResponse = { export type FetchWorkflowDraftResponse = {