mirror of
https://github.com/langgenius/dify.git
synced 2026-03-01 21:15:10 +00:00
Compare commits
4 Commits
move-token
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d68ac4e3 | ||
|
|
4e48a1c4c3 | ||
|
|
fb9a6bbc9f | ||
|
|
a86765e2b6 |
@@ -18,7 +18,7 @@ import {
|
|||||||
useWorkflowReadOnly,
|
useWorkflowReadOnly,
|
||||||
} from '../hooks'
|
} from '../hooks'
|
||||||
import { useStore, useWorkflowStore } from '../store'
|
import { useStore, useWorkflowStore } from '../store'
|
||||||
import { BlockEnum, ControlMode } from '../types'
|
import { BlockEnum, ControlMode, WorkflowRunningStatus } from '../types'
|
||||||
import {
|
import {
|
||||||
getLayoutByDagre,
|
getLayoutByDagre,
|
||||||
getLayoutForChildNodes,
|
getLayoutForChildNodes,
|
||||||
@@ -36,12 +36,17 @@ export const useWorkflowInteractions = () => {
|
|||||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||||
|
|
||||||
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
||||||
|
const { workflowRunningData } = workflowStore.getState()
|
||||||
|
const runningStatus = workflowRunningData?.result?.status
|
||||||
|
const isActiveRun = runningStatus === WorkflowRunningStatus.Running || runningStatus === WorkflowRunningStatus.Waiting
|
||||||
workflowStore.setState({
|
workflowStore.setState({
|
||||||
showDebugAndPreviewPanel: false,
|
showDebugAndPreviewPanel: false,
|
||||||
workflowRunningData: undefined,
|
...(isActiveRun ? {} : { workflowRunningData: undefined }),
|
||||||
})
|
})
|
||||||
handleNodeCancelRunningStatus()
|
if (!isActiveRun) {
|
||||||
handleEdgeCancelRunningStatus()
|
handleNodeCancelRunningStatus()
|
||||||
|
handleEdgeCancelRunningStatus()
|
||||||
|
}
|
||||||
}, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
|
}, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { BlockEnum } from '../../types'
|
import { BlockEnum } from '../../types'
|
||||||
import ConversationVariableModal from './conversation-variable-modal'
|
import ConversationVariableModal from './conversation-variable-modal'
|
||||||
import Empty from './empty'
|
import Empty from './empty'
|
||||||
import { useChat } from './hooks'
|
import { useChat } from './hooks/use-chat'
|
||||||
import UserInput from './user-input'
|
import UserInput from './user-input'
|
||||||
|
|
||||||
type ChatWrapperProps = {
|
type ChatWrapperProps = {
|
||||||
|
|||||||
@@ -1,516 +0,0 @@
|
|||||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
|
||||||
import type {
|
|
||||||
ChatItem,
|
|
||||||
ChatItemInTree,
|
|
||||||
Inputs,
|
|
||||||
} from '@/app/components/base/chat/types'
|
|
||||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
|
||||||
import { uniqBy } from 'es-toolkit/compat'
|
|
||||||
import { produce, setAutoFreeze } from 'immer'
|
|
||||||
import {
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import {
|
|
||||||
getProcessedInputs,
|
|
||||||
processOpeningStatement,
|
|
||||||
} from '@/app/components/base/chat/chat/utils'
|
|
||||||
import { getThreadMessages } from '@/app/components/base/chat/utils'
|
|
||||||
import {
|
|
||||||
getProcessedFiles,
|
|
||||||
getProcessedFilesFromResponse,
|
|
||||||
} from '@/app/components/base/file-uploader/utils'
|
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
|
||||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
|
||||||
import { TransferMethod } from '@/types/app'
|
|
||||||
import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../constants'
|
|
||||||
import {
|
|
||||||
useSetWorkflowVarsWithValue,
|
|
||||||
useWorkflowRun,
|
|
||||||
} from '../../hooks'
|
|
||||||
import { useHooksStore } from '../../hooks-store'
|
|
||||||
import { useWorkflowStore } from '../../store'
|
|
||||||
import { NodeRunningStatus, WorkflowRunningStatus } from '../../types'
|
|
||||||
|
|
||||||
type GetAbortController = (abortController: AbortController) => void
|
|
||||||
type SendCallback = {
|
|
||||||
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
|
|
||||||
}
|
|
||||||
export const useChat = (
|
|
||||||
config: any,
|
|
||||||
formSettings?: {
|
|
||||||
inputs: Inputs
|
|
||||||
inputsForm: InputForm[]
|
|
||||||
},
|
|
||||||
prevChatTree?: ChatItemInTree[],
|
|
||||||
stopChat?: (taskId: string) => void,
|
|
||||||
) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { notify } = useToastContext()
|
|
||||||
const { handleRun } = useWorkflowRun()
|
|
||||||
const hasStopResponded = useRef(false)
|
|
||||||
const workflowStore = useWorkflowStore()
|
|
||||||
const conversationId = useRef('')
|
|
||||||
const taskIdRef = useRef('')
|
|
||||||
const [isResponding, setIsResponding] = useState(false)
|
|
||||||
const isRespondingRef = useRef(false)
|
|
||||||
const configsMap = useHooksStore(s => s.configsMap)
|
|
||||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
|
||||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
|
||||||
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
|
|
||||||
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
|
|
||||||
const {
|
|
||||||
setIterTimes,
|
|
||||||
setLoopTimes,
|
|
||||||
} = workflowStore.getState()
|
|
||||||
|
|
||||||
const handleResponding = useCallback((isResponding: boolean) => {
|
|
||||||
setIsResponding(isResponding)
|
|
||||||
isRespondingRef.current = isResponding
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [chatTree, setChatTree] = useState<ChatItemInTree[]>(prevChatTree || [])
|
|
||||||
const chatTreeRef = useRef<ChatItemInTree[]>(chatTree)
|
|
||||||
const [targetMessageId, setTargetMessageId] = useState<string>()
|
|
||||||
const threadMessages = useMemo(() => getThreadMessages(chatTree, targetMessageId), [chatTree, targetMessageId])
|
|
||||||
|
|
||||||
const getIntroduction = useCallback((str: string) => {
|
|
||||||
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
|
|
||||||
}, [formSettings?.inputs, formSettings?.inputsForm])
|
|
||||||
|
|
||||||
/** Final chat list that will be rendered */
|
|
||||||
const chatList = useMemo(() => {
|
|
||||||
const ret = [...threadMessages]
|
|
||||||
if (config?.opening_statement) {
|
|
||||||
const index = threadMessages.findIndex(item => item.isOpeningStatement)
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
ret[index] = {
|
|
||||||
...ret[index],
|
|
||||||
content: getIntroduction(config.opening_statement),
|
|
||||||
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ret.unshift({
|
|
||||||
id: `${Date.now()}`,
|
|
||||||
content: getIntroduction(config.opening_statement),
|
|
||||||
isAnswer: true,
|
|
||||||
isOpeningStatement: true,
|
|
||||||
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAutoFreeze(false)
|
|
||||||
return () => {
|
|
||||||
setAutoFreeze(true)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/** Find the target node by bfs and then operate on it */
|
|
||||||
const produceChatTreeNode = useCallback((targetId: string, operation: (node: ChatItemInTree) => void) => {
|
|
||||||
return produce(chatTreeRef.current, (draft) => {
|
|
||||||
const queue: ChatItemInTree[] = [...draft]
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const current = queue.shift()!
|
|
||||||
if (current.id === targetId) {
|
|
||||||
operation(current)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (current.children)
|
|
||||||
queue.push(...current.children)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleStop = useCallback(() => {
|
|
||||||
hasStopResponded.current = true
|
|
||||||
handleResponding(false)
|
|
||||||
if (stopChat && taskIdRef.current)
|
|
||||||
stopChat(taskIdRef.current)
|
|
||||||
setIterTimes(DEFAULT_ITER_TIMES)
|
|
||||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
|
||||||
if (suggestedQuestionsAbortControllerRef.current)
|
|
||||||
suggestedQuestionsAbortControllerRef.current.abort()
|
|
||||||
}, [handleResponding, setIterTimes, setLoopTimes, stopChat])
|
|
||||||
|
|
||||||
const handleRestart = useCallback(() => {
|
|
||||||
conversationId.current = ''
|
|
||||||
taskIdRef.current = ''
|
|
||||||
handleStop()
|
|
||||||
setIterTimes(DEFAULT_ITER_TIMES)
|
|
||||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
|
||||||
setChatTree([])
|
|
||||||
setSuggestQuestions([])
|
|
||||||
}, [
|
|
||||||
handleStop,
|
|
||||||
setIterTimes,
|
|
||||||
setLoopTimes,
|
|
||||||
])
|
|
||||||
|
|
||||||
const updateCurrentQAOnTree = useCallback(({
|
|
||||||
parentId,
|
|
||||||
responseItem,
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
}: {
|
|
||||||
parentId?: string
|
|
||||||
responseItem: ChatItem
|
|
||||||
placeholderQuestionId: string
|
|
||||||
questionItem: ChatItem
|
|
||||||
}) => {
|
|
||||||
let nextState: ChatItemInTree[]
|
|
||||||
const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] }
|
|
||||||
if (!parentId && !chatTree.some(item => [placeholderQuestionId, questionItem.id].includes(item.id))) {
|
|
||||||
// QA whose parent is not provided is considered as a first message of the conversation,
|
|
||||||
// and it should be a root node of the chat tree
|
|
||||||
nextState = produce(chatTree, (draft) => {
|
|
||||||
draft.push(currentQA)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// find the target QA in the tree and update it; if not found, insert it to its parent node
|
|
||||||
nextState = produceChatTreeNode(parentId!, (parentNode) => {
|
|
||||||
const questionNodeIndex = parentNode.children!.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
|
||||||
if (questionNodeIndex === -1)
|
|
||||||
parentNode.children!.push(currentQA)
|
|
||||||
else
|
|
||||||
parentNode.children![questionNodeIndex] = currentQA
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setChatTree(nextState)
|
|
||||||
chatTreeRef.current = nextState
|
|
||||||
}, [chatTree, produceChatTreeNode])
|
|
||||||
|
|
||||||
const handleSend = useCallback((
|
|
||||||
params: {
|
|
||||||
query: string
|
|
||||||
files?: FileEntity[]
|
|
||||||
parent_message_id?: string
|
|
||||||
[key: string]: any
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onGetSuggestedQuestions,
|
|
||||||
}: SendCallback,
|
|
||||||
) => {
|
|
||||||
if (isRespondingRef.current) {
|
|
||||||
notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
|
|
||||||
|
|
||||||
const placeholderQuestionId = `question-${Date.now()}`
|
|
||||||
const questionItem = {
|
|
||||||
id: placeholderQuestionId,
|
|
||||||
content: params.query,
|
|
||||||
isAnswer: false,
|
|
||||||
message_files: params.files,
|
|
||||||
parentMessageId: params.parent_message_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
|
||||||
const placeholderAnswerItem = {
|
|
||||||
id: placeholderAnswerId,
|
|
||||||
content: '',
|
|
||||||
isAnswer: true,
|
|
||||||
parentMessageId: questionItem.id,
|
|
||||||
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
|
|
||||||
}
|
|
||||||
|
|
||||||
setTargetMessageId(parentMessage?.id)
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
responseItem: placeholderAnswerItem,
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
})
|
|
||||||
|
|
||||||
// answer
|
|
||||||
const responseItem: ChatItem = {
|
|
||||||
id: placeholderAnswerId,
|
|
||||||
content: '',
|
|
||||||
agent_thoughts: [],
|
|
||||||
message_files: [],
|
|
||||||
isAnswer: true,
|
|
||||||
parentMessageId: questionItem.id,
|
|
||||||
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResponding(true)
|
|
||||||
|
|
||||||
const { files, inputs, ...restParams } = params
|
|
||||||
const bodyParams = {
|
|
||||||
files: getProcessedFiles(files || []),
|
|
||||||
inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []),
|
|
||||||
...restParams,
|
|
||||||
}
|
|
||||||
if (bodyParams?.files?.length) {
|
|
||||||
bodyParams.files = bodyParams.files.map((item) => {
|
|
||||||
if (item.transfer_method === TransferMethod.local_file) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
url: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasSetResponseId = false
|
|
||||||
|
|
||||||
handleRun(
|
|
||||||
bodyParams,
|
|
||||||
{
|
|
||||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
|
||||||
responseItem.content = responseItem.content + message
|
|
||||||
|
|
||||||
if (messageId && !hasSetResponseId) {
|
|
||||||
questionItem.id = `question-${messageId}`
|
|
||||||
responseItem.id = messageId
|
|
||||||
responseItem.parentMessageId = questionItem.id
|
|
||||||
hasSetResponseId = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFirstMessage && newConversationId)
|
|
||||||
conversationId.current = newConversationId
|
|
||||||
|
|
||||||
taskIdRef.current = taskId
|
|
||||||
if (messageId)
|
|
||||||
responseItem.id = messageId
|
|
||||||
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async onCompleted(hasError?: boolean, errorMessage?: string) {
|
|
||||||
handleResponding(false)
|
|
||||||
fetchInspectVars({})
|
|
||||||
invalidAllLastRun()
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
if (errorMessage) {
|
|
||||||
responseItem.content = errorMessage
|
|
||||||
responseItem.isError = true
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
|
|
||||||
try {
|
|
||||||
const { data }: any = await onGetSuggestedQuestions(
|
|
||||||
responseItem.id,
|
|
||||||
newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
|
|
||||||
)
|
|
||||||
setSuggestQuestions(data)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
||||||
catch (error) {
|
|
||||||
setSuggestQuestions([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMessageEnd: (messageEnd) => {
|
|
||||||
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
|
||||||
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
|
|
||||||
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
|
|
||||||
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onMessageReplace: (messageReplace) => {
|
|
||||||
responseItem.content = messageReplace.answer
|
|
||||||
},
|
|
||||||
onError() {
|
|
||||||
handleResponding(false)
|
|
||||||
},
|
|
||||||
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
|
|
||||||
taskIdRef.current = task_id
|
|
||||||
responseItem.workflow_run_id = workflow_run_id
|
|
||||||
responseItem.workflowProcess = {
|
|
||||||
status: WorkflowRunningStatus.Running,
|
|
||||||
tracing: [],
|
|
||||||
}
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onWorkflowFinished: ({ data }) => {
|
|
||||||
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onIterationStart: ({ data }) => {
|
|
||||||
responseItem.workflowProcess!.tracing!.push({
|
|
||||||
...data,
|
|
||||||
status: NodeRunningStatus.Running,
|
|
||||||
})
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onIterationFinish: ({ data }) => {
|
|
||||||
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
|
||||||
if (currentTracingIndex > -1) {
|
|
||||||
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
|
||||||
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
|
||||||
...data,
|
|
||||||
}
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLoopStart: ({ data }) => {
|
|
||||||
responseItem.workflowProcess!.tracing!.push({
|
|
||||||
...data,
|
|
||||||
status: NodeRunningStatus.Running,
|
|
||||||
})
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onLoopFinish: ({ data }) => {
|
|
||||||
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
|
||||||
if (currentTracingIndex > -1) {
|
|
||||||
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
|
||||||
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
|
||||||
...data,
|
|
||||||
}
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onNodeStarted: ({ data }) => {
|
|
||||||
responseItem.workflowProcess!.tracing!.push({
|
|
||||||
...data,
|
|
||||||
status: NodeRunningStatus.Running,
|
|
||||||
} as any)
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onNodeRetry: ({ data }) => {
|
|
||||||
responseItem.workflowProcess!.tracing!.push(data)
|
|
||||||
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onNodeFinished: ({ data }) => {
|
|
||||||
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
|
||||||
if (currentTracingIndex > -1) {
|
|
||||||
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
|
||||||
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
|
||||||
...data,
|
|
||||||
}
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onAgentLog: ({ data }) => {
|
|
||||||
const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
|
||||||
if (currentNodeIndex > -1) {
|
|
||||||
const current = responseItem.workflowProcess!.tracing![currentNodeIndex]
|
|
||||||
|
|
||||||
if (current.execution_metadata) {
|
|
||||||
if (current.execution_metadata.agent_log) {
|
|
||||||
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.message_id === data.message_id)
|
|
||||||
if (currentLogIndex > -1) {
|
|
||||||
current.execution_metadata.agent_log[currentLogIndex] = {
|
|
||||||
...current.execution_metadata.agent_log[currentLogIndex],
|
|
||||||
...data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
current.execution_metadata.agent_log.push(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
current.execution_metadata.agent_log = [data]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
current.execution_metadata = {
|
|
||||||
agent_log: [data],
|
|
||||||
} as any
|
|
||||||
}
|
|
||||||
|
|
||||||
responseItem.workflowProcess!.tracing[currentNodeIndex] = {
|
|
||||||
...current,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentQAOnTree({
|
|
||||||
placeholderQuestionId,
|
|
||||||
questionItem,
|
|
||||||
responseItem,
|
|
||||||
parentId: params.parent_message_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, config?.suggested_questions_after_answer?.enabled, fetchInspectVars, invalidAllLastRun])
|
|
||||||
|
|
||||||
return {
|
|
||||||
conversationId: conversationId.current,
|
|
||||||
chatList,
|
|
||||||
setTargetMessageId,
|
|
||||||
handleSend,
|
|
||||||
handleStop,
|
|
||||||
handleRestart,
|
|
||||||
isResponding,
|
|
||||||
suggestedQuestions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { ChatItem, ChatItemInTree } from '@/app/components/base/chat/types'
|
||||||
|
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||||
|
|
||||||
|
export type ChatConfig = {
|
||||||
|
opening_statement?: string
|
||||||
|
suggested_questions?: string[]
|
||||||
|
suggested_questions_after_answer?: {
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
text_to_speech?: unknown
|
||||||
|
speech_to_text?: unknown
|
||||||
|
retriever_resource?: unknown
|
||||||
|
sensitive_word_avoidance?: unknown
|
||||||
|
file_upload?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetAbortController = (abortController: AbortController) => void
|
||||||
|
|
||||||
|
export type SendCallback = {
|
||||||
|
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SendParams = {
|
||||||
|
query: string
|
||||||
|
files?: FileEntity[]
|
||||||
|
parent_message_id?: string
|
||||||
|
inputs?: Record<string, unknown>
|
||||||
|
conversation_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateCurrentQAParams = {
|
||||||
|
parentId?: string
|
||||||
|
responseItem: ChatItem
|
||||||
|
placeholderQuestionId: string
|
||||||
|
questionItem: ChatItem
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatTreeUpdater = (updater: (chatTree: ChatItemInTree[]) => ChatItemInTree[]) => void
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../../constants'
|
||||||
|
import { useEdgesInteractionsWithoutSync } from '../../../hooks/use-edges-interactions-without-sync'
|
||||||
|
import { useNodesInteractionsWithoutSync } from '../../../hooks/use-nodes-interactions-without-sync'
|
||||||
|
import { useStore, useWorkflowStore } from '../../../store'
|
||||||
|
import { WorkflowRunningStatus } from '../../../types'
|
||||||
|
|
||||||
|
type UseChatFlowControlParams = {
|
||||||
|
stopChat?: (taskId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatFlowControl({
|
||||||
|
stopChat,
|
||||||
|
}: UseChatFlowControlParams) {
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const setIsResponding = useStore(s => s.setIsResponding)
|
||||||
|
const resetChatPreview = useStore(s => s.resetChatPreview)
|
||||||
|
const setActiveTaskId = useStore(s => s.setActiveTaskId)
|
||||||
|
const setHasStopResponded = useStore(s => s.setHasStopResponded)
|
||||||
|
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
|
||||||
|
const invalidateRun = useStore(s => s.invalidateRun)
|
||||||
|
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
|
||||||
|
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||||
|
|
||||||
|
const { setIterTimes, setLoopTimes } = workflowStore.getState()
|
||||||
|
|
||||||
|
const handleResponding = useCallback((responding: boolean) => {
|
||||||
|
setIsResponding(responding)
|
||||||
|
}, [setIsResponding])
|
||||||
|
|
||||||
|
const handleStop = useCallback(() => {
|
||||||
|
const {
|
||||||
|
activeTaskId,
|
||||||
|
suggestedQuestionsAbortController,
|
||||||
|
workflowRunningData,
|
||||||
|
setWorkflowRunningData,
|
||||||
|
} = workflowStore.getState()
|
||||||
|
const runningStatus = workflowRunningData?.result?.status
|
||||||
|
const isActiveRun = runningStatus === WorkflowRunningStatus.Running || runningStatus === WorkflowRunningStatus.Waiting
|
||||||
|
setHasStopResponded(true)
|
||||||
|
handleResponding(false)
|
||||||
|
if (stopChat && activeTaskId)
|
||||||
|
stopChat(activeTaskId)
|
||||||
|
setIterTimes(DEFAULT_ITER_TIMES)
|
||||||
|
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||||
|
if (suggestedQuestionsAbortController)
|
||||||
|
suggestedQuestionsAbortController.abort()
|
||||||
|
setSuggestedQuestionsAbortController(null)
|
||||||
|
setActiveTaskId('')
|
||||||
|
invalidateRun()
|
||||||
|
if (isActiveRun && workflowRunningData) {
|
||||||
|
setWorkflowRunningData({
|
||||||
|
...workflowRunningData,
|
||||||
|
result: {
|
||||||
|
...workflowRunningData.result,
|
||||||
|
status: WorkflowRunningStatus.Stopped,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (isActiveRun) {
|
||||||
|
handleNodeCancelRunningStatus()
|
||||||
|
handleEdgeCancelRunningStatus()
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
handleResponding,
|
||||||
|
setIterTimes,
|
||||||
|
setLoopTimes,
|
||||||
|
stopChat,
|
||||||
|
workflowStore,
|
||||||
|
setHasStopResponded,
|
||||||
|
setSuggestedQuestionsAbortController,
|
||||||
|
setActiveTaskId,
|
||||||
|
invalidateRun,
|
||||||
|
handleNodeCancelRunningStatus,
|
||||||
|
handleEdgeCancelRunningStatus,
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleRestart = useCallback(() => {
|
||||||
|
handleStop()
|
||||||
|
resetChatPreview()
|
||||||
|
setIterTimes(DEFAULT_ITER_TIMES)
|
||||||
|
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||||
|
}, [handleStop, setIterTimes, setLoopTimes, resetChatPreview])
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleResponding,
|
||||||
|
handleStop,
|
||||||
|
handleRestart,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||||
|
import type { ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
import { processOpeningStatement } from '@/app/components/base/chat/chat/utils'
|
||||||
|
import { getThreadMessages } from '@/app/components/base/chat/utils'
|
||||||
|
|
||||||
|
type UseChatListParams = {
|
||||||
|
chatTree: ChatItemInTree[]
|
||||||
|
targetMessageId: string | undefined
|
||||||
|
config: {
|
||||||
|
opening_statement?: string
|
||||||
|
suggested_questions?: string[]
|
||||||
|
} | undefined
|
||||||
|
formSettings?: {
|
||||||
|
inputs: Inputs
|
||||||
|
inputsForm: InputForm[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatList({
|
||||||
|
chatTree,
|
||||||
|
targetMessageId,
|
||||||
|
config,
|
||||||
|
formSettings,
|
||||||
|
}: UseChatListParams) {
|
||||||
|
const threadMessages = useMemo(
|
||||||
|
() => getThreadMessages(chatTree, targetMessageId),
|
||||||
|
[chatTree, targetMessageId],
|
||||||
|
)
|
||||||
|
|
||||||
|
const getIntroduction = useCallback((str: string) => {
|
||||||
|
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
|
||||||
|
}, [formSettings?.inputs, formSettings?.inputsForm])
|
||||||
|
|
||||||
|
const chatList = useMemo(() => {
|
||||||
|
const ret = [...threadMessages]
|
||||||
|
if (config?.opening_statement) {
|
||||||
|
const index = threadMessages.findIndex(item => item.isOpeningStatement)
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
ret[index] = {
|
||||||
|
...ret[index],
|
||||||
|
content: getIntroduction(config.opening_statement),
|
||||||
|
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret.unshift({
|
||||||
|
id: `${Date.now()}`,
|
||||||
|
content: getIntroduction(config.opening_statement),
|
||||||
|
isAnswer: true,
|
||||||
|
isOpeningStatement: true,
|
||||||
|
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
|
||||||
|
|
||||||
|
return {
|
||||||
|
threadMessages,
|
||||||
|
chatList,
|
||||||
|
getIntroduction,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
import type { SendCallback, SendParams, UpdateCurrentQAParams } from './types'
|
||||||
|
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||||
|
import type { ChatItem, ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||||
|
import { uniqBy } from 'es-toolkit/compat'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getProcessedInputs } from '@/app/components/base/chat/chat/utils'
|
||||||
|
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||||
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
|
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||||
|
import { TransferMethod } from '@/types/app'
|
||||||
|
import { useSetWorkflowVarsWithValue, useWorkflowRun } from '../../../hooks'
|
||||||
|
import { useHooksStore } from '../../../hooks-store'
|
||||||
|
import { useStore, useWorkflowStore } from '../../../store'
|
||||||
|
import { createWorkflowEventHandlers } from './use-workflow-event-handlers'
|
||||||
|
|
||||||
|
type UseChatMessageSenderParams = {
|
||||||
|
threadMessages: ChatItemInTree[]
|
||||||
|
config?: {
|
||||||
|
suggested_questions_after_answer?: {
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formSettings?: {
|
||||||
|
inputs: Inputs
|
||||||
|
inputsForm: InputForm[]
|
||||||
|
}
|
||||||
|
handleResponding: (responding: boolean) => void
|
||||||
|
updateCurrentQAOnTree: (params: UpdateCurrentQAParams) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatMessageSender({
|
||||||
|
threadMessages,
|
||||||
|
config,
|
||||||
|
formSettings,
|
||||||
|
handleResponding,
|
||||||
|
updateCurrentQAOnTree,
|
||||||
|
}: UseChatMessageSenderParams) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useToastContext()
|
||||||
|
const { handleRun } = useWorkflowRun()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
|
||||||
|
const configsMap = useHooksStore(s => s.configsMap)
|
||||||
|
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||||
|
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
||||||
|
const setConversationId = useStore(s => s.setConversationId)
|
||||||
|
const setTargetMessageId = useStore(s => s.setTargetMessageId)
|
||||||
|
const setSuggestedQuestions = useStore(s => s.setSuggestedQuestions)
|
||||||
|
const setActiveTaskId = useStore(s => s.setActiveTaskId)
|
||||||
|
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
|
||||||
|
const startRun = useStore(s => s.startRun)
|
||||||
|
|
||||||
|
const handleSend = useCallback((
|
||||||
|
params: SendParams,
|
||||||
|
{ onGetSuggestedQuestions }: SendCallback,
|
||||||
|
) => {
|
||||||
|
if (workflowStore.getState().isResponding) {
|
||||||
|
notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { suggestedQuestionsAbortController } = workflowStore.getState()
|
||||||
|
if (suggestedQuestionsAbortController)
|
||||||
|
suggestedQuestionsAbortController.abort()
|
||||||
|
setSuggestedQuestionsAbortController(null)
|
||||||
|
|
||||||
|
const runId = startRun()
|
||||||
|
const isCurrentRun = () => runId === workflowStore.getState().activeRunId
|
||||||
|
|
||||||
|
const parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
|
||||||
|
|
||||||
|
const placeholderQuestionId = `question-${Date.now()}`
|
||||||
|
const questionItem: ChatItem = {
|
||||||
|
id: placeholderQuestionId,
|
||||||
|
content: params.query,
|
||||||
|
isAnswer: false,
|
||||||
|
message_files: params.files,
|
||||||
|
parentMessageId: params.parent_message_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
const siblingIndex = parentMessage?.children?.length ?? workflowStore.getState().chatTree.length
|
||||||
|
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||||
|
const placeholderAnswerItem: ChatItem = {
|
||||||
|
id: placeholderAnswerId,
|
||||||
|
content: '',
|
||||||
|
isAnswer: true,
|
||||||
|
parentMessageId: questionItem.id,
|
||||||
|
siblingIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
setTargetMessageId(parentMessage?.id)
|
||||||
|
updateCurrentQAOnTree({
|
||||||
|
parentId: params.parent_message_id,
|
||||||
|
responseItem: placeholderAnswerItem,
|
||||||
|
placeholderQuestionId,
|
||||||
|
questionItem,
|
||||||
|
})
|
||||||
|
|
||||||
|
const responseItem: ChatItem = {
|
||||||
|
id: placeholderAnswerId,
|
||||||
|
content: '',
|
||||||
|
agent_thoughts: [],
|
||||||
|
message_files: [],
|
||||||
|
isAnswer: true,
|
||||||
|
parentMessageId: questionItem.id,
|
||||||
|
siblingIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResponding(true)
|
||||||
|
|
||||||
|
const { files, inputs, ...restParams } = params
|
||||||
|
const bodyParams = {
|
||||||
|
files: getProcessedFiles(files || []),
|
||||||
|
inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []),
|
||||||
|
...restParams,
|
||||||
|
}
|
||||||
|
if (bodyParams?.files?.length) {
|
||||||
|
bodyParams.files = bodyParams.files.map((item) => {
|
||||||
|
if (item.transfer_method === TransferMethod.local_file) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
url: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasSetResponseId = false
|
||||||
|
|
||||||
|
const workflowHandlers = createWorkflowEventHandlers({
|
||||||
|
responseItem,
|
||||||
|
questionItem,
|
||||||
|
placeholderQuestionId,
|
||||||
|
parentMessageId: params.parent_message_id,
|
||||||
|
updateCurrentQAOnTree,
|
||||||
|
})
|
||||||
|
|
||||||
|
handleRun(
|
||||||
|
bodyParams,
|
||||||
|
{
|
||||||
|
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
responseItem.content = responseItem.content + message
|
||||||
|
|
||||||
|
if (messageId && !hasSetResponseId) {
|
||||||
|
questionItem.id = `question-${messageId}`
|
||||||
|
responseItem.id = messageId
|
||||||
|
responseItem.parentMessageId = questionItem.id
|
||||||
|
hasSetResponseId = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstMessage && newConversationId)
|
||||||
|
setConversationId(newConversationId)
|
||||||
|
|
||||||
|
if (taskId)
|
||||||
|
setActiveTaskId(taskId)
|
||||||
|
if (messageId)
|
||||||
|
responseItem.id = messageId
|
||||||
|
|
||||||
|
updateCurrentQAOnTree({
|
||||||
|
placeholderQuestionId,
|
||||||
|
questionItem,
|
||||||
|
responseItem,
|
||||||
|
parentId: params.parent_message_id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async onCompleted(hasError?: boolean, errorMessage?: string) {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
handleResponding(false)
|
||||||
|
fetchInspectVars({})
|
||||||
|
invalidAllLastRun()
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
if (errorMessage) {
|
||||||
|
responseItem.content = errorMessage
|
||||||
|
responseItem.isError = true
|
||||||
|
updateCurrentQAOnTree({
|
||||||
|
placeholderQuestionId,
|
||||||
|
questionItem,
|
||||||
|
responseItem,
|
||||||
|
parentId: params.parent_message_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config?.suggested_questions_after_answer?.enabled && !workflowStore.getState().hasStopResponded && onGetSuggestedQuestions) {
|
||||||
|
try {
|
||||||
|
const result = await onGetSuggestedQuestions(
|
||||||
|
responseItem.id,
|
||||||
|
newAbortController => setSuggestedQuestionsAbortController(newAbortController),
|
||||||
|
) as { data: string[] }
|
||||||
|
setSuggestedQuestions(result.data)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
setSuggestedQuestions([])
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setSuggestedQuestionsAbortController(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMessageEnd: (messageEnd) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
||||||
|
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
|
||||||
|
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
|
||||||
|
|
||||||
|
updateCurrentQAOnTree({
|
||||||
|
placeholderQuestionId,
|
||||||
|
questionItem,
|
||||||
|
responseItem,
|
||||||
|
parentId: params.parent_message_id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onMessageReplace: (messageReplace) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
responseItem.content = messageReplace.answer
|
||||||
|
},
|
||||||
|
onError() {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
handleResponding(false)
|
||||||
|
},
|
||||||
|
onWorkflowStarted: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
const taskId = workflowHandlers.onWorkflowStarted(event)
|
||||||
|
if (taskId)
|
||||||
|
setActiveTaskId(taskId)
|
||||||
|
},
|
||||||
|
onWorkflowFinished: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onWorkflowFinished(event)
|
||||||
|
},
|
||||||
|
onIterationStart: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onIterationStart(event)
|
||||||
|
},
|
||||||
|
onIterationFinish: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onIterationFinish(event)
|
||||||
|
},
|
||||||
|
onLoopStart: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onLoopStart(event)
|
||||||
|
},
|
||||||
|
onLoopFinish: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onLoopFinish(event)
|
||||||
|
},
|
||||||
|
onNodeStarted: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onNodeStarted(event)
|
||||||
|
},
|
||||||
|
onNodeRetry: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onNodeRetry(event)
|
||||||
|
},
|
||||||
|
onNodeFinished: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onNodeFinished(event)
|
||||||
|
},
|
||||||
|
onAgentLog: (event) => {
|
||||||
|
if (!isCurrentRun())
|
||||||
|
return
|
||||||
|
workflowHandlers.onAgentLog(event)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}, [
|
||||||
|
threadMessages,
|
||||||
|
updateCurrentQAOnTree,
|
||||||
|
handleResponding,
|
||||||
|
formSettings?.inputsForm,
|
||||||
|
handleRun,
|
||||||
|
notify,
|
||||||
|
t,
|
||||||
|
config?.suggested_questions_after_answer?.enabled,
|
||||||
|
setTargetMessageId,
|
||||||
|
setConversationId,
|
||||||
|
setSuggestedQuestions,
|
||||||
|
setActiveTaskId,
|
||||||
|
setSuggestedQuestionsAbortController,
|
||||||
|
startRun,
|
||||||
|
fetchInspectVars,
|
||||||
|
invalidAllLastRun,
|
||||||
|
workflowStore,
|
||||||
|
])
|
||||||
|
|
||||||
|
return { handleSend }
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { ChatTreeUpdater, UpdateCurrentQAParams } from './types'
|
||||||
|
import type { ChatItemInTree } from '@/app/components/base/chat/types'
|
||||||
|
import { produce } from 'immer'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
export function useChatTreeOperations(updateChatTree: ChatTreeUpdater) {
|
||||||
|
const produceChatTreeNode = useCallback(
|
||||||
|
(tree: ChatItemInTree[], targetId: string, operation: (node: ChatItemInTree) => void) => {
|
||||||
|
return produce(tree, (draft) => {
|
||||||
|
const queue: ChatItemInTree[] = [...draft]
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!
|
||||||
|
if (current.id === targetId) {
|
||||||
|
operation(current)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (current.children)
|
||||||
|
queue.push(...current.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateCurrentQAOnTree = useCallback(({
|
||||||
|
parentId,
|
||||||
|
responseItem,
|
||||||
|
placeholderQuestionId,
|
||||||
|
questionItem,
|
||||||
|
}: UpdateCurrentQAParams) => {
|
||||||
|
const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] } as ChatItemInTree
|
||||||
|
updateChatTree((currentChatTree) => {
|
||||||
|
if (!parentId) {
|
||||||
|
const questionIndex = currentChatTree.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
||||||
|
return produce(currentChatTree, (draft) => {
|
||||||
|
if (questionIndex === -1)
|
||||||
|
draft.push(currentQA)
|
||||||
|
else
|
||||||
|
draft[questionIndex] = currentQA
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return produceChatTreeNode(currentChatTree, parentId, (parentNode) => {
|
||||||
|
if (!parentNode.children)
|
||||||
|
parentNode.children = []
|
||||||
|
const questionNodeIndex = parentNode.children.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
||||||
|
if (questionNodeIndex === -1)
|
||||||
|
parentNode.children.push(currentQA)
|
||||||
|
else
|
||||||
|
parentNode.children[questionNodeIndex] = currentQA
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [produceChatTreeNode, updateChatTree])
|
||||||
|
|
||||||
|
return {
|
||||||
|
produceChatTreeNode,
|
||||||
|
updateCurrentQAOnTree,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import type { ChatConfig } from './types'
|
||||||
|
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||||
|
import type { ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { useStore } from '../../../store'
|
||||||
|
import { useChatFlowControl } from './use-chat-flow-control'
|
||||||
|
import { useChatList } from './use-chat-list'
|
||||||
|
import { useChatMessageSender } from './use-chat-message-sender'
|
||||||
|
import { useChatTreeOperations } from './use-chat-tree-operations'
|
||||||
|
|
||||||
|
export function useChat(
|
||||||
|
config: ChatConfig | undefined,
|
||||||
|
formSettings?: {
|
||||||
|
inputs: Inputs
|
||||||
|
inputsForm: InputForm[]
|
||||||
|
},
|
||||||
|
prevChatTree?: ChatItemInTree[],
|
||||||
|
stopChat?: (taskId: string) => void,
|
||||||
|
) {
|
||||||
|
const chatTree = useStore(s => s.chatTree)
|
||||||
|
const conversationId = useStore(s => s.conversationId)
|
||||||
|
const isResponding = useStore(s => s.isResponding)
|
||||||
|
const suggestedQuestions = useStore(s => s.suggestedQuestions)
|
||||||
|
const targetMessageId = useStore(s => s.targetMessageId)
|
||||||
|
const updateChatTree = useStore(s => s.updateChatTree)
|
||||||
|
const setTargetMessageId = useStore(s => s.setTargetMessageId)
|
||||||
|
|
||||||
|
const initialChatTreeRef = useRef(prevChatTree)
|
||||||
|
useEffect(() => {
|
||||||
|
const initialChatTree = initialChatTreeRef.current
|
||||||
|
if (!initialChatTree || initialChatTree.length === 0)
|
||||||
|
return
|
||||||
|
updateChatTree(currentChatTree => (currentChatTree.length === 0 ? initialChatTree : currentChatTree))
|
||||||
|
}, [updateChatTree])
|
||||||
|
|
||||||
|
const { updateCurrentQAOnTree } = useChatTreeOperations(updateChatTree)
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleResponding,
|
||||||
|
handleStop,
|
||||||
|
handleRestart,
|
||||||
|
} = useChatFlowControl({
|
||||||
|
stopChat,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
threadMessages,
|
||||||
|
chatList,
|
||||||
|
} = useChatList({
|
||||||
|
chatTree,
|
||||||
|
targetMessageId,
|
||||||
|
config,
|
||||||
|
formSettings,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleSend } = useChatMessageSender({
|
||||||
|
threadMessages,
|
||||||
|
config,
|
||||||
|
formSettings,
|
||||||
|
handleResponding,
|
||||||
|
updateCurrentQAOnTree,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
conversationId,
|
||||||
|
chatList,
|
||||||
|
setTargetMessageId,
|
||||||
|
handleSend,
|
||||||
|
handleStop,
|
||||||
|
handleRestart,
|
||||||
|
isResponding,
|
||||||
|
suggestedQuestions,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import type { UpdateCurrentQAParams } from './types'
|
||||||
|
import type { ChatItem } from '@/app/components/base/chat/types'
|
||||||
|
import type { AgentLogItem, NodeTracing } from '@/types/workflow'
|
||||||
|
import { NodeRunningStatus, WorkflowRunningStatus } from '../../../types'
|
||||||
|
|
||||||
|
type WorkflowEventHandlersContext = {
|
||||||
|
responseItem: ChatItem
|
||||||
|
questionItem: ChatItem
|
||||||
|
placeholderQuestionId: string
|
||||||
|
parentMessageId?: string
|
||||||
|
updateCurrentQAOnTree: (params: UpdateCurrentQAParams) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type TracingData = Partial<NodeTracing> & { id: string }
|
||||||
|
type AgentLogData = Partial<AgentLogItem> & { node_id: string, message_id: string }
|
||||||
|
|
||||||
|
export function createWorkflowEventHandlers(ctx: WorkflowEventHandlersContext) {
|
||||||
|
const { responseItem, questionItem, placeholderQuestionId, parentMessageId, updateCurrentQAOnTree } = ctx
|
||||||
|
|
||||||
|
const updateTree = () => {
|
||||||
|
updateCurrentQAOnTree({
|
||||||
|
placeholderQuestionId,
|
||||||
|
questionItem,
|
||||||
|
responseItem,
|
||||||
|
parentId: parentMessageId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTracingItem = (data: TracingData) => {
|
||||||
|
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
||||||
|
if (currentTracingIndex > -1) {
|
||||||
|
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
||||||
|
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
||||||
|
...data,
|
||||||
|
}
|
||||||
|
updateTree()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
onWorkflowStarted: ({ workflow_run_id, task_id }: { workflow_run_id: string, task_id: string }) => {
|
||||||
|
responseItem.workflow_run_id = workflow_run_id
|
||||||
|
responseItem.workflowProcess = {
|
||||||
|
status: WorkflowRunningStatus.Running,
|
||||||
|
tracing: [],
|
||||||
|
}
|
||||||
|
updateTree()
|
||||||
|
return task_id
|
||||||
|
},
|
||||||
|
|
||||||
|
onWorkflowFinished: ({ data }: { data: { status: string } }) => {
|
||||||
|
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
|
||||||
|
updateTree()
|
||||||
|
},
|
||||||
|
|
||||||
|
onIterationStart: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||||
|
responseItem.workflowProcess!.tracing!.push({
|
||||||
|
...data,
|
||||||
|
status: NodeRunningStatus.Running,
|
||||||
|
} as NodeTracing)
|
||||||
|
updateTree()
|
||||||
|
},
|
||||||
|
|
||||||
|
onIterationFinish: ({ data }: { data: TracingData }) => {
|
||||||
|
updateTracingItem(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoopStart: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||||
|
responseItem.workflowProcess!.tracing!.push({
|
||||||
|
...data,
|
||||||
|
status: NodeRunningStatus.Running,
|
||||||
|
} as NodeTracing)
|
||||||
|
updateTree()
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoopFinish: ({ data }: { data: TracingData }) => {
|
||||||
|
updateTracingItem(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
onNodeStarted: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||||
|
responseItem.workflowProcess!.tracing!.push({
|
||||||
|
...data,
|
||||||
|
status: NodeRunningStatus.Running,
|
||||||
|
} as NodeTracing)
|
||||||
|
updateTree()
|
||||||
|
},
|
||||||
|
|
||||||
|
onNodeRetry: ({ data }: { data: NodeTracing }) => {
|
||||||
|
responseItem.workflowProcess!.tracing!.push(data)
|
||||||
|
updateTree()
|
||||||
|
},
|
||||||
|
|
||||||
|
onNodeFinished: ({ data }: { data: TracingData }) => {
|
||||||
|
updateTracingItem(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
onAgentLog: ({ data }: { data: AgentLogData }) => {
|
||||||
|
const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||||
|
if (currentNodeIndex > -1) {
|
||||||
|
const current = responseItem.workflowProcess!.tracing![currentNodeIndex]
|
||||||
|
|
||||||
|
if (current.execution_metadata) {
|
||||||
|
if (current.execution_metadata.agent_log) {
|
||||||
|
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.message_id === data.message_id)
|
||||||
|
if (currentLogIndex > -1) {
|
||||||
|
current.execution_metadata.agent_log[currentLogIndex] = {
|
||||||
|
...current.execution_metadata.agent_log[currentLogIndex],
|
||||||
|
...data,
|
||||||
|
} as AgentLogItem
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
current.execution_metadata.agent_log.push(data as AgentLogItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
current.execution_metadata.agent_log = [data as AgentLogItem]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
current.execution_metadata = {
|
||||||
|
agent_log: [data as AgentLogItem],
|
||||||
|
} as NodeTracing['execution_metadata']
|
||||||
|
}
|
||||||
|
|
||||||
|
responseItem.workflowProcess!.tracing[currentNodeIndex] = {
|
||||||
|
...current,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTree()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import type { StateCreator } from 'zustand'
|
||||||
|
import type { ChatItemInTree } from '@/app/components/base/chat/types'
|
||||||
|
|
||||||
|
type ChatPreviewState = {
|
||||||
|
chatTree: ChatItemInTree[]
|
||||||
|
targetMessageId: string | undefined
|
||||||
|
suggestedQuestions: string[]
|
||||||
|
conversationId: string
|
||||||
|
isResponding: boolean
|
||||||
|
activeRunId: number
|
||||||
|
activeTaskId: string
|
||||||
|
hasStopResponded: boolean
|
||||||
|
suggestedQuestionsAbortController: AbortController | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatPreviewActions = {
|
||||||
|
setChatTree: (chatTree: ChatItemInTree[]) => void
|
||||||
|
updateChatTree: (updater: (chatTree: ChatItemInTree[]) => ChatItemInTree[]) => void
|
||||||
|
setTargetMessageId: (messageId: string | undefined) => void
|
||||||
|
setSuggestedQuestions: (questions: string[]) => void
|
||||||
|
setConversationId: (conversationId: string) => void
|
||||||
|
setIsResponding: (isResponding: boolean) => void
|
||||||
|
setActiveTaskId: (taskId: string) => void
|
||||||
|
setHasStopResponded: (hasStopResponded: boolean) => void
|
||||||
|
setSuggestedQuestionsAbortController: (controller: AbortController | null) => void
|
||||||
|
startRun: () => number
|
||||||
|
invalidateRun: () => number
|
||||||
|
resetChatPreview: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatPreviewSliceShape = ChatPreviewState & ChatPreviewActions
|
||||||
|
|
||||||
|
const initialState: ChatPreviewState = {
|
||||||
|
chatTree: [],
|
||||||
|
targetMessageId: undefined,
|
||||||
|
suggestedQuestions: [],
|
||||||
|
conversationId: '',
|
||||||
|
isResponding: false,
|
||||||
|
activeRunId: 0,
|
||||||
|
activeTaskId: '',
|
||||||
|
hasStopResponded: false,
|
||||||
|
suggestedQuestionsAbortController: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createChatPreviewSlice: StateCreator<ChatPreviewSliceShape> = (set, get) => ({
|
||||||
|
...initialState,
|
||||||
|
|
||||||
|
setChatTree: chatTree => set({ chatTree }),
|
||||||
|
|
||||||
|
updateChatTree: updater => set((state) => {
|
||||||
|
const nextChatTree = updater(state.chatTree)
|
||||||
|
if (nextChatTree === state.chatTree)
|
||||||
|
return state
|
||||||
|
return { chatTree: nextChatTree }
|
||||||
|
}),
|
||||||
|
|
||||||
|
setTargetMessageId: targetMessageId => set({ targetMessageId }),
|
||||||
|
|
||||||
|
setSuggestedQuestions: suggestedQuestions => set({ suggestedQuestions }),
|
||||||
|
|
||||||
|
setConversationId: conversationId => set({ conversationId }),
|
||||||
|
|
||||||
|
setIsResponding: isResponding => set({ isResponding }),
|
||||||
|
|
||||||
|
setActiveTaskId: activeTaskId => set({ activeTaskId }),
|
||||||
|
|
||||||
|
setHasStopResponded: hasStopResponded => set({ hasStopResponded }),
|
||||||
|
|
||||||
|
setSuggestedQuestionsAbortController: suggestedQuestionsAbortController => set({ suggestedQuestionsAbortController }),
|
||||||
|
|
||||||
|
startRun: () => {
|
||||||
|
const activeRunId = get().activeRunId + 1
|
||||||
|
set({
|
||||||
|
activeRunId,
|
||||||
|
activeTaskId: '',
|
||||||
|
hasStopResponded: false,
|
||||||
|
suggestedQuestionsAbortController: null,
|
||||||
|
})
|
||||||
|
return activeRunId
|
||||||
|
},
|
||||||
|
|
||||||
|
invalidateRun: () => {
|
||||||
|
const activeRunId = get().activeRunId + 1
|
||||||
|
set({
|
||||||
|
activeRunId,
|
||||||
|
activeTaskId: '',
|
||||||
|
suggestedQuestionsAbortController: null,
|
||||||
|
})
|
||||||
|
return activeRunId
|
||||||
|
},
|
||||||
|
|
||||||
|
resetChatPreview: () => set(state => ({
|
||||||
|
...initialState,
|
||||||
|
activeRunId: state.activeRunId + 1,
|
||||||
|
})),
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
StateCreator,
|
StateCreator,
|
||||||
} from 'zustand'
|
} from 'zustand'
|
||||||
|
import type { ChatPreviewSliceShape } from './chat-preview-slice'
|
||||||
import type { ChatVariableSliceShape } from './chat-variable-slice'
|
import type { ChatVariableSliceShape } from './chat-variable-slice'
|
||||||
import type { InspectVarsSliceShape } from './debug/inspect-vars-slice'
|
import type { InspectVarsSliceShape } from './debug/inspect-vars-slice'
|
||||||
import type { EnvVariableSliceShape } from './env-variable-slice'
|
import type { EnvVariableSliceShape } from './env-variable-slice'
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from 'zustand'
|
} from 'zustand'
|
||||||
import { createStore } from 'zustand/vanilla'
|
import { createStore } from 'zustand/vanilla'
|
||||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||||
|
import { createChatPreviewSlice } from './chat-preview-slice'
|
||||||
import { createChatVariableSlice } from './chat-variable-slice'
|
import { createChatVariableSlice } from './chat-variable-slice'
|
||||||
import { createInspectVarsSlice } from './debug/inspect-vars-slice'
|
import { createInspectVarsSlice } from './debug/inspect-vars-slice'
|
||||||
import { createEnvVariableSlice } from './env-variable-slice'
|
import { createEnvVariableSlice } from './env-variable-slice'
|
||||||
@@ -42,7 +44,8 @@ export type SliceFromInjection
|
|||||||
& Partial<RagPipelineSliceShape>
|
& Partial<RagPipelineSliceShape>
|
||||||
|
|
||||||
export type Shape
|
export type Shape
|
||||||
= ChatVariableSliceShape
|
= ChatPreviewSliceShape
|
||||||
|
& ChatVariableSliceShape
|
||||||
& EnvVariableSliceShape
|
& EnvVariableSliceShape
|
||||||
& FormSliceShape
|
& FormSliceShape
|
||||||
& HelpLineSliceShape
|
& HelpLineSliceShape
|
||||||
@@ -67,6 +70,7 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => {
|
|||||||
const { injectWorkflowStoreSliceFn } = params || {}
|
const { injectWorkflowStoreSliceFn } = params || {}
|
||||||
|
|
||||||
return createStore<Shape>((...args) => ({
|
return createStore<Shape>((...args) => ({
|
||||||
|
...createChatPreviewSlice(...args),
|
||||||
...createChatVariableSlice(...args),
|
...createChatVariableSlice(...args),
|
||||||
...createEnvVariableSlice(...args),
|
...createEnvVariableSlice(...args),
|
||||||
...createFormSlice(...args),
|
...createFormSlice(...args),
|
||||||
|
|||||||
@@ -3769,11 +3769,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/workflow/panel/debug-and-preview/hooks.ts": {
|
|
||||||
"ts/no-explicit-any": {
|
|
||||||
"count": 7
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"app/components/workflow/panel/env-panel/variable-modal.tsx": {
|
"app/components/workflow/panel/env-panel/variable-modal.tsx": {
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||||
"count": 4
|
"count": 4
|
||||||
|
|||||||
Reference in New Issue
Block a user