Browse Source

feat: agent log (#3537)

Co-authored-by: jyong <718720800@qq.com>
KVOJJJin 1 year ago
parent
commit
e70482dfc0

+ 6 - 3
web/app/components/app/chat/log/index.tsx

@@ -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>
   )
 }

+ 3 - 0
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 = {

+ 1 - 1
web/app/components/app/configuration/debug/index.tsx

@@ -473,7 +473,7 @@ const Debug: FC<IDebug> = ({
                 )}
               </div>
             )}
-            {showPromptLogModal && (
+            {mode === AppType.completion && showPromptLogModal && (
               <PromptLogModal
                 width={width}
                 currentLogItem={currentLogItem}

+ 20 - 4
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<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,

+ 4 - 0
web/app/components/app/store.ts

@@ -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 })),
 }))

+ 132 - 0
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<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

+ 61 - 0
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<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

+ 50 - 0
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<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

+ 126 - 0
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<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

+ 140 - 0
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<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

+ 25 - 0
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<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

+ 7 - 0
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,
+                  },
                 }
               }
             })

+ 12 - 1
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<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>
   )

+ 1 - 0
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

+ 2 - 2
web/app/components/base/message-log-modal/index.tsx

@@ -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,
           }
           : {

+ 7 - 1
web/app/components/base/prompt-log-modal/index.tsx

@@ -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'>

+ 16 - 0
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

+ 8 - 0
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

+ 8 - 0
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

+ 8 - 0
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

+ 8 - 0
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

+ 9 - 1
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

+ 9 - 1
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

+ 8 - 0
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

+ 55 - 0
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[]
+}

+ 6 - 0
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<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 })
+}