瀏覽代碼

Feat: Add agent-log-list page (#9076)

### What problem does this PR solve?

Fix: Add agent-log-list page And RAPTOR:Save directly after enabling,
incomplete form submission #3221

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
tags/v0.20.0
chanx 3 月之前
父節點
當前提交
4f8e7ef763
No account linked to committer's email address

+ 186
- 0
web/src/components/originui/time-range-picker.tsx 查看文件

@@ -0,0 +1,186 @@
import {
endOfMonth,
endOfYear,
format,
startOfMonth,
startOfYear,
subDays,
subMonths,
subYears,
} from 'date-fns';
import { useEffect, useId, useState } from 'react';

import { Calendar, DateRange } from '@/components/originui/calendar';
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { CalendarIcon } from 'lucide-react';

const CalendarComp = ({
selectDateRange,
onSelect,
...props
}: ITimeRangePickerProps) => {
const today = new Date();
const yesterday = {
from: subDays(today, 1),
to: subDays(today, 1),
};
const last7Days = {
from: subDays(today, 6),
to: today,
};
const last30Days = {
from: subDays(today, 29),
to: today,
};
const monthToDate = {
from: startOfMonth(today),
to: today,
};
const lastMonth = {
from: startOfMonth(subMonths(today, 1)),
to: endOfMonth(subMonths(today, 1)),
};
const yearToDate = {
from: startOfYear(today),
to: today,
};
const lastYear = {
from: startOfYear(subYears(today, 1)),
to: endOfYear(subYears(today, 1)),
};
const dateRangeList = [
{ key: 'yestday', value: yesterday },
{ key: 'last7Days', value: last7Days },
{ key: 'last30Days', value: last30Days },
{ key: 'monthToDate', value: monthToDate },
{ key: 'lastMonth', value: lastMonth },
{ key: 'yearToDate', value: yearToDate },
{ key: 'lastYear', value: lastYear },
];
const [month, setMonth] = useState(today);
const [date, setDate] = useState<DateRange>(selectDateRange || last7Days);
useEffect(() => {
onSelect?.(date);
}, [date, onSelect]);
return (
<div>
<div className="rounded-md border">
<div className="flex max-sm:flex-col">
<div className="relative py-4 max-sm:order-1 max-sm:border-t sm:w-32">
<div className="h-full sm:border-e">
<div className="flex flex-col px-2 gap-2">
<Button
variant="ghost"
size="sm"
className="w-full justify-start"
onClick={() => {
setDate({
from: today,
to: today,
});
setMonth(today);
}}
>
Today
</Button>
{dateRangeList.map((dateRange) => (
<Button
key={dateRange.key}
variant="ghost"
size="sm"
className="w-full justify-start"
onClick={() => {
setDate(dateRange.value);
setMonth(dateRange.value.to);
}}
>
{dateRange.key}
</Button>
))}
</div>
</div>
</div>
<Calendar
mode="range"
selected={date}
onSelect={(newDate) => {
if (newDate) {
setDate(newDate as DateRange);
}
}}
month={month}
onMonthChange={setMonth}
className="p-2"
{...props}
// disabled={[
// { after: today }, // Dates before today
// ]}
/>
</div>
</div>
</div>
);
};

export type ITimeRangePickerProps = {
onSelect: (e: DateRange) => void;
selectDateRange: DateRange;
className?: string;
};
const TimeRangePicker = ({
onSelect,
selectDateRange,
...props
}: ITimeRangePickerProps) => {
const id = useId();
const today = new Date();
const [date, setDate] = useState<DateRange | undefined>(
selectDateRange || { from: today, to: today },
);
const onChange = (e: DateRange | undefined) => {
if (!e) return;
setDate(e);
onSelect?.(e);
};
return (
<Popover>
<PopoverTrigger asChild>
<Button
id={id}
variant="outline"
className="group bg-muted-foreground/10 hover:bg-muted-foreground/10 border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]"
>
<span className={cn('truncate', !date && 'text-muted-foreground')}>
{date?.from ? (
date.to ? (
<>
{format(date.from, 'LLL dd, y')} -{' '}
{format(date.to, 'LLL dd, y')}
</>
) : (
format(date.from, 'LLL dd, y')
)
) : (
'Pick a date range'
)}
</span>
<CalendarIcon
size={16}
className="text-muted-foreground/80 group-hover:text-foreground shrink-0 transition-colors"
aria-hidden="true"
/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2" align="start">
<CalendarComp selectDateRange={date} onSelect={onChange} {...props} />
</PopoverContent>
</Popover>
);
};
export default TimeRangePicker;

+ 8
- 0
web/src/hooks/logic-hooks/navigate-hooks.ts 查看文件

@@ -54,6 +54,13 @@ export const useNavigatePage = () => {
[navigate],
);

const navigateToAgentLogs = useCallback(
(id: string) => () => {
navigate(`${Routes.AgentLogPage}/${id}`);
},
[navigate],
);

const navigateToAgentTemplates = useCallback(() => {
navigate(Routes.AgentTemplates);
}, [navigate]);
@@ -120,6 +127,7 @@ export const useNavigatePage = () => {
navigateToChunk,
navigateToAgents,
navigateToAgent,
navigateToAgentLogs,
navigateToAgentTemplates,
navigateToSearchList,
navigateToSearch,

+ 28
- 2
web/src/hooks/use-agent-request.ts 查看文件

@@ -1,13 +1,20 @@
import { FileUploadProps } from '@/components/file-upload';
import message from '@/components/ui/message';
import { AgentGlobals } from '@/constants/agent';
import { ITraceData } from '@/interfaces/database/agent';
import {
IAgentLogsRequest,
IAgentLogsResponse,
ITraceData,
} from '@/interfaces/database/agent';
import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow';
import { IDebugSingleRequestBody } from '@/interfaces/request/agent';
import i18n from '@/locales/config';
import { BeginId } from '@/pages/agent/constant';
import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks';
import agentService, { fetchTrace } from '@/services/agent-service';
import agentService, {
fetchAgentLogsByCanvasId,
fetchTrace,
} from '@/services/agent-service';
import api from '@/utils/api';
import { buildMessageListWithUuid } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
@@ -558,3 +565,22 @@ export const useFetchAgentAvatar = (): {

return { data, loading, refetch };
};

export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => {
const { id } = useParams();
const { data, isFetching: loading } = useQuery<IAgentLogsResponse>({
queryKey: ['fetchAgentLog', id, searchParams],
initialData: {} as IAgentLogsResponse,
gcTime: 0,
queryFn: async () => {
console.log('useFetchAgentLog', searchParams);
const { data } = await fetchAgentLogsByCanvasId(id as string, {
...searchParams,
});

return data?.data ?? [];
},
});

return { data, loading };
};

+ 35
- 0
web/src/interfaces/database/agent.ts 查看文件

@@ -229,3 +229,38 @@ export interface ITraceData {
component_id: string;
trace: Array<Record<string, any>>;
}

export interface IAgentLogResponse {
id: string;
message: IAgentLogMessage[];
update_date: string;
create_date: string;
update_time: number;
create_time: number;
round: number;
thumb_up: number;
errors: string;
source: string;
user_id: string;
dsl: string;
reference: IReference;
}
export interface IAgentLogsResponse {
total: number;
sessions: IAgentLogResponse[];
}
export interface IAgentLogsRequest {
keywords?: string;
to_date?: string | Date;
from_date?: string | Date;
orderby?: string;
desc?: boolean;
page?: number;
page_size?: number;
}

export interface IAgentLogMessage {
content: string;
role: 'user' | 'assistant';
id: string;
}

+ 9
- 8
web/src/pages/agent/index.tsx 查看文件

@@ -25,7 +25,6 @@ import {
CirclePlay,
Download,
History,
Key,
LaptopMinimalCheck,
Logs,
ScreenShare,
@@ -42,7 +41,6 @@ import {
useGetBeginNodeDataInputs,
useGetBeginNodeDataQueryIsSafe,
} from './hooks/use-get-begin-query';
import { useOpenDocument } from './hooks/use-open-document';
import {
useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer,
@@ -73,7 +71,7 @@ export default function Agent() {
const { t } = useTranslation();
const { data: userInfo } = useFetchUserInfo();

const openDocument = useOpenDocument();
// const openDocument = useOpenDocument();
const {
handleExportJson,
handleImportJson,
@@ -100,7 +98,7 @@ export default function Agent() {

const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
const { navigateToAgentLogs } = useNavigatePage();
const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe();

return (
@@ -135,7 +133,10 @@ export default function Agent() {
<History />
{t('flow.historyversion')}
</Button>
<Button variant={'secondary'}>
<Button
variant={'secondary'}
onClick={navigateToAgentLogs(id as string)}
>
<Logs />
{t('flow.log')}
</Button>
@@ -147,11 +148,11 @@ export default function Agent() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<AgentDropdownMenuItem onClick={openDocument}>
{/* <AgentDropdownMenuItem onClick={openDocument}>
<Key />
API
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
</AgentDropdownMenuItem> */}
{/* <DropdownMenuSeparator /> */}
<AgentDropdownMenuItem onClick={handleImportJson}>
<Download />
{t('flow.import')}

+ 319
- 0
web/src/pages/agents/agent-log-page.tsx 查看文件

@@ -0,0 +1,319 @@
import TimeRangePicker from '@/components/originui/time-range-picker';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { SearchInput } from '@/components/ui/input';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { Spin } from '@/components/ui/spin';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchAgentLog } from '@/hooks/use-agent-request';
import { IAgentLogResponse } from '@/interfaces/database/agent';
import React, { useEffect, useState } from 'react';
import { useParams } from 'umi';
import { DateRange } from '../../components/originui/calendar/index';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../../components/ui/table';
import { useFetchDataOnMount } from '../agent/hooks/use-fetch-data';

const AgentLogPage: React.FC = () => {
const { navigateToAgentList, navigateToAgent } = useNavigatePage();
const { flowDetail: agentDetail } = useFetchDataOnMount();
const { id: canvasId } = useParams();
const today = new Date();
const init = {
keywords: '',
from_date: today,
to_date: today,
orderby: 'create_time',
desc: false,
page: 1,
page_size: 10,
};
const [searchParams, setSearchParams] = useState(init);
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: 'Title',
dataIndex: 'title',
key: 'title',
render: (text, record: IAgentLogResponse) => (
<span>
{record?.message?.length ? record?.message[0]?.content : ''}
</span>
),
},
{
title: 'State',
dataIndex: 'state',
key: 'state',
render: (text, record: IAgentLogResponse) => (
<div
className="size-2 rounded-full"
style={{ backgroundColor: record.errors ? 'red' : 'green' }}
></div>
),
},
{
title: 'Number',
dataIndex: 'round',
key: 'round',
},
{
title: 'Latest Date',
dataIndex: 'update_date',
key: 'update_date',
sortable: true,
},
{
title: 'Create Date',
dataIndex: 'create_date',
key: 'create_date',
sortable: true,
},
];

const { data: logData, loading } = useFetchAgentLog(searchParams);
const { sessions: data, total } = logData || {};
const [currentDate, setCurrentDate] = useState<DateRange>({
from: searchParams.from_date,
to: searchParams.to_date,
});
const [keywords, setKeywords] = useState(searchParams.keywords);
const handleDateRangeChange = ({
from: startDate,
to: endDate,
}: DateRange) => {
setCurrentDate({ from: startDate, to: endDate });
};

const [pagination, setPagination] = useState<{
current: number;
pageSize: number;
total: number;
}>({
current: 1,
pageSize: 10,
total: total,
});

useEffect(() => {
setPagination((pre) => {
return {
...pre,
total: total,
};
});
}, [total]);

const [sortConfig, setSortConfig] = useState<{
orderby: string;
desc: boolean;
} | null>({ orderby: init.orderby, desc: init.desc ? true : false });

const handlePageChange = (current?: number, pageSize?: number) => {
console.log('current', current, 'pageSize', pageSize);
setPagination((pre) => {
return {
...pre,
current: current ?? pre.current,
pageSize: pageSize ?? pre.pageSize,
};
});
};

const handleSearch = () => {
setSearchParams((pre) => {
return {
...pre,
from_date: currentDate.from as Date,
to_date: currentDate.to as Date,
page: pagination.current,
page_size: pagination.pageSize,
orderby: sortConfig?.orderby || '',
desc: sortConfig?.desc as boolean,
keywords: keywords,
};
});
};

useEffect(() => {
handleSearch();
}, [pagination.current, pagination.pageSize, sortConfig]);
// handle sort
const handleSort = (key: string) => {
let desc = false;
if (sortConfig && sortConfig.orderby === key) {
desc = !sortConfig.desc;
}
setSortConfig({ orderby: key, desc });
};

const handleReset = () => {
setSearchParams(init);
};
return (
<div className=" text-white">
<PageHeader>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToAgentList}>
Agent
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToAgent(canvasId as string)}>
{agentDetail.title}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Log</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="p-4">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold mb-4">Log</h1>

<div className="flex justify-end space-x-2 mb-4">
<div className="flex items-center space-x-2">
<span>ID/Title</span>
<SearchInput
value={keywords}
onChange={(e) => {
setKeywords(e.target.value);
}}
className="w-32"
></SearchInput>
</div>
<div className="flex items-center space-x-2">
<span className="whitespace-nowrap">Latest Date</span>
<TimeRangePicker
onSelect={handleDateRangeChange}
selectDateRange={{ from: currentDate.from, to: currentDate.to }}
/>
</div>
<button
type="button"
className="bg-foreground text-text-title-invert px-4 py-1 rounded"
onClick={() => {
setPagination({ ...pagination, current: 1 });
handleSearch();
}}
>
Search
</button>
<button
type="button"
className="bg-transparent text-foreground px-4 py-1 rounded border"
onClick={() => handleReset()}
>
Reset
</button>
</div>
</div>
<div className="border rounded-md overflow-auto">
{/* <div className="max-h-[500px] overflow-y-auto w-full"> */}
<Table rootClassName="max-h-[calc(100vh-200px)]">
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
<TableRow>
{columns.map((column) => (
<TableHead
key={column.dataIndex}
onClick={
column.sortable
? () => handleSort(column.dataIndex)
: undefined
}
className={
column.sortable ? 'cursor-pointer hover:bg-muted/50' : ''
}
>
<div className="flex items-center">
{column.title}
{column.sortable &&
sortConfig?.orderby === column.dataIndex && (
<span className="ml-1">
{sortConfig.desc ? '↓' : '↑'}
</span>
)}
</div>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading && (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
<Spin size="large">
<span className="sr-only">Loading...</span>
</Spin>
</TableCell>
</TableRow>
)}
{!loading &&
data?.map((item) => (
<TableRow key={item.id}>
{columns.map((column) => (
<TableCell key={column.dataIndex}>
{column.render
? column.render(item[column.dataIndex], item)
: item[column.dataIndex]}
</TableCell>
))}
</TableRow>
))}
{!loading && (!data || data.length === 0) && (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No data
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* </div> */}
</div>
<div className="flex justify-end mt-4 w-full">
<div className="space-x-2">
<RAGFlowPagination
{...pagination}
total={pagination.total}
onChange={(page, pageSize) => {
handlePageChange(page, pageSize);
}}
></RAGFlowPagination>
</div>
</div>
</div>
</div>
);
};

export default AgentLogPage;

+ 11
- 7
web/src/pages/dataset/setting/chunk-method-form.tsx 查看文件

@@ -88,13 +88,17 @@ export function ChunkMethodForm() {
let beValid = await form.formControl.trigger();
if (beValid) {
// setSubmitLoading(true);
let postData = form.formState.values;
delete postData['avatar']; // has submitted in first form general

saveKnowledgeConfiguration({
...postData,
kb_id,
});
// let postData = form.formState.values;
// console.log('submit form -->', form);
// delete postData['avatar']; // has submitted in first form general
form.handleSubmit(async (values) => {
console.log('saveKnowledgeConfiguration: ', values);
delete values['avatar'];
await saveKnowledgeConfiguration({
kb_id,
...values,
});
})();
}
} catch (e) {
console.log(e);

+ 6
- 0
web/src/routes.ts 查看文件

@@ -36,6 +36,7 @@ export enum Routes {
Result = '/result',
ResultView = `${Chunk}${Result}`,
KnowledgeGraph = '/knowledge-graph',
AgentLogPage = '/agent-log-page',
}

const routes = [
@@ -244,6 +245,11 @@ const routes = [
},
],
},
{
path: `${Routes.AgentLogPage}/:id`,
layout: false,
component: `@/pages${Routes.Agents}${Routes.AgentLogPage}`,
},
{
path: `${Routes.Agent}/:id`,
layout: false,

+ 12
- 0
web/src/services/agent-service.ts 查看文件

@@ -1,3 +1,4 @@
import { IAgentLogsRequest } from '@/interfaces/database/agent';
import api from '@/utils/api';
import { registerNextServer } from '@/utils/register-server';
import request from '@/utils/request';
@@ -22,6 +23,7 @@ const {
fetchVersion,
fetchCanvas,
fetchAgentAvatar,
fetchAgentLogs,
} = api;

const methods = {
@@ -101,6 +103,10 @@ const methods = {
url: fetchAgentAvatar,
method: 'get',
},
fetchAgentLogs: {
url: fetchAgentLogs,
method: 'get',
},
} as const;

const agentService = registerNextServer<keyof typeof methods>(methods);
@@ -108,5 +114,11 @@ const agentService = registerNextServer<keyof typeof methods>(methods);
export const fetchTrace = (data: { canvas_id: string; message_id: string }) => {
return request.get(methods.trace.url, { params: data });
};
export const fetchAgentLogsByCanvasId = (
canvasId: string,
params: IAgentLogsRequest,
) => {
return request.get(methods.fetchAgentLogs.url(canvasId), { params: params });
};

export default agentService;

+ 2
- 0
web/src/utils/api.ts 查看文件

@@ -153,6 +153,8 @@ export default {
fetchCanvas: (id: string) => `${api_host}/canvas/get/${id}`,
fetchAgentAvatar: (id: string) => `${api_host}/canvas/getsse/${id}`,
uploadAgentFile: (id?: string) => `${api_host}/canvas/upload/${id}`,
fetchAgentLogs: (canvasId: string) =>
`${api_host}/canvas/${canvasId}/sessions`,

// mcp server
listMcpServer: `${api_host}/mcp_server/list`,

+ 11
- 0
web/src/utils/date.ts 查看文件

@@ -32,3 +32,14 @@ export function formatPureDate(date: any) {
}
return dayjs(date).format('DD/MM/YYYY');
}

export function formatStandardDate(date: any) {
if (!date) {
return '';
}
const parsedDate = dayjs(date);
if (!parsedDate.isValid()) {
return '';
}
return parsedDate.format('YYYY-MM-DD');
}

Loading…
取消
儲存