mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
refactor(web): migrate app log messages and agent logs to the console contract
Chat messages, feedback, log annotations and agent log detail now call consoleClient directly. The log chat formatter and agent log modal read the generated response types; untyped JSON fields are narrowed where they enter the chat UI types. The agent log response schema now matches what the service returns: `tool_label` is a name or i18n labels, `tool_icon` a URL or emoji icon. The never-sent `meta.error` is no longer read, and a missing annotation account no longer crashes the log detail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bfe2915439
commit
be52167547
@@ -48,11 +48,11 @@ class AgentToolCallResponse(ResponseModel):
|
||||
error: str | None = None
|
||||
time_cost: float | int
|
||||
tool_name: str
|
||||
tool_label: str
|
||||
tool_label: str | dict[str, str] = Field(description="Tool name or its i18n labels keyed by locale")
|
||||
tool_input: dict[str, Any]
|
||||
tool_output: dict[str, Any]
|
||||
tool_parameters: dict[str, Any]
|
||||
tool_icon: Any = Field(default=None)
|
||||
tool_icon: str | dict[str, str] | None = Field(default=None, description="Icon URL or emoji icon")
|
||||
|
||||
|
||||
class AgentIterationLogResponse(ResponseModel):
|
||||
|
||||
@@ -441,11 +441,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/app/log/__tests__/list-utils.spec.ts": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/app/log/__tests__/list.spec.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 9
|
||||
@@ -3991,11 +3986,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/models/log.ts": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"web/models/pipeline.ts": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 6
|
||||
|
||||
@@ -2762,11 +2762,20 @@ export type AgentToolCallResponse = {
|
||||
error?: string | null
|
||||
status: string
|
||||
time_cost: number | number
|
||||
tool_icon?: unknown
|
||||
tool_icon?:
|
||||
| string
|
||||
| {
|
||||
[key: string]: string
|
||||
}
|
||||
| null
|
||||
tool_input: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
tool_label: string
|
||||
tool_label:
|
||||
| string
|
||||
| {
|
||||
[key: string]: string
|
||||
}
|
||||
tool_name: string
|
||||
tool_output: {
|
||||
[key: string]: unknown
|
||||
|
||||
@@ -2846,9 +2846,9 @@ export const zAgentToolCallResponse = z.object({
|
||||
error: z.string().nullish(),
|
||||
status: z.string(),
|
||||
time_cost: z.union([z.number(), z.int()]),
|
||||
tool_icon: z.unknown().optional(),
|
||||
tool_icon: z.union([z.string(), z.record(z.string(), z.string())]).nullish(),
|
||||
tool_input: z.record(z.string(), z.unknown()),
|
||||
tool_label: z.string(),
|
||||
tool_label: z.union([z.string(), z.record(z.string(), z.string())]),
|
||||
tool_name: z.string(),
|
||||
tool_output: z.record(z.string(), z.unknown()),
|
||||
tool_parameters: z.record(z.string(), z.unknown()),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MessageDetailResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import {
|
||||
applyAnnotationAdded,
|
||||
@@ -22,11 +23,30 @@ const createChatItems = (): IChatItem[] =>
|
||||
{ id: 'answer-2', content: 'reply', isAnswer: true, parentMessageId: 'question-2' },
|
||||
] as IChatItem[]
|
||||
|
||||
const createMessage = (overrides: Partial<MessageDetailResponse>): MessageDetailResponse => ({
|
||||
id: 'message',
|
||||
conversation_id: 'conversation',
|
||||
inputs: {},
|
||||
query: '',
|
||||
answer: '',
|
||||
answer_tokens: 0,
|
||||
message_tokens: 0,
|
||||
message: [],
|
||||
message_files: [],
|
||||
agent_thoughts: [],
|
||||
feedbacks: [],
|
||||
metadata: {},
|
||||
from_source: 'console',
|
||||
provider_response_latency: 0,
|
||||
status: 'normal',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('log list utils', () => {
|
||||
it('should format chat messages into paired question and answer items', () => {
|
||||
const items = getFormattedChatList(
|
||||
[
|
||||
{
|
||||
createMessage({
|
||||
id: 'message-1',
|
||||
inputs: { query: 'hello' },
|
||||
query: 'hello',
|
||||
@@ -36,11 +56,23 @@ describe('log list utils', () => {
|
||||
message_tokens: 2,
|
||||
message: [{ role: 'user', text: 'hello' }],
|
||||
message_files: [
|
||||
{ belongs_to: 'assistant', id: 'file-1' },
|
||||
{ belongs_to: 'user', id: 'file-2' },
|
||||
{
|
||||
belongs_to: 'assistant',
|
||||
id: 'file-1',
|
||||
filename: '',
|
||||
transfer_method: 'local_file',
|
||||
type: 'image',
|
||||
},
|
||||
{
|
||||
belongs_to: 'user',
|
||||
id: 'file-2',
|
||||
filename: '',
|
||||
transfer_method: 'local_file',
|
||||
type: 'image',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any,
|
||||
}),
|
||||
],
|
||||
'conversation-1',
|
||||
'UTC',
|
||||
'YYYY-MM-DD',
|
||||
@@ -67,7 +99,7 @@ describe('log list utils', () => {
|
||||
it('should preserve feedback and annotation hit history when formatting chat items', () => {
|
||||
const items = getFormattedChatList(
|
||||
[
|
||||
{
|
||||
createMessage({
|
||||
id: 'message-2',
|
||||
inputs: { default_input: 'fallback prompt' },
|
||||
query: '',
|
||||
@@ -82,11 +114,11 @@ describe('log list utils', () => {
|
||||
],
|
||||
annotation_hit_history: {
|
||||
annotation_id: 'annotation-1',
|
||||
annotation_create_account: {},
|
||||
annotation_create_account: null,
|
||||
created_at: 123,
|
||||
},
|
||||
},
|
||||
] as any,
|
||||
}),
|
||||
],
|
||||
'conversation-2',
|
||||
'UTC',
|
||||
'YYYY-MM-DD',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MessageDetailResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
@@ -50,18 +51,15 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/log', () => ({
|
||||
fetchChatMessages: (...args: unknown[]) => mockFetchChatMessages(...args),
|
||||
updateLogMessageFeedbacks: (...args: unknown[]) => mockUpdateLogMessageFeedbacks(...args),
|
||||
updateLogMessageAnnotations: (...args: unknown[]) => mockUpdateLogMessageAnnotations(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/console')>()),
|
||||
consoleClient: {
|
||||
apps: {
|
||||
byAppId: {
|
||||
chatMessages: { get: (...args: unknown[]) => mockFetchChatMessages(...args) },
|
||||
feedbacks: { post: (...args: unknown[]) => mockUpdateLogMessageFeedbacks(...args) },
|
||||
annotations: {
|
||||
post: (...args: unknown[]) => mockUpdateLogMessageAnnotations(...args),
|
||||
byAnnotationId: { delete: (...args: unknown[]) => mockDelAnnotation(...args) },
|
||||
},
|
||||
},
|
||||
@@ -302,8 +300,17 @@ const createCompletionLogs = () => ({
|
||||
],
|
||||
})
|
||||
|
||||
const createChatMessage = (id: string, overrides: Record<string, unknown> = {}) => ({
|
||||
const createChatMessage = (
|
||||
id: string,
|
||||
overrides: Partial<MessageDetailResponse> = {},
|
||||
): MessageDetailResponse => ({
|
||||
id,
|
||||
conversation_id: 'conversation-1',
|
||||
agent_thoughts: [],
|
||||
metadata: {},
|
||||
from_source: 'console',
|
||||
provider_response_latency: 0,
|
||||
status: 'normal',
|
||||
answer: `Assistant reply ${id}`,
|
||||
query: `Question ${id}`,
|
||||
created_at: 1710000000,
|
||||
@@ -315,7 +322,7 @@ const createChatMessage = (id: string, overrides: Record<string, unknown> = {})
|
||||
message_files: [],
|
||||
answer_tokens: 10,
|
||||
message_tokens: 5,
|
||||
parent_message_id: undefined,
|
||||
parent_message_id: null,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
@@ -498,11 +505,8 @@ describe('ConversationList', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchChatMessages).toHaveBeenCalledWith({
|
||||
url: '/apps/app-1/chat-messages',
|
||||
params: {
|
||||
conversation_id: 'conversation-1',
|
||||
limit: 10,
|
||||
},
|
||||
params: { app_id: 'app-1' },
|
||||
query: { conversation_id: 'conversation-1', limit: 10 },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -517,7 +521,7 @@ describe('ConversationList', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateLogMessageFeedbacks).toHaveBeenCalledWith({
|
||||
url: '/apps/app-1/feedbacks',
|
||||
params: { app_id: 'app-1' },
|
||||
body: {
|
||||
message_id: 'message-1',
|
||||
rating: 'like',
|
||||
@@ -602,7 +606,7 @@ describe('ConversationList', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateLogMessageFeedbacks).toHaveBeenCalledWith({
|
||||
url: '/apps/app-1/feedbacks',
|
||||
params: { app_id: 'app-1' },
|
||||
body: {
|
||||
message_id: 'message-1',
|
||||
rating: 'like',
|
||||
@@ -706,7 +710,7 @@ describe('ConversationList', () => {
|
||||
annotation: {
|
||||
id: 'annotation-1',
|
||||
content: 'Annotated answer',
|
||||
account: { name: 'Admin' },
|
||||
account: { id: 'account-1', name: 'Admin', email: 'admin@example.com' },
|
||||
},
|
||||
}),
|
||||
createChatMessage('message-2', { parent_message_id: 'message-1' }),
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { MessageDetailResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { ChatItemInTree } from '../../base/chat/types'
|
||||
import type { FeedbackType, IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { Annotation, ChatMessage } from '@/models/log'
|
||||
import type {
|
||||
CitationItem,
|
||||
FeedbackType,
|
||||
IChatItem,
|
||||
ThoughtItem,
|
||||
} from '@/app/components/base/chat/chat/type'
|
||||
import type { Annotation } from '@/models/log'
|
||||
import type { FileResponse } from '@/types/workflow'
|
||||
import dayjs from 'dayjs'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
@@ -10,6 +16,7 @@ import { isPlainObject } from 'es-toolkit/predicate'
|
||||
import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
@@ -41,94 +48,150 @@ const getUserInputVariable = (item: unknown) => {
|
||||
return typeof variable === 'string' ? variable : undefined
|
||||
}
|
||||
|
||||
const toFileResponse = (file: NonNullable<ChatMessage['message_files']>[number]): FileResponse => ({
|
||||
type LogMessage = MessageDetailResponse
|
||||
|
||||
const isTransferMethod = (value: string): value is TransferMethod =>
|
||||
(Object.values(TransferMethod) as string[]).includes(value)
|
||||
|
||||
const toFileResponse = (file: LogMessage['message_files'][number]): FileResponse => ({
|
||||
related_id: file.id ?? file.upload_file_id,
|
||||
extension: '',
|
||||
filename: '',
|
||||
size: 0,
|
||||
mime_type: '',
|
||||
transfer_method: file.transfer_method,
|
||||
transfer_method: isTransferMethod(file.transfer_method)
|
||||
? file.transfer_method
|
||||
: TransferMethod.local_file,
|
||||
type: file.type,
|
||||
url: file.url,
|
||||
upload_file_id: file.upload_file_id,
|
||||
remote_url: file.url,
|
||||
url: file.url ?? '',
|
||||
upload_file_id: file.upload_file_id ?? '',
|
||||
remote_url: file.url ?? '',
|
||||
})
|
||||
|
||||
const toText = (value: unknown) => (typeof value === 'string' && value ? value : undefined)
|
||||
|
||||
// `inputs` values are untyped JSON in the console API response.
|
||||
export const getStringInputs = (inputs: LogMessage['inputs']) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(inputs).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
||||
),
|
||||
)
|
||||
|
||||
const toFeedback = (feedback?: LogMessage['feedbacks'][number]): FeedbackType | undefined =>
|
||||
feedback?.rating === 'like' || feedback?.rating === 'dislike'
|
||||
? { rating: feedback.rating, content: feedback.content }
|
||||
: undefined
|
||||
|
||||
const toThoughtItems = (message: LogMessage): ThoughtItem[] =>
|
||||
message.agent_thoughts.map((thought) => ({
|
||||
id: thought.id,
|
||||
tool: thought.tool ?? '',
|
||||
thought: thought.thought ?? '',
|
||||
answer: thought.answer ?? undefined,
|
||||
tool_input: thought.tool_input ?? '',
|
||||
// `tool_labels` is untyped JSON in the console API response.
|
||||
tool_labels: isPlainObject(thought.tool_labels)
|
||||
? (thought.tool_labels as ThoughtItem['tool_labels'])
|
||||
: undefined,
|
||||
message_id: thought.message_id,
|
||||
conversation_id: message.conversation_id,
|
||||
observation: thought.observation ?? '',
|
||||
position: thought.position,
|
||||
files: thought.files,
|
||||
}))
|
||||
|
||||
// `message` and `metadata` are untyped JSON in the console API response.
|
||||
const getPromptLog = (message: LogMessage) =>
|
||||
(Array.isArray(message.message) ? message.message : []) as NonNullable<IChatItem['log']>
|
||||
|
||||
const getCitations = (message: LogMessage) =>
|
||||
isPlainObject(message.metadata) && Array.isArray(message.metadata.retriever_resources)
|
||||
? (message.metadata.retriever_resources as CitationItem[])
|
||||
: undefined
|
||||
|
||||
const getAnnotation = (message: LogMessage): Annotation | undefined => {
|
||||
if (message.annotation_hit_history) {
|
||||
return {
|
||||
id: message.annotation_hit_history.annotation_id,
|
||||
authorName: message.annotation_hit_history.annotation_create_account?.name || 'N/A',
|
||||
created_at: message.annotation_hit_history.created_at ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
if (message.annotation) {
|
||||
const { account } = message.annotation
|
||||
return {
|
||||
id: message.annotation.id,
|
||||
authorName: account?.name || 'N/A',
|
||||
logAnnotation: {
|
||||
id: message.annotation.id,
|
||||
content: message.annotation.content,
|
||||
account: { id: account?.id ?? '', name: account?.name ?? '', email: account?.email ?? '' },
|
||||
created_at: message.annotation.created_at ?? 0,
|
||||
},
|
||||
created_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const getFormattedChatList = (
|
||||
messages: ChatMessage[],
|
||||
messages: LogMessage[],
|
||||
conversationId: string,
|
||||
timezone: string,
|
||||
format: string,
|
||||
) => {
|
||||
const newChatList: IChatItem[] = []
|
||||
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
const questionFiles = item.message_files?.filter((file) => file.belongs_to === 'user') || []
|
||||
messages.forEach((item) => {
|
||||
const files = getProcessedFilesFromResponse(item.message_files.map(toFileResponse))
|
||||
const questionFiles = getProcessedFilesFromResponse(
|
||||
item.message_files.filter((file) => file.belongs_to === 'user').map(toFileResponse),
|
||||
)
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.inputs.query || item.inputs.default_input || item.query,
|
||||
content: toText(item.inputs.query) || toText(item.inputs.default_input) || item.query,
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map(toFileResponse)),
|
||||
message_files: questionFiles,
|
||||
parentMessageId: item.parent_message_id || undefined,
|
||||
})
|
||||
|
||||
const answerFiles = item.message_files?.filter((file) => file.belongs_to === 'assistant') || []
|
||||
const answerFiles = getProcessedFilesFromResponse(
|
||||
item.message_files.filter((file) => file.belongs_to === 'assistant').map(toFileResponse),
|
||||
)
|
||||
const promptLog = getPromptLog(item)
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(
|
||||
item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts,
|
||||
item.message_files,
|
||||
agent_thoughts: addFileInfos(sortAgentSorts(toThoughtItems(item)), files),
|
||||
feedback: toFeedback(item.feedbacks.find((feedback) => feedback.from_source === 'user')),
|
||||
adminFeedback: toFeedback(
|
||||
item.feedbacks.find((feedback) => feedback.from_source === 'admin'),
|
||||
),
|
||||
feedback: item.feedbacks?.find((feedback) => feedback.from_source === 'user'),
|
||||
adminFeedback: item.feedbacks?.find((feedback) => feedback.from_source === 'admin'),
|
||||
feedbackDisabled: false,
|
||||
isAnswer: true,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map(toFileResponse)),
|
||||
message_files: answerFiles,
|
||||
log: [
|
||||
...(item.message ?? []),
|
||||
...(item.message?.[item.message.length - 1]?.role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
text: item.answer,
|
||||
files: item.message_files?.filter((file) => file.belongs_to === 'assistant') || [],
|
||||
},
|
||||
]
|
||||
...promptLog,
|
||||
...(promptLog.at(-1)?.role !== 'assistant'
|
||||
? [{ role: 'assistant', text: item.answer, files: answerFiles }]
|
||||
: []),
|
||||
] as IChatItem['log'],
|
||||
workflow_run_id: item.workflow_run_id,
|
||||
],
|
||||
workflow_run_id: item.workflow_run_id ?? undefined,
|
||||
conversationId,
|
||||
input: {
|
||||
inputs: item.inputs,
|
||||
query: item.query,
|
||||
},
|
||||
more: {
|
||||
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
||||
time: item.created_at ? dayjs.unix(item.created_at).tz(timezone).format(format) : '',
|
||||
tokens: item.answer_tokens + item.message_tokens,
|
||||
latency: (item.provider_response_latency ?? 0).toFixed(2),
|
||||
},
|
||||
citation: item.metadata?.retriever_resources,
|
||||
annotation: (() => {
|
||||
if (item.annotation_hit_history) {
|
||||
return {
|
||||
id: item.annotation_hit_history.annotation_id,
|
||||
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
|
||||
created_at: item.annotation_hit_history.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
if (item.annotation) {
|
||||
return {
|
||||
id: item.annotation.id,
|
||||
authorName: item.annotation.account.name,
|
||||
logAnnotation: item.annotation,
|
||||
created_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
})(),
|
||||
citation: getCitations(item),
|
||||
annotation: getAnnotation(item),
|
||||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ConversationPagination,
|
||||
ConversationWithSummary,
|
||||
ConversationWithSummaryPagination,
|
||||
GetAppsByAppIdChatMessagesData,
|
||||
} from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { FC } from 'react'
|
||||
import type { ChatItemInTree } from '../../base/chat/types'
|
||||
@@ -17,7 +18,7 @@ import type {
|
||||
IChatItem,
|
||||
SubmitAnnotationFunc,
|
||||
} from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatMessagesRequest, LogAnnotation } from '@/models/log'
|
||||
import type { LogAnnotation } from '@/models/log'
|
||||
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
@@ -56,11 +57,6 @@ import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import {
|
||||
fetchChatMessages,
|
||||
updateLogMessageAnnotations,
|
||||
updateLogMessageFeedbacks,
|
||||
} from '@/service/log'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import PromptLogModal from '../../base/prompt-log-modal'
|
||||
@@ -75,6 +71,7 @@ import {
|
||||
getDetailVarList,
|
||||
getFormattedChatList,
|
||||
getLogModelInfo,
|
||||
getStringInputs,
|
||||
getThreadChatItems,
|
||||
isNearTopLoadMore,
|
||||
mergePaginatedChatItems,
|
||||
@@ -205,22 +202,22 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
try {
|
||||
isLoadingRef.current = true
|
||||
|
||||
const params: ChatMessagesRequest = {
|
||||
const params: GetAppsByAppIdChatMessagesData['query'] = {
|
||||
conversation_id: detail.id,
|
||||
limit: 10,
|
||||
}
|
||||
// Use ref for pagination anchor to avoid stale closure issues
|
||||
if (oldestAnswerIdRef.current) params.first_id = oldestAnswerIdRef.current
|
||||
|
||||
const messageRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail.id}/chat-messages`,
|
||||
params,
|
||||
const messageRes = await consoleClient.apps.byAppId.chatMessages.get({
|
||||
params: { app_id: appDetail.id },
|
||||
query: params,
|
||||
})
|
||||
|
||||
// Ignore stale responses
|
||||
if (currentRequestId !== requestIdRef.current || controller.signal.aborted) return
|
||||
if (messageRes.data.length > 0) {
|
||||
const varValues = messageRes.data.at(-1)!.inputs
|
||||
const varValues = getStringInputs(messageRes.data.at(-1)!.inputs)
|
||||
setVarValues(varValues)
|
||||
}
|
||||
setHasMore(messageRes.has_more)
|
||||
@@ -331,7 +328,7 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const params: ChatMessagesRequest = {
|
||||
const params: GetAppsByAppIdChatMessagesData['query'] = {
|
||||
conversation_id: detail.id,
|
||||
limit: 10,
|
||||
}
|
||||
@@ -341,9 +338,9 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
params.first_id = oldestAnswerIdRef.current
|
||||
}
|
||||
|
||||
const messageRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail.id}/chat-messages`,
|
||||
params,
|
||||
const messageRes = await consoleClient.apps.byAppId.chatMessages.get({
|
||||
params: { app_id: appDetail.id },
|
||||
query: params,
|
||||
})
|
||||
|
||||
if (!messageRes.data || messageRes.data.length === 0) {
|
||||
@@ -353,7 +350,7 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
|
||||
}
|
||||
|
||||
if (messageRes.data.length > 0) {
|
||||
const varValues = messageRes.data.at(-1)!.inputs
|
||||
const varValues = getStringInputs(messageRes.data.at(-1)!.inputs)
|
||||
setVarValues(varValues)
|
||||
}
|
||||
|
||||
@@ -652,8 +649,8 @@ const CompletionConversationDetailComp: FC<ConversationDetailProps> = ({
|
||||
const { t } = useTranslation(['appLog', 'common'])
|
||||
|
||||
const handleFeedback = async (mid: string, { rating, content }: FeedbackType) => {
|
||||
await updateLogMessageFeedbacks({
|
||||
url: `/apps/${appDetail.id}/feedbacks`,
|
||||
await consoleClient.apps.byAppId.feedbacks.post({
|
||||
params: { app_id: appDetail.id },
|
||||
body: { message_id: mid, rating, content: content ?? undefined },
|
||||
})
|
||||
conversationDetailMutate()
|
||||
@@ -661,8 +658,8 @@ const CompletionConversationDetailComp: FC<ConversationDetailProps> = ({
|
||||
|
||||
const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
|
||||
try {
|
||||
await updateLogMessageAnnotations({
|
||||
url: `/apps/${appDetail.id}/annotations`,
|
||||
await consoleClient.apps.byAppId.annotations.post({
|
||||
params: { app_id: appDetail.id },
|
||||
body: { message_id: mid, content: value },
|
||||
})
|
||||
conversationDetailMutate()
|
||||
@@ -712,16 +709,16 @@ const ChatConversationDetailComp: FC<ConversationDetailProps> = ({
|
||||
const { t } = useTranslation(['appLog', 'common'])
|
||||
|
||||
const handleFeedback = async (mid: string, { rating, content }: FeedbackType) => {
|
||||
await updateLogMessageFeedbacks({
|
||||
url: `/apps/${appDetail.id}/feedbacks`,
|
||||
await consoleClient.apps.byAppId.feedbacks.post({
|
||||
params: { app_id: appDetail.id },
|
||||
body: { message_id: mid, rating, content: content ?? undefined },
|
||||
})
|
||||
}
|
||||
|
||||
const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
|
||||
try {
|
||||
await updateLogMessageAnnotations({
|
||||
url: `/apps/${appDetail.id}/annotations`,
|
||||
await consoleClient.apps.byAppId.annotations.post({
|
||||
params: { app_id: appDetail.id },
|
||||
body: { message_id: mid, content: value },
|
||||
})
|
||||
toast.success(t(($) => $['actionMsg.modifiedSuccessfully'], { ns: 'common' }))
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentLogDetailResponse } from '@/models/log'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { createHttpError } from '@/service/http-error'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import AgentLogDetail from '../detail'
|
||||
|
||||
const { mockToast } = vi.hoisted(() => {
|
||||
@@ -19,10 +18,14 @@ const { mockToast } = vi.hoisted(() => {
|
||||
return { mockToast }
|
||||
})
|
||||
|
||||
vi.mock('@/service/log', () => ({
|
||||
const { fetchAgentLogDetail } = vi.hoisted(() => ({
|
||||
fetchAgentLogDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleClient: { apps: { byAppId: { agent: { logs: { get: fetchAgentLogDetail } } } } },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
@@ -100,6 +103,10 @@ const createMockResponse = (
|
||||
tool_name: 'tool1',
|
||||
status: 'success',
|
||||
tool_icon: null,
|
||||
time_cost: 0,
|
||||
tool_input: {},
|
||||
tool_output: {},
|
||||
tool_parameters: {},
|
||||
tool_label: { 'en-US': 'Tool 1' },
|
||||
},
|
||||
],
|
||||
@@ -136,7 +143,7 @@ describe('AgentLogDetail', () => {
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should show loading indicator while fetching data', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
fetchAgentLogDetail.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
renderComponent()
|
||||
|
||||
@@ -144,7 +151,7 @@ describe('AgentLogDetail', () => {
|
||||
})
|
||||
|
||||
it('should display result panel after data loads', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
fetchAgentLogDetail.mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
@@ -153,23 +160,20 @@ describe('AgentLogDetail', () => {
|
||||
})
|
||||
|
||||
it('should call fetchAgentLogDetail with correct params', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
fetchAgentLogDetail.mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
expect(fetchAgentLogDetail).toHaveBeenCalledWith({
|
||||
appID: 'app-id',
|
||||
params: {
|
||||
conversation_id: 'conv-id',
|
||||
message_id: 'msg-id',
|
||||
},
|
||||
params: { app_id: 'app-id' },
|
||||
query: { conversation_id: 'conv-id', message_id: 'msg-id' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should default to DETAIL tab when activeTab is not provided', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
fetchAgentLogDetail.mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
@@ -178,7 +182,7 @@ describe('AgentLogDetail', () => {
|
||||
})
|
||||
|
||||
it('should show TRACING tab when activeTab is TRACING', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
fetchAgentLogDetail.mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData({ activeTab: 'TRACING' })
|
||||
|
||||
@@ -189,7 +193,7 @@ describe('AgentLogDetail', () => {
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should switch to TRACING tab when clicked', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
fetchAgentLogDetail.mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
@@ -205,7 +209,7 @@ describe('AgentLogDetail', () => {
|
||||
})
|
||||
|
||||
it('should switch back to DETAIL tab after switching to TRACING', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse())
|
||||
fetchAgentLogDetail.mockResolvedValue(createMockResponse())
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
@@ -226,7 +230,7 @@ describe('AgentLogDetail', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should notify on API error', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockRejectedValue(
|
||||
fetchAgentLogDetail.mockRejectedValue(
|
||||
createHttpError(new Response(null, { status: 500 }), { message: 'API Error' }),
|
||||
)
|
||||
|
||||
@@ -238,7 +242,7 @@ describe('AgentLogDetail', () => {
|
||||
})
|
||||
|
||||
it('should stop loading after API error', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockRejectedValue(new Error('Network failure'))
|
||||
fetchAgentLogDetail.mockRejectedValue(new Error('Network failure'))
|
||||
|
||||
renderComponent()
|
||||
|
||||
@@ -248,7 +252,7 @@ describe('AgentLogDetail', () => {
|
||||
})
|
||||
|
||||
it('should handle response with empty iterations', async () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(createMockResponse({ iterations: [] }))
|
||||
fetchAgentLogDetail.mockResolvedValue(createMockResponse({ iterations: [] }))
|
||||
|
||||
await renderAndWaitForData()
|
||||
})
|
||||
@@ -267,12 +271,20 @@ describe('AgentLogDetail', () => {
|
||||
tool_name: 'tool1',
|
||||
status: 'success',
|
||||
tool_icon: null,
|
||||
time_cost: 0,
|
||||
tool_input: {},
|
||||
tool_output: {},
|
||||
tool_parameters: {},
|
||||
tool_label: { 'en-US': 'Tool 1' },
|
||||
},
|
||||
{
|
||||
tool_name: 'tool2',
|
||||
status: 'success',
|
||||
tool_icon: null,
|
||||
time_cost: 0,
|
||||
tool_input: {},
|
||||
tool_output: {},
|
||||
tool_parameters: {},
|
||||
tool_label: { 'en-US': 'Tool 2' },
|
||||
},
|
||||
],
|
||||
@@ -288,13 +300,17 @@ describe('AgentLogDetail', () => {
|
||||
tool_name: 'tool1',
|
||||
status: 'success',
|
||||
tool_icon: null,
|
||||
time_cost: 0,
|
||||
tool_input: {},
|
||||
tool_output: {},
|
||||
tool_parameters: {},
|
||||
tool_label: { 'en-US': 'Tool 1' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue(response)
|
||||
fetchAgentLogDetail.mockResolvedValue(response)
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import AgentLogModal from '../index'
|
||||
|
||||
const { mockToast } = vi.hoisted(() => {
|
||||
@@ -17,10 +16,14 @@ const { mockToast } = vi.hoisted(() => {
|
||||
return { mockToast }
|
||||
})
|
||||
|
||||
vi.mock('@/service/log', () => ({
|
||||
const { fetchAgentLogDetail } = vi.hoisted(() => ({
|
||||
fetchAgentLogDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleClient: { apps: { byAppId: { agent: { logs: { get: fetchAgentLogDetail } } } } },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
toast: mockToast,
|
||||
}))
|
||||
@@ -86,7 +89,7 @@ const mockProps = {
|
||||
describe('AgentLogModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(fetchAgentLogDetail).mockResolvedValue({
|
||||
fetchAgentLogDetail.mockResolvedValue({
|
||||
meta: {
|
||||
status: 'succeeded',
|
||||
executor: 'User',
|
||||
@@ -146,7 +149,7 @@ describe('AgentLogModal', () => {
|
||||
})
|
||||
|
||||
it('should render the floating modal through a dialog portal', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
fetchAgentLogDetail.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
const { container } = render(<AgentLogModal appId="app-id" {...mockProps} floating />)
|
||||
|
||||
@@ -157,7 +160,7 @@ describe('AgentLogModal', () => {
|
||||
})
|
||||
|
||||
it('should call onCancel when close button is clicked', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
fetchAgentLogDetail.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
render(<AgentLogModal appId="app-id" {...mockProps} />)
|
||||
|
||||
@@ -168,7 +171,7 @@ describe('AgentLogModal', () => {
|
||||
})
|
||||
|
||||
it('should call onCancel when clicking away', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
fetchAgentLogDetail.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
let clickAwayHandler!: (event: Event) => void
|
||||
vi.mocked(useClickAway).mockImplementation((callback) => {
|
||||
@@ -182,7 +185,7 @@ describe('AgentLogModal', () => {
|
||||
})
|
||||
|
||||
it('should ignore click-away before mounted state is set', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
fetchAgentLogDetail.mockReturnValue(new Promise(() => {}))
|
||||
let invoked = false
|
||||
vi.mocked(useClickAway).mockImplementation((callback) => {
|
||||
if (!invoked) {
|
||||
@@ -197,7 +200,7 @@ describe('AgentLogModal', () => {
|
||||
})
|
||||
|
||||
it('should not use click-away to close the floating dialog', () => {
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {}))
|
||||
fetchAgentLogDetail.mockReturnValue(new Promise(() => {}))
|
||||
|
||||
let clickAwayHandler!: (event: Event) => void
|
||||
vi.mocked(useClickAway).mockImplementation((callback) => {
|
||||
@@ -228,15 +231,13 @@ describe('AgentLogModal', () => {
|
||||
const pending = new Promise<Awaited<ReturnType<typeof fetchAgentLogDetail>>>((resolve) => {
|
||||
resolveOldResponse = resolve
|
||||
})
|
||||
vi.mocked(fetchAgentLogDetail)
|
||||
.mockResolvedValue(response)
|
||||
.mockImplementationOnce(() => pending)
|
||||
fetchAgentLogDetail.mockResolvedValue(response).mockImplementationOnce(() => pending)
|
||||
const { rerender } = render(<AgentLogModal appId="old-app" {...mockProps} />)
|
||||
rerender(<AgentLogModal appId="new-app" {...mockProps} />)
|
||||
await screen.findByText('New executor')
|
||||
expect(fetchAgentLogDetail).toHaveBeenLastCalledWith({
|
||||
appID: 'new-app',
|
||||
params: { conversation_id: mockLog.conversationId, message_id: mockLog.id },
|
||||
params: { app_id: 'new-app' },
|
||||
query: { conversation_id: mockLog.conversationId, message_id: mockLog.id },
|
||||
})
|
||||
await act(async () => {
|
||||
resolveOldResponse({ ...response, meta: { ...response.meta, executor: 'Old executor' } })
|
||||
@@ -263,8 +264,8 @@ describe('AgentLogModal', () => {
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
||||
await waitFor(() => expect(screen.queryByRole('progressbar')).not.toBeInTheDocument())
|
||||
expect(fetchAgentLogDetail).toHaveBeenLastCalledWith({
|
||||
appID: 'app-id',
|
||||
params: { conversation_id: mockLog.conversationId, message_id: 'next-message' },
|
||||
params: { app_id: 'app-id' },
|
||||
query: { conversation_id: mockLog.conversationId, message_id: 'next-message' },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -273,7 +274,7 @@ describe('AgentLogModal', () => {
|
||||
const pending = new Promise<Awaited<ReturnType<typeof fetchAgentLogDetail>>>((_, reject) => {
|
||||
rejectOldResponse = reject
|
||||
})
|
||||
vi.mocked(fetchAgentLogDetail).mockReturnValueOnce(pending)
|
||||
fetchAgentLogDetail.mockReturnValueOnce(pending)
|
||||
const { rerender } = render(<AgentLogModal appId="old-app" {...mockProps} />)
|
||||
rerender(<AgentLogModal appId="new-app" {...mockProps} />)
|
||||
await screen.findByText('User')
|
||||
|
||||
@@ -26,6 +26,10 @@ const mockIterationInfo: AgentIteration = {
|
||||
tool_name: 'test_tool',
|
||||
tool_label: { en: 'Test Tool', 'en-US': 'Test Tool' },
|
||||
tool_icon: null,
|
||||
time_cost: 0,
|
||||
tool_input: {},
|
||||
tool_output: {},
|
||||
tool_parameters: {},
|
||||
},
|
||||
],
|
||||
tool_raw: {
|
||||
|
||||
@@ -22,7 +22,16 @@ const createIteration = (thought: string, tokens: number): AgentIteration => ({
|
||||
thought,
|
||||
tokens,
|
||||
tool_calls: [
|
||||
{ tool_name: 'tool1', status: 'success', tool_icon: null, tool_label: { 'en-US': 'Tool 1' } },
|
||||
{
|
||||
tool_name: 'tool1',
|
||||
status: 'success',
|
||||
tool_icon: null,
|
||||
tool_label: { 'en-US': 'Tool 1' },
|
||||
time_cost: 0,
|
||||
tool_input: {},
|
||||
tool_output: {},
|
||||
tool_parameters: {},
|
||||
},
|
||||
],
|
||||
tool_raw: { inputs: '', outputs: '' },
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { fetchAgentLogDetail } from '@/service/log'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import ResultPanel from './result'
|
||||
import TracingPanel from './tracing'
|
||||
|
||||
@@ -94,21 +94,23 @@ function AgentLogContent({
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
fetchAgentLogDetail({
|
||||
appID: appId,
|
||||
params: { conversation_id: conversationID, message_id: messageID },
|
||||
}).then(
|
||||
(data) => {
|
||||
if (!active) return
|
||||
setRunDetail(data)
|
||||
setLoading(false)
|
||||
},
|
||||
(error) => {
|
||||
if (!active) return
|
||||
toast.error(error instanceof Error ? error.message : String(error))
|
||||
setLoading(false)
|
||||
},
|
||||
)
|
||||
consoleClient.apps.byAppId.agent.logs
|
||||
.get({
|
||||
params: { app_id: appId },
|
||||
query: { conversation_id: conversationID, message_id: messageID },
|
||||
})
|
||||
.then(
|
||||
(data) => {
|
||||
if (!active) return
|
||||
setRunDetail(data)
|
||||
setLoading(false)
|
||||
},
|
||||
(error) => {
|
||||
if (!active) return
|
||||
toast.error(error instanceof Error ? error.message : String(error))
|
||||
setLoading(false)
|
||||
},
|
||||
)
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
@@ -131,8 +133,7 @@ function AgentLogContent({
|
||||
inputs={log.input}
|
||||
outputs={log.content}
|
||||
status={runDetail.meta.status}
|
||||
error={runDetail.meta.error}
|
||||
elapsed_time={runDetail.meta.elapsed_time}
|
||||
elapsed_time={runDetail.meta.elapsed_time ?? undefined}
|
||||
total_tokens={runDetail.meta.total_tokens}
|
||||
created_at={runDetail.meta.start_time}
|
||||
created_by={runDetail.meta.executor}
|
||||
|
||||
@@ -14,7 +14,6 @@ const MOCK_RESPONSE: AgentLogDetailResponse = {
|
||||
total_tokens: 2589,
|
||||
agent_mode: 'ReACT',
|
||||
iterations: 2,
|
||||
error: undefined,
|
||||
},
|
||||
iterations: [
|
||||
{
|
||||
@@ -33,6 +32,7 @@ const MOCK_RESPONSE: AgentLogDetailResponse = {
|
||||
'en-US': 'Revenue Search',
|
||||
},
|
||||
time_cost: 1.8,
|
||||
tool_parameters: {},
|
||||
},
|
||||
],
|
||||
tool_raw: {
|
||||
|
||||
@@ -10,8 +10,15 @@ import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const getToolIcon = (icon: ToolCall['tool_icon']) => {
|
||||
if (typeof icon === 'string') return icon
|
||||
if (icon?.content !== undefined && icon.background !== undefined)
|
||||
return { content: icon.content, background: icon.background }
|
||||
return undefined
|
||||
}
|
||||
|
||||
type Props = Readonly<{
|
||||
toolCall: ToolCall
|
||||
toolCall: Partial<ToolCall> & Pick<ToolCall, 'status'>
|
||||
isLLM: boolean
|
||||
isFinal?: boolean
|
||||
tokens?: number
|
||||
@@ -31,7 +38,9 @@ const ToolCallItem: FC<Props> = ({
|
||||
const locale = useLocale()
|
||||
const toolName = isLLM
|
||||
? 'LLM'
|
||||
: toolCall.tool_label[locale] || toolCall.tool_label[locale.replaceAll('-', '_')]
|
||||
: typeof toolCall.tool_label === 'string'
|
||||
? toolCall.tool_label
|
||||
: toolCall.tool_label?.[locale] || toolCall.tool_label?.[locale.replaceAll('-', '_')]
|
||||
|
||||
const getTime = (time: number) => {
|
||||
if (time < 1) return `${(time * 1000).toFixed(3)} ms`
|
||||
@@ -73,7 +82,7 @@ const ToolCallItem: FC<Props> = ({
|
||||
<BlockIcon
|
||||
className={cn('mr-2 shrink-0')}
|
||||
type={isLLM ? BlockEnum.LLM : BlockEnum.Tool}
|
||||
toolIcon={toolCall.tool_icon}
|
||||
toolIcon={getToolIcon(toolCall.tool_icon)}
|
||||
/>
|
||||
<div
|
||||
className={cn('grow truncate text-[13px] leading-4 font-semibold text-text-secondary')}
|
||||
|
||||
+8
-116
@@ -1,7 +1,10 @@
|
||||
import type {
|
||||
AgentIterationLogResponse,
|
||||
AgentLogResponse,
|
||||
AgentToolCallResponse,
|
||||
} from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { Viewport } from 'reactflow'
|
||||
import type { Metadata, ThoughtItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import type { VisionFile } from '@/types/app'
|
||||
|
||||
export type LogAnnotation = {
|
||||
id: string
|
||||
@@ -21,69 +24,8 @@ export type Annotation = {
|
||||
created_at?: number
|
||||
}
|
||||
|
||||
type MessageContent = {
|
||||
id: string
|
||||
conversation_id: string
|
||||
query: string
|
||||
inputs: Record<string, any>
|
||||
message: { role: string; text: string; files?: VisionFile[] }[]
|
||||
message_tokens: number
|
||||
answer_tokens: number
|
||||
answer: string
|
||||
provider_response_latency: number
|
||||
created_at: number
|
||||
annotation: LogAnnotation
|
||||
annotation_hit_history: {
|
||||
annotation_id: string
|
||||
annotation_create_account: {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
created_at: number
|
||||
}
|
||||
feedbacks: Array<{
|
||||
rating: 'like' | 'dislike' | null
|
||||
content: string | null
|
||||
from_source?: 'admin' | 'user'
|
||||
from_end_user_id?: string
|
||||
}>
|
||||
message_files: VisionFile[]
|
||||
metadata: Metadata
|
||||
agent_thoughts: ThoughtItem[]
|
||||
workflow_run_id: string
|
||||
parent_message_id: string | null
|
||||
}
|
||||
|
||||
export type ChatMessagesRequest = {
|
||||
conversation_id: string
|
||||
first_id?: string
|
||||
limit: number
|
||||
}
|
||||
export type ChatMessage = MessageContent
|
||||
|
||||
export type ChatMessagesResponse = {
|
||||
data: Array<ChatMessage>
|
||||
has_more: boolean
|
||||
limit: number
|
||||
}
|
||||
|
||||
export type MessageRating = 'like' | 'dislike' | null
|
||||
|
||||
export type LogMessageFeedbacksRequest = {
|
||||
message_id: string
|
||||
rating: MessageRating
|
||||
content?: string
|
||||
}
|
||||
|
||||
export type LogMessageFeedbacksResponse = {
|
||||
result: 'success' | 'error'
|
||||
}
|
||||
|
||||
export type LogMessageAnnotationsRequest = Omit<LogMessageFeedbacksRequest, 'rating'>
|
||||
|
||||
export type LogMessageAnnotationsResponse = LogMessageFeedbacksResponse
|
||||
|
||||
export const WorkflowRunTriggeredFrom = {
|
||||
DEBUGGING: 'debugging',
|
||||
APP_RUN: 'app-run',
|
||||
@@ -159,56 +101,6 @@ export type WorkflowRunDetailResponse = {
|
||||
exceptions_count?: number
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
||||
export type AgentLogDetailResponse = AgentLogResponse
|
||||
export type AgentIteration = AgentIterationLogResponse
|
||||
export type ToolCall = AgentToolCallResponse
|
||||
|
||||
+2
-53
@@ -1,47 +1,6 @@
|
||||
import type {
|
||||
AgentLogDetailRequest,
|
||||
AgentLogDetailResponse,
|
||||
ChatMessagesRequest,
|
||||
ChatMessagesResponse,
|
||||
LogMessageAnnotationsRequest,
|
||||
LogMessageAnnotationsResponse,
|
||||
LogMessageFeedbacksRequest,
|
||||
LogMessageFeedbacksResponse,
|
||||
WorkflowRunDetailResponse,
|
||||
} from '@/models/log'
|
||||
import type { WorkflowRunDetailResponse } from '@/models/log'
|
||||
import type { NodeTracingListResponse } from '@/types/workflow'
|
||||
import { get, post } from './base'
|
||||
|
||||
// (Chat Application) Message list in one session
|
||||
export const fetchChatMessages = ({
|
||||
url,
|
||||
params,
|
||||
}: {
|
||||
url: string
|
||||
params: ChatMessagesRequest
|
||||
}): Promise<ChatMessagesResponse> => {
|
||||
return get<ChatMessagesResponse>(url, { params })
|
||||
}
|
||||
|
||||
export const updateLogMessageFeedbacks = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
body: LogMessageFeedbacksRequest
|
||||
}): Promise<LogMessageFeedbacksResponse> => {
|
||||
return post<LogMessageFeedbacksResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const updateLogMessageAnnotations = ({
|
||||
url,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
body: LogMessageAnnotationsRequest
|
||||
}): Promise<LogMessageAnnotationsResponse> => {
|
||||
return post<LogMessageAnnotationsResponse>(url, { body })
|
||||
}
|
||||
import { get } from './base'
|
||||
|
||||
export const fetchRunDetail = (url: string): Promise<WorkflowRunDetailResponse> => {
|
||||
return get<WorkflowRunDetailResponse>(url)
|
||||
@@ -50,13 +9,3 @@ export const fetchRunDetail = (url: string): Promise<WorkflowRunDetailResponse>
|
||||
export const fetchTracingList = ({ url }: { url: string }): Promise<NodeTracingListResponse> => {
|
||||
return get<NodeTracingListResponse>(url)
|
||||
}
|
||||
|
||||
export const fetchAgentLogDetail = ({
|
||||
appID,
|
||||
params,
|
||||
}: {
|
||||
appID: string
|
||||
params: AgentLogDetailRequest
|
||||
}): Promise<AgentLogDetailResponse> => {
|
||||
return get<AgentLogDetailResponse>(`/apps/${appID}/agent/logs`, { params })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user