| @@ -11,8 +11,9 @@ const Log: FC<LogProps> = ({ | |||
| 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 ( | |||
| <div | |||
| @@ -23,12 +24,14 @@ const Log: FC<LogProps> = ({ | |||
| setCurrentLogItem(logItem) | |||
| if (runID) | |||
| setShowMessageLogModal(true) | |||
| else if (isAgent) | |||
| setShowAgentLogModal(true) | |||
| else | |||
| setShowPromptLogModal(true) | |||
| }} | |||
| > | |||
| <File02 className='mr-1 w-4 h-4' /> | |||
| <div className='text-xs leading-4'>{runID ? t('appLog.viewLog') : t('appLog.promptLog')}</div> | |||
| <div className='text-xs leading-4'>{runID ? t('appLog.viewLog') : isAgent ? t('appLog.agentLog') : t('appLog.promptLog')}</div> | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -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 = { | |||
| @@ -473,7 +473,7 @@ const Debug: FC<IDebug> = ({ | |||
| )} | |||
| </div> | |||
| )} | |||
| {showPromptLogModal && ( | |||
| {mode === AppType.completion && showPromptLogModal && ( | |||
| <PromptLogModal | |||
| width={width} | |||
| currentLogItem={currentLogItem} | |||
| @@ -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<T> = { | |||
| function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback }: IDetailPanel<T>) { | |||
| 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<IChatItem[]>([]) | |||
| const [hasMore, setHasMore] = useState(true) | |||
| @@ -172,7 +178,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo | |||
| const varValues = messageRes.data[0].inputs | |||
| setVarValues(varValues) | |||
| } | |||
| const newItems = [...getFormattedChatList(messageRes.data), ...items] | |||
| const newItems = [...getFormattedChatList(messageRes.data, detail.id), ...items] | |||
| if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) { | |||
| newItems.unshift({ | |||
| id: 'introduction', | |||
| @@ -401,6 +407,16 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo | |||
| }} | |||
| /> | |||
| )} | |||
| {showAgentLogModal && ( | |||
| <AgentLogModal | |||
| width={width} | |||
| currentLogItem={currentLogItem} | |||
| onCancel={() => { | |||
| setCurrentLogItem() | |||
| setShowAgentLogModal(false) | |||
| }} | |||
| /> | |||
| )} | |||
| {showMessageLogModal && ( | |||
| <MessageLogModal | |||
| width={width} | |||
| @@ -607,7 +623,7 @@ const ConversationList: FC<IConversationList> = ({ 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' | |||
| > | |||
| <DrawerContext.Provider value={{ | |||
| onClose: onCloseDrawer, | |||
| @@ -7,6 +7,7 @@ type State = { | |||
| appSidebarExpand: string | |||
| currentLogItem?: IChatItem | |||
| showPromptLogModal: boolean | |||
| showAgentLogModal: boolean | |||
| showMessageLogModal: boolean | |||
| } | |||
| @@ -15,6 +16,7 @@ type Action = { | |||
| setAppSiderbarExpand: (state: string) => 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<State & Action>(set => ({ | |||
| setCurrentLogItem: currentLogItem => set(() => ({ currentLogItem })), | |||
| showPromptLogModal: false, | |||
| setShowPromptLogModal: showPromptLogModal => set(() => ({ showPromptLogModal })), | |||
| showAgentLogModal: false, | |||
| setShowAgentLogModal: showAgentLogModal => set(() => ({ showAgentLogModal })), | |||
| showMessageLogModal: false, | |||
| setShowMessageLogModal: showMessageLogModal => set(() => ({ showMessageLogModal })), | |||
| })) | |||
| @@ -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<AgentLogDetailProps> = ({ | |||
| activeTab = 'DETAIL', | |||
| conversationID, | |||
| messageID, | |||
| log, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useContext(ToastContext) | |||
| const [currentTab, setCurrentTab] = useState<string>(activeTab) | |||
| const { appDetail } = useAppStore() | |||
| const [loading, setLoading] = useState<boolean>(true) | |||
| const [runDetail, setRunDetail] = useState<AgentLogDetailResponse>() | |||
| const [list, setList] = useState<AgentIteration[]>([]) | |||
| 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 ( | |||
| <div className='grow relative flex flex-col'> | |||
| {/* tab */} | |||
| <div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'> | |||
| <div | |||
| className={cn( | |||
| 'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer', | |||
| currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-gray-700', | |||
| )} | |||
| onClick={() => switchTab('DETAIL')} | |||
| >{t('runLog.detail')}</div> | |||
| <div | |||
| className={cn( | |||
| 'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer', | |||
| currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-gray-700', | |||
| )} | |||
| onClick={() => switchTab('TRACING')} | |||
| >{t('runLog.tracing')}</div> | |||
| </div> | |||
| {/* panel detal */} | |||
| <div className={cn('grow bg-white h-0 overflow-y-auto rounded-b-2xl', currentTab !== 'DETAIL' && '!bg-gray-50')}> | |||
| {loading && ( | |||
| <div className='flex h-full items-center justify-center bg-white'> | |||
| <Loading /> | |||
| </div> | |||
| )} | |||
| {!loading && currentTab === 'DETAIL' && runDetail && ( | |||
| <ResultPanel | |||
| inputs={log.input} | |||
| outputs={log.content} | |||
| status={runDetail.meta.status} | |||
| error={runDetail.meta.error} | |||
| elapsed_time={runDetail.meta.elapsed_time} | |||
| total_tokens={runDetail.meta.total_tokens} | |||
| created_at={runDetail.meta.start_time} | |||
| created_by={runDetail.meta.executor} | |||
| agentMode={runDetail.meta.agent_mode} | |||
| tools={tools} | |||
| iterations={runDetail.iterations.length} | |||
| /> | |||
| )} | |||
| {!loading && currentTab === 'TRACING' && ( | |||
| <TracingPanel | |||
| list={list} | |||
| /> | |||
| )} | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default AgentLogDetail | |||
| @@ -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<AgentLogModalProps> = ({ | |||
| 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 ( | |||
| <div | |||
| className={cn('relative flex flex-col py-3 bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl z-10')} | |||
| style={{ | |||
| width: 480, | |||
| position: 'fixed', | |||
| top: 56 + 8, | |||
| left: 8 + (width - 480), | |||
| bottom: 16, | |||
| }} | |||
| ref={ref} | |||
| > | |||
| <h1 className='shrink-0 px-4 py-1 text-md font-semibold text-gray-900'>{t('appLog.runDetail.workflowTitle')}</h1> | |||
| <span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onCancel}> | |||
| <XClose className='w-4 h-4 text-gray-500' /> | |||
| </span> | |||
| <AgentLogDetail | |||
| conversationID={currentLogItem.conversationId} | |||
| messageID={currentLogItem.id} | |||
| log={currentLogItem} | |||
| /> | |||
| </div> | |||
| ) | |||
| } | |||
| export default AgentLogModal | |||
| @@ -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<Props> = ({ iterationInfo, isFinal, index }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className={cn('px-4 py-2')}> | |||
| <div className='flex items-center'> | |||
| {isFinal && ( | |||
| <div className='shrink-0 mr-3 text-gray-500 text-xs leading-[18px] font-semibold'>{t('appLog.agentLogDetail.finalProcessing')}</div> | |||
| )} | |||
| {!isFinal && ( | |||
| <div className='shrink-0 mr-3 text-gray-500 text-xs leading-[18px] font-semibold'>{`${t('appLog.agentLogDetail.iteration').toUpperCase()} ${index}`}</div> | |||
| )} | |||
| <div className='grow h-[1px] bg-gradient-to-r from-[#f3f4f6] to-gray-50'></div> | |||
| </div> | |||
| <ToolCall | |||
| isLLM | |||
| isFinal={isFinal} | |||
| tokens={iterationInfo.tokens} | |||
| observation={iterationInfo.tool_raw.outputs} | |||
| finalAnswer={iterationInfo.thought} | |||
| toolCall={{ | |||
| status: 'success', | |||
| tool_icon: null, | |||
| }} | |||
| /> | |||
| {iterationInfo.tool_calls.map((toolCall, index) => ( | |||
| <ToolCall | |||
| isLLM={false} | |||
| key={index} | |||
| toolCall={toolCall} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ) | |||
| } | |||
| export default Iteration | |||
| @@ -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<ResultPanelProps> = ({ | |||
| status, | |||
| elapsed_time, | |||
| total_tokens, | |||
| error, | |||
| inputs, | |||
| outputs, | |||
| created_by, | |||
| created_at = 0, | |||
| agentMode, | |||
| tools, | |||
| iterations, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className='bg-white py-2'> | |||
| <div className='px-4 py-2'> | |||
| <StatusPanel | |||
| status='succeeded' | |||
| time={elapsed_time} | |||
| tokens={total_tokens} | |||
| error={error} | |||
| /> | |||
| </div> | |||
| <div className='px-4 py-2 flex flex-col gap-2'> | |||
| <CodeEditor | |||
| readOnly | |||
| title={<div>INPUT</div>} | |||
| language={CodeLanguage.json} | |||
| value={inputs} | |||
| isJSONStringifyBeauty | |||
| /> | |||
| <CodeEditor | |||
| readOnly | |||
| title={<div>OUTPUT</div>} | |||
| language={CodeLanguage.json} | |||
| value={outputs} | |||
| isJSONStringifyBeauty | |||
| /> | |||
| </div> | |||
| <div className='px-4 py-2'> | |||
| <div className='h-[0.5px] bg-black opacity-5' /> | |||
| </div> | |||
| <div className='px-4 py-2'> | |||
| <div className='relative'> | |||
| <div className='h-6 leading-6 text-gray-500 text-xs font-medium'>{t('runLog.meta.title')}</div> | |||
| <div className='py-1'> | |||
| <div className='flex'> | |||
| <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.status')}</div> | |||
| <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'> | |||
| <span>SUCCESS</span> | |||
| </div> | |||
| </div> | |||
| <div className='flex'> | |||
| <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.executor')}</div> | |||
| <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'> | |||
| <span>{created_by || 'N/A'}</span> | |||
| </div> | |||
| </div> | |||
| <div className='flex'> | |||
| <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.startTime')}</div> | |||
| <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'> | |||
| <span>{dayjs(created_at).format('YYYY-MM-DD hh:mm:ss')}</span> | |||
| </div> | |||
| </div> | |||
| <div className='flex'> | |||
| <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.time')}</div> | |||
| <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'> | |||
| <span>{`${elapsed_time?.toFixed(3)}s`}</span> | |||
| </div> | |||
| </div> | |||
| <div className='flex'> | |||
| <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.tokens')}</div> | |||
| <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'> | |||
| <span>{`${total_tokens || 0} Tokens`}</span> | |||
| </div> | |||
| </div> | |||
| <div className='flex'> | |||
| <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('appLog.agentLogDetail.agentMode')}</div> | |||
| <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'> | |||
| <span>{agentMode === 'function_call' ? t('appDebug.agent.agentModeType.functionCall') : t('appDebug.agent.agentModeType.ReACT')}</span> | |||
| </div> | |||
| </div> | |||
| <div className='flex'> | |||
| <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('appLog.agentLogDetail.toolUsed')}</div> | |||
| <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'> | |||
| <span>{tools?.length ? tools?.join(', ') : 'Null'}</span> | |||
| </div> | |||
| </div> | |||
| <div className='flex'> | |||
| <div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('appLog.agentLogDetail.iterations')}</div> | |||
| <div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'> | |||
| <span>{iterations}</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default ResultPanel | |||
| @@ -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<Props> = ({ toolCall, isLLM = false, isFinal, tokens, observation, finalAnswer }) => { | |||
| const [collapseState, setCollapseState] = useState<boolean>(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 ( | |||
| <div className={cn('py-1')}> | |||
| <div className={cn('group transition-all bg-white border border-gray-100 rounded-2xl shadow-xs hover:shadow-md')}> | |||
| <div | |||
| className={cn( | |||
| 'flex items-center py-3 pl-[6px] pr-3 cursor-pointer', | |||
| !collapseState && '!pb-2', | |||
| )} | |||
| onClick={() => setCollapseState(!collapseState)} | |||
| > | |||
| <ChevronRight | |||
| className={cn( | |||
| 'shrink-0 w-3 h-3 mr-1 text-gray-400 transition-all group-hover:text-gray-500', | |||
| !collapseState && 'rotate-90', | |||
| )} | |||
| /> | |||
| <BlockIcon className={cn('shrink-0 mr-2')} type={isLLM ? BlockEnum.LLM : BlockEnum.Tool} toolIcon={toolCall.tool_icon} /> | |||
| <div className={cn( | |||
| 'grow text-gray-700 text-[13px] leading-[16px] font-semibold truncate', | |||
| )} title={toolName}>{toolName}</div> | |||
| <div className='shrink-0 text-gray-500 text-xs leading-[18px]'> | |||
| {toolCall.time_cost && ( | |||
| <span>{getTime(toolCall.time_cost || 0)}</span> | |||
| )} | |||
| {isLLM && ( | |||
| <span>{`${getTokenCount(tokens || 0)} tokens`}</span> | |||
| )} | |||
| </div> | |||
| {toolCall.status === 'success' && ( | |||
| <CheckCircle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#12B76A]' /> | |||
| )} | |||
| {toolCall.status === 'error' && ( | |||
| <AlertCircle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F04438]' /> | |||
| )} | |||
| </div> | |||
| {!collapseState && ( | |||
| <div className='pb-2'> | |||
| <div className={cn('px-[10px] py-1')}> | |||
| {toolCall.status === 'error' && ( | |||
| <div className='px-3 py-[10px] bg-[#fef3f2] rounded-lg border-[0.5px] border-[rbga(0,0,0,0.05)] text-xs leading-[18px] text-[#d92d20] shadow-xs'>{toolCall.error}</div> | |||
| )} | |||
| </div> | |||
| {toolCall.tool_input && ( | |||
| <div className={cn('px-[10px] py-1')}> | |||
| <CodeEditor | |||
| readOnly | |||
| title={<div>INPUT</div>} | |||
| language={CodeLanguage.json} | |||
| value={toolCall.tool_input} | |||
| isJSONStringifyBeauty | |||
| /> | |||
| </div> | |||
| )} | |||
| {toolCall.tool_output && ( | |||
| <div className={cn('px-[10px] py-1')}> | |||
| <CodeEditor | |||
| readOnly | |||
| title={<div>OUTPUT</div>} | |||
| language={CodeLanguage.json} | |||
| value={toolCall.tool_output} | |||
| isJSONStringifyBeauty | |||
| /> | |||
| </div> | |||
| )} | |||
| {isLLM && ( | |||
| <div className={cn('px-[10px] py-1')}> | |||
| <CodeEditor | |||
| readOnly | |||
| title={<div>OBSERVATION</div>} | |||
| language={CodeLanguage.json} | |||
| value={observation} | |||
| isJSONStringifyBeauty | |||
| /> | |||
| </div> | |||
| )} | |||
| {isLLM && ( | |||
| <div className={cn('px-[10px] py-1')}> | |||
| <CodeEditor | |||
| readOnly | |||
| title={<div>{isFinal ? 'FINAL ANSWER' : 'THOUGHT'}</div>} | |||
| language={CodeLanguage.json} | |||
| value={finalAnswer} | |||
| isJSONStringifyBeauty | |||
| /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default ToolCallItem | |||
| @@ -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<TracingPanelProps> = ({ list }) => { | |||
| return ( | |||
| <div className='bg-gray-50'> | |||
| {list.map((iteration, index) => ( | |||
| <Iteration | |||
| key={index} | |||
| index={index + 1} | |||
| isFinal={index + 1 === list.length} | |||
| iterationInfo={iteration} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ) | |||
| } | |||
| export default TracingPanel | |||
| @@ -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, | |||
| }, | |||
| } | |||
| } | |||
| }) | |||
| @@ -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<ChatProps> = ({ | |||
| 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<HTMLDivElement>(null) | |||
| const chatContainerInnerRef = useRef<HTMLDivElement>(null) | |||
| @@ -259,6 +260,16 @@ const Chat: FC<ChatProps> = ({ | |||
| }} | |||
| /> | |||
| )} | |||
| {showAgentLogModal && ( | |||
| <AgentLogModal | |||
| width={width} | |||
| currentLogItem={currentLogItem} | |||
| onCancel={() => { | |||
| setCurrentLogItem() | |||
| setShowAgentLogModal(false) | |||
| }} | |||
| /> | |||
| )} | |||
| </div> | |||
| </ChatContextProvider> | |||
| ) | |||
| @@ -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 | |||
| @@ -39,12 +39,12 @@ const MessageLogModal: FC<MessageLogModalProps> = ({ | |||
| <div | |||
| className={cn('relative flex flex-col py-3 bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl z-10')} | |||
| style={{ | |||
| width, | |||
| width: fixedWidth ? width : 480, | |||
| ...(!fixedWidth | |||
| ? { | |||
| position: 'fixed', | |||
| top: 56 + 8, | |||
| left: 8, | |||
| left: 8 + (width - 480), | |||
| bottom: 16, | |||
| } | |||
| : { | |||
| @@ -34,7 +34,13 @@ const PromptLogModal: FC<PromptLogModalProps> = ({ | |||
| return ( | |||
| <div | |||
| className='fixed top-16 left-2 bottom-2 flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl z-10' | |||
| style={{ width }} | |||
| style={{ | |||
| width: 480, | |||
| position: 'fixed', | |||
| top: 56 + 8, | |||
| left: 8 + (width - 480), | |||
| bottom: 16, | |||
| }} | |||
| ref={ref} | |||
| > | |||
| <div className='shrink-0 flex justify-between items-center pl-6 pr-5 h-14 border-b border-b-gray-100'> | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -77,7 +77,15 @@ const translation = { | |||
| workflowTitle: 'ログの詳細', | |||
| }, | |||
| promptLog: 'プロンプトログ', | |||
| agentLog: 'エージェントログ', | |||
| viewLog: 'ログを表示', | |||
| agentLogDetail: { | |||
| agentMode: 'エージェントモード', | |||
| toolUsed: '使用したツール', | |||
| iterations: '反復', | |||
| iteration: '反復', | |||
| finalProcessing: '最終処理', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -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 | |||
| @@ -77,7 +77,15 @@ const translation = { | |||
| workflowTitle: 'Деталі Журналу', | |||
| }, | |||
| promptLog: 'Журнал Запитань', | |||
| viewLog: 'Переглянути Журнал', | |||
| agentLog: 'Журнал агента', | |||
| viewLog: 'Переглянути журнал', | |||
| agentLogDetail: { | |||
| agentMode: 'Режим агента', | |||
| toolUsed: 'Використаний інструмент', | |||
| iterations: 'Ітерації', | |||
| iteration: 'Ітерація', | |||
| finalProcessing: 'Остаточна обробка', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -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 | |||
| @@ -77,7 +77,15 @@ const translation = { | |||
| workflowTitle: '日志详情', | |||
| }, | |||
| promptLog: 'Prompt 日志', | |||
| agentLog: 'Agent 日志', | |||
| viewLog: '查看日志', | |||
| agentLogDetail: { | |||
| agentMode: 'Agent 模式', | |||
| toolUsed: '使用工具', | |||
| iterations: '迭代次数', | |||
| iteration: '迭代', | |||
| finalProcessing: '最终处理', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -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[] | |||
| } | |||
| @@ -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<NodeTracingListResponse, { url: string }> = ({ url }) => { | |||
| return get<NodeTracingListResponse>(url) | |||
| } | |||
| export const fetchAgentLogDetail = ({ appID, params }: { appID: string; params: AgentLogDetailRequest }) => { | |||
| return get<AgentLogDetailResponse>(`/apps/${appID}/agent/logs`, { params }) | |||
| } | |||