diff --git a/api/core/model_runtime/entities/message_entities.py b/api/core/model_runtime/entities/message_entities.py index 83b12082b28525..823c217c0928cc 100644 --- a/api/core/model_runtime/entities/message_entities.py +++ b/api/core/model_runtime/entities/message_entities.py @@ -88,6 +88,14 @@ class PromptMessage(ABC, BaseModel): content: Optional[str | list[PromptMessageContent]] = None name: Optional[str] = None + def is_empty(self) -> bool: + """ + Check if prompt message is empty. + + :return: True if prompt message is empty, False otherwise + """ + return not self.content + class UserPromptMessage(PromptMessage): """ @@ -118,6 +126,16 @@ class ToolCallFunction(BaseModel): role: PromptMessageRole = PromptMessageRole.ASSISTANT tool_calls: list[ToolCall] = [] + def is_empty(self) -> bool: + """ + Check if prompt message is empty. + + :return: True if prompt message is empty, False otherwise + """ + if not super().is_empty() and not self.tool_calls: + return False + + return True class SystemPromptMessage(PromptMessage): """ @@ -132,3 +150,14 @@ class ToolPromptMessage(PromptMessage): """ role: PromptMessageRole = PromptMessageRole.TOOL tool_call_id: str + + def is_empty(self) -> bool: + """ + Check if prompt message is empty. + + :return: True if prompt message is empty, False otherwise + """ + if not super().is_empty() and not self.tool_call_id: + return False + + return True diff --git a/api/core/model_runtime/model_providers/nvidia/llm/llm.py b/api/core/model_runtime/model_providers/nvidia/llm/llm.py index 81291bf6c49213..b1c2b77358da9b 100644 --- a/api/core/model_runtime/model_providers/nvidia/llm/llm.py +++ b/api/core/model_runtime/model_providers/nvidia/llm/llm.py @@ -131,7 +131,7 @@ def _validate_credentials(self, model: str, credentials: dict) -> None: endpoint_url, headers=headers, json=data, - timeout=(10, 60) + timeout=(10, 300) ) if response.status_code != 200: @@ -232,7 +232,7 @@ def _generate(self, model: str, credentials: dict, prompt_messages: list[PromptM endpoint_url, headers=headers, json=data, - timeout=(10, 60), + timeout=(10, 300), stream=stream ) diff --git a/api/core/model_runtime/model_providers/ollama/llm/llm.py b/api/core/model_runtime/model_providers/ollama/llm/llm.py index 3589ca77ccbbb9..fcb94084a51651 100644 --- a/api/core/model_runtime/model_providers/ollama/llm/llm.py +++ b/api/core/model_runtime/model_providers/ollama/llm/llm.py @@ -201,7 +201,7 @@ def _generate(self, model: str, credentials: dict, endpoint_url, headers=headers, json=data, - timeout=(10, 60), + timeout=(10, 300), stream=stream ) diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py b/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py index 45a5b49a8b0d71..e86755d693f797 100644 --- a/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py +++ b/api/core/model_runtime/model_providers/openai_api_compatible/llm/llm.py @@ -138,7 +138,7 @@ def validate_credentials(self, model: str, credentials: dict) -> None: endpoint_url, headers=headers, json=data, - timeout=(10, 60) + timeout=(10, 300) ) if response.status_code != 200: @@ -334,7 +334,7 @@ def _generate(self, model: str, credentials: dict, prompt_messages: list[PromptM endpoint_url, headers=headers, json=data, - timeout=(10, 60), + timeout=(10, 300), stream=stream ) diff --git a/api/core/tools/provider/builtin/jina/tools/jina_reader.py b/api/core/tools/provider/builtin/jina/tools/jina_reader.py index 322265cefe401f..fd29a00aa500bc 100644 --- a/api/core/tools/provider/builtin/jina/tools/jina_reader.py +++ b/api/core/tools/provider/builtin/jina/tools/jina_reader.py @@ -20,7 +20,7 @@ def _invoke(self, url = tool_parameters['url'] headers = { - 'Accept': 'text/event-stream' + 'Accept': 'application/json' } response = ssrf_proxy.get( diff --git a/api/core/workflow/nodes/llm/llm_node.py b/api/core/workflow/nodes/llm/llm_node.py index 491e9844772558..00999aa1a68bfb 100644 --- a/api/core/workflow/nodes/llm/llm_node.py +++ b/api/core/workflow/nodes/llm/llm_node.py @@ -438,7 +438,11 @@ def _fetch_prompt_messages(self, node_data: LLMNodeData, stop = model_config.stop vision_enabled = node_data.vision.enabled + filtered_prompt_messages = [] for prompt_message in prompt_messages: + if prompt_message.is_empty(): + continue + if not isinstance(prompt_message.content, str): prompt_message_content = [] for content_item in prompt_message.content: @@ -453,7 +457,13 @@ def _fetch_prompt_messages(self, node_data: LLMNodeData, and prompt_message_content[0].type == PromptMessageContentType.TEXT): prompt_message.content = prompt_message_content[0].data - return prompt_messages, stop + filtered_prompt_messages.append(prompt_message) + + if not filtered_prompt_messages: + raise ValueError("No prompt found in the LLM configuration. " + "Please ensure a prompt is properly configured before proceeding.") + + return filtered_prompt_messages, stop @classmethod def deduct_llm_quota(cls, tenant_id: str, model_instance: ModelInstance, usage: LLMUsage) -> None: diff --git a/web/app/components/app/chat/log/index.tsx b/web/app/components/app/chat/log/index.tsx index 34b8440addc1a9..d4c1cff2b287e5 100644 --- a/web/app/components/app/chat/log/index.tsx +++ b/web/app/components/app/chat/log/index.tsx @@ -11,8 +11,9 @@ const Log: FC = ({ logItem, }) => { const { t } = useTranslation() - const { setCurrentLogItem, setShowPromptLogModal, setShowMessageLogModal } = useAppStore() - const { workflow_run_id: runID } = logItem + const { setCurrentLogItem, setShowPromptLogModal, setShowAgentLogModal, setShowMessageLogModal } = useAppStore() + const { workflow_run_id: runID, agent_thoughts } = logItem + const isAgent = agent_thoughts && agent_thoughts.length > 0 return (
= ({ setCurrentLogItem(logItem) if (runID) setShowMessageLogModal(true) + else if (isAgent) + setShowAgentLogModal(true) else setShowPromptLogModal(true) }} > -
{runID ? t('appLog.viewLog') : t('appLog.promptLog')}
+
{runID ? t('appLog.viewLog') : isAgent ? t('appLog.agentLog') : t('appLog.promptLog')}
) } diff --git a/web/app/components/app/chat/type.ts b/web/app/components/app/chat/type.ts index f49f6d1881ca5d..9c96e36e8c0bf7 100644 --- a/web/app/components/app/chat/type.ts +++ b/web/app/components/app/chat/type.ts @@ -83,6 +83,9 @@ export type IChatItem = { agent_thoughts?: ThoughtItem[] message_files?: VisionFile[] workflow_run_id?: string + // for agent log + conversationId?: string + input?: any } export type MessageEnd = { diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index b2057d8cf5659f..0058f13361b54e 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -473,7 +473,7 @@ const Debug: FC = ({ )} )} - {showPromptLogModal && ( + {mode === AppType.completion && showPromptLogModal && ( { const matched = pathname.match(/\/app\/([^/]+)/) const appId = (matched?.length && matched[1]) ? matched[1] : '' const [mode, setMode] = useState('') - const [publishedConfig, setPublishedConfig] = useState(null) + const [publishedConfig, setPublishedConfig] = useState(null) const modalConfig = useMemo(() => appDetail?.model_config || {} as BackendModelConfig, [appDetail]) const [conversationId, setConversationId] = useState('') @@ -225,7 +225,7 @@ const Configuration: FC = () => { const [isShowHistoryModal, { setTrue: showHistoryModal, setFalse: hideHistoryModal }] = useBoolean(false) - const syncToPublishedConfig = (_publishedConfig: PublichConfig) => { + const syncToPublishedConfig = (_publishedConfig: PublishConfig) => { const modelConfig = _publishedConfig.modelConfig setModelConfig(_publishedConfig.modelConfig) setCompletionParams(_publishedConfig.completionParams) diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 94e56e6e4eac39..35bf7e7b607b58 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -35,6 +35,7 @@ import ModelName from '@/app/components/header/account-setting/model-provider-pa import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import TextGeneration from '@/app/components/app/text-generate/item' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' +import AgentLogModal from '@/app/components/base/agent-log-modal' import PromptLogModal from '@/app/components/base/prompt-log-modal' import MessageLogModal from '@/app/components/base/message-log-modal' import { useStore as useAppStore } from '@/app/components/app/store' @@ -76,7 +77,7 @@ const PARAM_MAP = { } // Format interface data for easy display -const getFormattedChatList = (messages: ChatMessage[]) => { +const getFormattedChatList = (messages: ChatMessage[], conversationId: string) => { const newChatList: IChatItem[] = [] messages.forEach((item: ChatMessage) => { newChatList.push({ @@ -107,6 +108,11 @@ const getFormattedChatList = (messages: ChatMessage[]) => { : []), ], workflow_run_id: item.workflow_run_id, + conversationId, + input: { + inputs: item.inputs, + query: item.query, + }, more: { time: dayjs.unix(item.created_at).format('hh:mm A'), tokens: item.answer_tokens + item.message_tokens, @@ -148,7 +154,7 @@ type IDetailPanel = { function DetailPanel({ detail, onFeedback }: IDetailPanel) { const { onClose, appDetail } = useContext(DrawerContext) - const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showMessageLogModal, setShowMessageLogModal } = useAppStore() + const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal, showMessageLogModal, setShowMessageLogModal } = useAppStore() const { t } = useTranslation() const [items, setItems] = React.useState([]) const [hasMore, setHasMore] = useState(true) @@ -172,7 +178,7 @@ function DetailPanel )} + {showAgentLogModal && ( + { + setCurrentLogItem() + setShowAgentLogModal(false) + }} + /> + )} {showMessageLogModal && ( = ({ logs, appDetail, onRefresh }) onClose={onCloseDrawer} mask={isMobile} footer={null} - panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl' + panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl' > void setCurrentLogItem: (item?: IChatItem) => void setShowPromptLogModal: (showPromptLogModal: boolean) => void + setShowAgentLogModal: (showAgentLogModal: boolean) => void setShowMessageLogModal: (showMessageLogModal: boolean) => void } @@ -27,6 +29,8 @@ export const useStore = create(set => ({ setCurrentLogItem: currentLogItem => set(() => ({ currentLogItem })), showPromptLogModal: false, setShowPromptLogModal: showPromptLogModal => set(() => ({ showPromptLogModal })), + showAgentLogModal: false, + setShowAgentLogModal: showAgentLogModal => set(() => ({ showAgentLogModal })), showMessageLogModal: false, setShowMessageLogModal: showMessageLogModal => set(() => ({ showMessageLogModal })), })) diff --git a/web/app/components/base/agent-log-modal/detail.tsx b/web/app/components/base/agent-log-modal/detail.tsx new file mode 100644 index 00000000000000..d83901d0a2a10a --- /dev/null +++ b/web/app/components/base/agent-log-modal/detail.tsx @@ -0,0 +1,132 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useContext } from 'use-context-selector' +import { useTranslation } from 'react-i18next' +import { flatten, uniq } from 'lodash-es' +import cn from 'classnames' +import ResultPanel from './result' +import TracingPanel from './tracing' +import { ToastContext } from '@/app/components/base/toast' +import Loading from '@/app/components/base/loading' +import { fetchAgentLogDetail } from '@/service/log' +import type { AgentIteration, AgentLogDetailResponse } from '@/models/log' +import { useStore as useAppStore } from '@/app/components/app/store' +import type { IChatItem } from '@/app/components/app/chat/type' + +export type AgentLogDetailProps = { + activeTab?: 'DETAIL' | 'TRACING' + conversationID: string + log: IChatItem + messageID: string +} + +const AgentLogDetail: FC = ({ + activeTab = 'DETAIL', + conversationID, + messageID, + log, +}) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const [currentTab, setCurrentTab] = useState(activeTab) + const { appDetail } = useAppStore() + const [loading, setLoading] = useState(true) + const [runDetail, setRunDetail] = useState() + const [list, setList] = useState([]) + + const tools = useMemo(() => { + const res = uniq(flatten(runDetail?.iterations.map((iteration: any) => { + return iteration.tool_calls.map((tool: any) => tool.tool_name).filter(Boolean) + })).filter(Boolean)) + return res + }, [runDetail]) + + const getLogDetail = useCallback(async (appID: string, conversationID: string, messageID: string) => { + try { + const res = await fetchAgentLogDetail({ + appID, + params: { + conversation_id: conversationID, + message_id: messageID, + }, + }) + setRunDetail(res) + setList(res.iterations) + } + catch (err) { + notify({ + type: 'error', + message: `${err}`, + }) + } + }, [notify]) + + const getData = async (appID: string, conversationID: string, messageID: string) => { + setLoading(true) + await getLogDetail(appID, conversationID, messageID) + setLoading(false) + } + + const switchTab = async (tab: string) => { + setCurrentTab(tab) + } + + useEffect(() => { + // fetch data + if (appDetail) + getData(appDetail.id, conversationID, messageID) + }, [appDetail, conversationID, messageID]) + + return ( +
+ {/* tab */} +
+
switchTab('DETAIL')} + >{t('runLog.detail')}
+
switchTab('TRACING')} + >{t('runLog.tracing')}
+
+ {/* panel detal */} +
+ {loading && ( +
+ +
+ )} + {!loading && currentTab === 'DETAIL' && runDetail && ( + + )} + {!loading && currentTab === 'TRACING' && ( + + )} +
+
+ ) +} + +export default AgentLogDetail diff --git a/web/app/components/base/agent-log-modal/index.tsx b/web/app/components/base/agent-log-modal/index.tsx new file mode 100644 index 00000000000000..e0917a391e28bf --- /dev/null +++ b/web/app/components/base/agent-log-modal/index.tsx @@ -0,0 +1,61 @@ +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import cn from 'classnames' +import { useEffect, useRef, useState } from 'react' +import { useClickAway } from 'ahooks' +import AgentLogDetail from './detail' +import { XClose } from '@/app/components/base/icons/src/vender/line/general' +import type { IChatItem } from '@/app/components/app/chat/type' + +type AgentLogModalProps = { + currentLogItem?: IChatItem + width: number + onCancel: () => void +} +const AgentLogModal: FC = ({ + currentLogItem, + width, + onCancel, +}) => { + const { t } = useTranslation() + const ref = useRef(null) + const [mounted, setMounted] = useState(false) + + useClickAway(() => { + if (mounted) + onCancel() + }, ref) + + useEffect(() => { + setMounted(true) + }, []) + + if (!currentLogItem || !currentLogItem.conversationId) + return null + + return ( +
+

{t('appLog.runDetail.workflowTitle')}

+ + + + +
+ ) +} + +export default AgentLogModal diff --git a/web/app/components/base/agent-log-modal/iteration.tsx b/web/app/components/base/agent-log-modal/iteration.tsx new file mode 100644 index 00000000000000..8b1af48d8face6 --- /dev/null +++ b/web/app/components/base/agent-log-modal/iteration.tsx @@ -0,0 +1,50 @@ +'use client' +import { useTranslation } from 'react-i18next' +import type { FC } from 'react' +import cn from 'classnames' +import ToolCall from './tool-call' +import type { AgentIteration } from '@/models/log' + +type Props = { + isFinal: boolean + index: number + iterationInfo: AgentIteration +} + +const Iteration: FC = ({ iterationInfo, isFinal, index }) => { + const { t } = useTranslation() + + return ( +
+
+ {isFinal && ( +
{t('appLog.agentLogDetail.finalProcessing')}
+ )} + {!isFinal && ( +
{`${t('appLog.agentLogDetail.iteration').toUpperCase()} ${index}`}
+ )} +
+
+ + {iterationInfo.tool_calls.map((toolCall, index) => ( + + ))} +
+ ) +} + +export default Iteration diff --git a/web/app/components/base/agent-log-modal/result.tsx b/web/app/components/base/agent-log-modal/result.tsx new file mode 100644 index 00000000000000..e8cd95315f28fd --- /dev/null +++ b/web/app/components/base/agent-log-modal/result.tsx @@ -0,0 +1,126 @@ +'use client' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import dayjs from 'dayjs' +import StatusPanel from '@/app/components/workflow/run/status' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' + +type ResultPanelProps = { + status: string + elapsed_time?: number + total_tokens?: number + error?: string + inputs?: any + outputs?: any + created_by?: string + created_at?: string + agentMode?: string + tools?: string[] + iterations?: number +} + +const ResultPanel: FC = ({ + status, + elapsed_time, + total_tokens, + error, + inputs, + outputs, + created_by, + created_at = 0, + agentMode, + tools, + iterations, +}) => { + const { t } = useTranslation() + + return ( +
+
+ +
+
+ INPUT
} + language={CodeLanguage.json} + value={inputs} + isJSONStringifyBeauty + /> + OUTPUT
} + language={CodeLanguage.json} + value={outputs} + isJSONStringifyBeauty + /> + +
+
+
+
+
+
{t('runLog.meta.title')}
+
+
+
{t('runLog.meta.status')}
+
+ SUCCESS +
+
+
+
{t('runLog.meta.executor')}
+
+ {created_by || 'N/A'} +
+
+
+
{t('runLog.meta.startTime')}
+
+ {dayjs(created_at).format('YYYY-MM-DD hh:mm:ss')} +
+
+
+
{t('runLog.meta.time')}
+
+ {`${elapsed_time?.toFixed(3)}s`} +
+
+
+
{t('runLog.meta.tokens')}
+
+ {`${total_tokens || 0} Tokens`} +
+
+
+
{t('appLog.agentLogDetail.agentMode')}
+
+ {agentMode === 'function_call' ? t('appDebug.agent.agentModeType.functionCall') : t('appDebug.agent.agentModeType.ReACT')} +
+
+
+
{t('appLog.agentLogDetail.toolUsed')}
+
+ {tools?.length ? tools?.join(', ') : 'Null'} +
+
+
+
{t('appLog.agentLogDetail.iterations')}
+
+ {iterations} +
+
+
+
+
+
+ ) +} + +export default ResultPanel diff --git a/web/app/components/base/agent-log-modal/tool-call.tsx b/web/app/components/base/agent-log-modal/tool-call.tsx new file mode 100644 index 00000000000000..c4d3f2a2ccea68 --- /dev/null +++ b/web/app/components/base/agent-log-modal/tool-call.tsx @@ -0,0 +1,140 @@ +'use client' +import type { FC } from 'react' +import { useState } from 'react' +import cn from 'classnames' +import { useContext } from 'use-context-selector' +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 { AlertCircle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback' +import { CheckCircle } from '@/app/components/base/icons/src/vender/line/general' +import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows' +import type { ToolCall } from '@/models/log' +import { BlockEnum } from '@/app/components/workflow/types' +import I18n from '@/context/i18n' + +type Props = { + toolCall: ToolCall + isLLM: boolean + isFinal?: boolean + tokens?: number + observation?: any + finalAnswer?: any +} + +const ToolCallItem: FC = ({ toolCall, isLLM = false, isFinal, tokens, observation, finalAnswer }) => { + const [collapseState, setCollapseState] = useState(true) + const { locale } = useContext(I18n) + const toolName = isLLM ? 'LLM' : (toolCall.tool_label[locale] || toolCall.tool_label[locale.replaceAll('-', '_')]) + + const getTime = (time: number) => { + if (time < 1) + return `${(time * 1000).toFixed(3)} ms` + if (time > 60) + return `${parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s` + return `${time.toFixed(3)} s` + } + + const getTokenCount = (tokens: number) => { + if (tokens < 1000) + return tokens + if (tokens >= 1000 && tokens < 1000000) + return `${parseFloat((tokens / 1000).toFixed(3))}K` + if (tokens >= 1000000) + return `${parseFloat((tokens / 1000000).toFixed(3))}M` + } + + return ( +
+
+
setCollapseState(!collapseState)} + > + + +
{toolName}
+
+ {toolCall.time_cost && ( + {getTime(toolCall.time_cost || 0)} + )} + {isLLM && ( + {`${getTokenCount(tokens || 0)} tokens`} + )} +
+ {toolCall.status === 'success' && ( + + )} + {toolCall.status === 'error' && ( + + )} +
+ {!collapseState && ( +
+
+ {toolCall.status === 'error' && ( +
{toolCall.error}
+ )} +
+ {toolCall.tool_input && ( +
+ INPUT
} + language={CodeLanguage.json} + value={toolCall.tool_input} + isJSONStringifyBeauty + /> +
+ )} + {toolCall.tool_output && ( +
+ OUTPUT
} + language={CodeLanguage.json} + value={toolCall.tool_output} + isJSONStringifyBeauty + /> +
+ )} + {isLLM && ( +
+ OBSERVATION
} + language={CodeLanguage.json} + value={observation} + isJSONStringifyBeauty + /> +
+ )} + {isLLM && ( +
+ {isFinal ? 'FINAL ANSWER' : 'THOUGHT'}
} + language={CodeLanguage.json} + value={finalAnswer} + isJSONStringifyBeauty + /> + + )} + + )} + + + ) +} + +export default ToolCallItem diff --git a/web/app/components/base/agent-log-modal/tracing.tsx b/web/app/components/base/agent-log-modal/tracing.tsx new file mode 100644 index 00000000000000..59cffa00553c3c --- /dev/null +++ b/web/app/components/base/agent-log-modal/tracing.tsx @@ -0,0 +1,25 @@ +'use client' +import type { FC } from 'react' +import Iteration from './iteration' +import type { AgentIteration } from '@/models/log' + +type TracingPanelProps = { + list: AgentIteration[] +} + +const TracingPanel: FC = ({ list }) => { + return ( +
+ {list.map((iteration, index) => ( + + ))} +
+ ) +} + +export default TracingPanel diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 4cdb6e8e38e779..0cbe7b761662ee 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -322,6 +322,7 @@ export const useChat = ( } draft[index] = { ...draft[index], + content: newResponseItem.answer, log: [ ...newResponseItem.message, ...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant' @@ -339,6 +340,12 @@ export const useChat = ( tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens, latency: newResponseItem.provider_response_latency.toFixed(2), }, + // for agent log + conversationId: connversationId.current, + input: { + inputs: newResponseItem.inputs, + query: newResponseItem.query, + }, } } }) diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index 87332931f3d8a4..6d374b0089d442 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -26,6 +26,7 @@ import { ChatContextProvider } from './context' import type { Emoji } from '@/app/components/tools/types' import Button from '@/app/components/base/button' import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' +import AgentLogModal from '@/app/components/base/agent-log-modal' import PromptLogModal from '@/app/components/base/prompt-log-modal' import { useStore as useAppStore } from '@/app/components/app/store' @@ -78,7 +79,7 @@ const Chat: FC = ({ chatAnswerContainerInner, }) => { const { t } = useTranslation() - const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal } = useAppStore() + const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore() const [width, setWidth] = useState(0) const chatContainerRef = useRef(null) const chatContainerInnerRef = useRef(null) @@ -259,6 +260,16 @@ const Chat: FC = ({ }} /> )} + {showAgentLogModal && ( + { + setCurrentLogItem() + setShowAgentLogModal(false) + }} + /> + )} ) diff --git a/web/app/components/base/chat/types.ts b/web/app/components/base/chat/types.ts index 8edc2574dcdcf5..b3c3f1b5c4ad6d 100644 --- a/web/app/components/base/chat/types.ts +++ b/web/app/components/base/chat/types.ts @@ -59,6 +59,7 @@ export type WorkflowProcess = { export type ChatItem = IChatItem & { isError?: boolean workflowProcess?: WorkflowProcess + conversationId?: string } export type OnSend = (message: string, files?: VisionFile[]) => void diff --git a/web/app/components/base/message-log-modal/index.tsx b/web/app/components/base/message-log-modal/index.tsx index 01653736f3454d..4c389f7e10eb15 100644 --- a/web/app/components/base/message-log-modal/index.tsx +++ b/web/app/components/base/message-log-modal/index.tsx @@ -39,12 +39,12 @@ const MessageLogModal: FC = ({
= ({ return (
diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index ea9af3e9aa35ec..e092f1cbd3d2ee 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -820,8 +820,14 @@ export const useNodesInteractions = () => { const { getNodes, + edges, } = store.getState() + const currentEdgeIndex = edges.findIndex(edge => edge.selected) + + if (currentEdgeIndex > -1) + return + const nodes = getNodes() const nodesToDelete = nodes.filter(node => node.data.selected) diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index fdd6d73fad8cd1..ca501e299834f6 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -137,8 +137,8 @@ const Workflow: FC = memo(({ }, }) - useKeyPress(['delete'], handleEdgeDelete) useKeyPress(['delete', 'backspace'], handleNodeDeleteSelected) + useKeyPress(['delete', 'backspace'], handleEdgeDelete) useKeyPress(['ctrl.c', 'meta.c'], handleNodeCopySelected) useKeyPress(['ctrl.x', 'meta.x'], handleNodeCut) useKeyPress(['ctrl.v', 'meta.v'], handleNodePaste) diff --git a/web/i18n/de-DE/app-log.ts b/web/i18n/de-DE/app-log.ts index 164665887f0141..f0985a2ed7a6c9 100644 --- a/web/i18n/de-DE/app-log.ts +++ b/web/i18n/de-DE/app-log.ts @@ -64,6 +64,22 @@ const translation = { not_annotated: 'Nicht annotiert', }, }, + workflowTitle: 'Workflow-Protokolle', + workflowSubtitle: 'Das Protokoll hat den Vorgang von Automate aufgezeichnet.', + runDetail: { + title: 'Konversationsprotokoll', + workflowTitle: 'Protokolldetail', + }, + promptLog: 'Prompt-Protokoll', + agentLog: 'Agentenprotokoll', + viewLog: 'Protokoll anzeigen', + agentLogDetail: { + agentMode: 'Agentenmodus', + toolUsed: 'Verwendetes Werkzeug', + iterations: 'Iterationen', + iteration: 'Iteration', + finalProcessing: 'Endverarbeitung', + }, } export default translation diff --git a/web/i18n/en-US/app-log.ts b/web/i18n/en-US/app-log.ts index 5c86703db969d2..b45c1640d14b6f 100644 --- a/web/i18n/en-US/app-log.ts +++ b/web/i18n/en-US/app-log.ts @@ -77,7 +77,15 @@ const translation = { workflowTitle: 'Log Detail', }, promptLog: 'Prompt Log', + agentLog: 'Agent Log', viewLog: 'View Log', + agentLogDetail: { + agentMode: 'Agent Mode', + toolUsed: 'Tool Used', + iterations: 'Iterations', + iteration: 'Iteration', + finalProcessing: 'Final Processing', + }, } export default translation diff --git a/web/i18n/fr-FR/app-log.ts b/web/i18n/fr-FR/app-log.ts index 3724a2c0c78da4..ca438d0a371557 100644 --- a/web/i18n/fr-FR/app-log.ts +++ b/web/i18n/fr-FR/app-log.ts @@ -77,7 +77,15 @@ const translation = { workflowTitle: 'Détail du journal', }, promptLog: 'Journal de consigne', + agentLog: 'Journal des agents', viewLog: 'Voir le journal', + agentLogDetail: { + agentMode: 'Mode Agent', + toolUsed: 'Outil utilisé', + iterations: 'Itérations', + iteration: 'Itération', + finalProcessing: 'Traitement final', + }, } export default translation diff --git a/web/i18n/ja-JP/app-log.ts b/web/i18n/ja-JP/app-log.ts index 3935503d79a9d8..9d5ef54be8632f 100644 --- a/web/i18n/ja-JP/app-log.ts +++ b/web/i18n/ja-JP/app-log.ts @@ -77,7 +77,15 @@ const translation = { workflowTitle: 'ログの詳細', }, promptLog: 'プロンプトログ', + agentLog: 'エージェントログ', viewLog: 'ログを表示', + agentLogDetail: { + agentMode: 'エージェントモード', + toolUsed: '使用したツール', + iterations: '反復', + iteration: '反復', + finalProcessing: '最終処理', + }, } export default translation diff --git a/web/i18n/pt-BR/app-log.ts b/web/i18n/pt-BR/app-log.ts index 2f7b1be8b4a542..9b3ba9aaf22b8f 100644 --- a/web/i18n/pt-BR/app-log.ts +++ b/web/i18n/pt-BR/app-log.ts @@ -77,7 +77,15 @@ const translation = { workflowTitle: 'Detalhes do Registro', }, promptLog: 'Registro de Prompt', + agentLog: 'Registro do agente', viewLog: 'Ver Registro', + agenteLogDetail: { + agentMode: 'Modo Agente', + toolUsed: 'Ferramenta usada', + iterações: 'Iterações', + iteração: 'Iteração', + finalProcessing: 'Processamento Final', + }, } export default translation diff --git a/web/i18n/uk-UA/app-log.ts b/web/i18n/uk-UA/app-log.ts index 3e8bc4988cdacd..c613589e8cf875 100644 --- a/web/i18n/uk-UA/app-log.ts +++ b/web/i18n/uk-UA/app-log.ts @@ -77,7 +77,15 @@ const translation = { workflowTitle: 'Деталі Журналу', }, promptLog: 'Журнал Запитань', - viewLog: 'Переглянути Журнал', + agentLog: 'Журнал агента', + viewLog: 'Переглянути журнал', + agentLogDetail: { + agentMode: 'Режим агента', + toolUsed: 'Використаний інструмент', + iterations: 'Ітерації', + iteration: 'Ітерація', + finalProcessing: 'Остаточна обробка', + }, } export default translation diff --git a/web/i18n/vi-VN/app-log.ts b/web/i18n/vi-VN/app-log.ts index c6461a8743a041..193927c91d8b67 100644 --- a/web/i18n/vi-VN/app-log.ts +++ b/web/i18n/vi-VN/app-log.ts @@ -77,7 +77,15 @@ const translation = { workflowTitle: 'Chi Tiết Nhật Ký', }, promptLog: 'Nhật Ký Nhắc Nhở', - viewLog: 'Xem Nhật Ký', + AgentLog: 'Nhật ký đại lý', + viewLog: 'Xem nhật ký', + agentLogDetail: { + AgentMode: 'Chế độ đại lý', + toolUsed: 'Công cụ được sử dụng', + iterations: 'Lặp lại', + iteration: 'Lặp lại', + finalProcessing: 'Xử lý cuối cùng', + }, } export default translation diff --git a/web/i18n/zh-Hans/app-log.ts b/web/i18n/zh-Hans/app-log.ts index 27b21946056d50..d8993c8f75125e 100644 --- a/web/i18n/zh-Hans/app-log.ts +++ b/web/i18n/zh-Hans/app-log.ts @@ -77,7 +77,15 @@ const translation = { workflowTitle: '日志详情', }, promptLog: 'Prompt 日志', + agentLog: 'Agent 日志', viewLog: '查看日志', + agentLogDetail: { + agentMode: 'Agent 模式', + toolUsed: '使用工具', + iterations: '迭代次数', + iteration: '迭代', + finalProcessing: '最终处理', + }, } export default translation diff --git a/web/models/log.ts b/web/models/log.ts index 3b8509b1b727e7..3b893e1e883bae 100644 --- a/web/models/log.ts +++ b/web/models/log.ts @@ -4,6 +4,7 @@ import type { Edge, Node, } from '@/app/components/workflow/types' + // Log type contains key:string conversation_id:string created_at:string quesiton:string answer:string export type Conversation = { id: string @@ -292,3 +293,57 @@ export type WorkflowRunDetailResponse = { created_at: number finished_at: number } + +export type AgentLogMeta = { + status: string + executor: string + start_time: string + elapsed_time: number + total_tokens: number + agent_mode: string + iterations: number + error?: string +} + +export type ToolCall = { + status: string + error?: string | null + time_cost?: number + tool_icon: any + tool_input?: any + tool_output?: any + tool_name?: string + tool_label?: any + tool_parameters?: any +} + +export type AgentIteration = { + created_at: string + files: string[] + thought: string + tokens: number + tool_calls: ToolCall[] + tool_raw: { + inputs: string + outputs: string + } +} + +export type AgentLogFile = { + id: string + type: string + url: string + name: string + belongs_to: string +} + +export type AgentLogDetailRequest = { + conversation_id: string + message_id: string +} + +export type AgentLogDetailResponse = { + meta: AgentLogMeta + iterations: AgentIteration[] + files: AgentLogFile[] +} diff --git a/web/service/log.ts b/web/service/log.ts index 4d26b473984479..ec22785e40d470 100644 --- a/web/service/log.ts +++ b/web/service/log.ts @@ -1,6 +1,8 @@ import type { Fetcher } from 'swr' import { get, post } from './base' import type { + AgentLogDetailRequest, + AgentLogDetailResponse, AnnotationsCountResponse, ChatConversationFullDetailResponse, ChatConversationsRequest, @@ -73,3 +75,7 @@ export const fetchRunDetail = ({ appID, runID }: { appID: string; runID: string export const fetchTracingList: Fetcher = ({ url }) => { return get(url) } + +export const fetchAgentLogDetail = ({ appID, params }: { appID: string; params: AgentLogDetailRequest }) => { + return get(`/apps/${appID}/agent/logs`, { params }) +}