mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-05-03 08:47:48 +08:00
Feat: Export Agent Logs. (#13658)
### What problem does this PR solve? Feat: Export Agent Logs. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Co-authored-by: balibabu <assassin_cike@163.com>
This commit is contained in:
@ -688,6 +688,8 @@ async def set_session(canvas_id):
|
||||
session_id=get_uuid()
|
||||
canvas = Canvas(cvs.dsl, tenant_id, canvas_id, canvas_id=cvs.id)
|
||||
canvas.reset()
|
||||
# Get the version title for this canvas (using latest, not necessarily released)
|
||||
version_title = UserCanvasVersionService.get_latest_version_title(cvs.id, release_mode=False)
|
||||
conv = {
|
||||
"id": session_id,
|
||||
"name": req.get("name", ""),
|
||||
@ -697,7 +699,8 @@ async def set_session(canvas_id):
|
||||
"message": [],
|
||||
"source": "agent",
|
||||
"dsl": cvs.dsl,
|
||||
"reference": []
|
||||
"reference": [],
|
||||
"version_title": version_title
|
||||
}
|
||||
API4ConversationService.save(**conv)
|
||||
return get_json_result(data=conv)
|
||||
|
||||
@ -32,6 +32,7 @@ from api.db.services.api_service import API4ConversationService
|
||||
from api.db.services.canvas_service import UserCanvasService, completion_openai
|
||||
from api.db.services.canvas_service import completion as agent_completion
|
||||
from api.db.services.conversation_service import ConversationService
|
||||
from api.db.services.user_canvas_version import UserCanvasVersionService
|
||||
from api.db.services.conversation_service import async_iframe_completion as iframe_completion
|
||||
from api.db.services.conversation_service import async_completion as rag_completion
|
||||
from api.db.services.dialog_service import DialogService, async_ask, async_chat, gen_mindmap
|
||||
@ -104,8 +105,17 @@ async def create_agent_session(tenant_id, agent_id):
|
||||
canvas.reset()
|
||||
|
||||
cvs.dsl = json.loads(str(canvas))
|
||||
conv = {"id": session_id, "dialog_id": cvs.id, "user_id": user_id,
|
||||
"message": [{"role": "assistant", "content": canvas.get_prologue()}], "source": "agent", "dsl": cvs.dsl}
|
||||
# Get the version title based on release_mode
|
||||
version_title = UserCanvasVersionService.get_latest_version_title(cvs.id, release_mode=release_mode)
|
||||
conv = {
|
||||
"id": session_id,
|
||||
"dialog_id": cvs.id,
|
||||
"user_id": user_id,
|
||||
"message": [{"role": "assistant", "content": canvas.get_prologue()}],
|
||||
"source": "agent",
|
||||
"dsl": cvs.dsl,
|
||||
"version_title": version_title
|
||||
}
|
||||
API4ConversationService.save(**conv)
|
||||
conv["agent_id"] = conv.pop("dialog_id")
|
||||
return get_result(data=conv)
|
||||
|
||||
@ -1034,6 +1034,7 @@ class API4Conversation(DataBaseModel):
|
||||
round = IntegerField(default=0, index=True)
|
||||
thumb_up = IntegerField(default=0, index=True)
|
||||
errors = TextField(null=True, help_text="errors")
|
||||
version_title = CharField(max_length=255, null=True, help_text="canvas version title when session created", index=False)
|
||||
|
||||
class Meta:
|
||||
db_table = "api_4_conversation"
|
||||
@ -1632,6 +1633,7 @@ def migrate_db():
|
||||
alter_db_add_column(migrator, "memory", "tenant_embd_id", IntegerField(null=True, help_text="id in tenant_llm", index=True))
|
||||
alter_db_add_column(migrator, "memory", "tenant_llm_id", IntegerField(null=True, help_text="id in tenant_llm", index=True))
|
||||
alter_db_add_column(migrator, "user_canvas_version", "release", BooleanField(null=False, help_text="is released", default=False, index=True))
|
||||
alter_db_add_column(migrator, "api_4_conversation", "version_title", CharField(max_length=255, null=True, help_text="canvas version title when session created", index=False))
|
||||
logging.disable(logging.NOTSET)
|
||||
# this is after re-enabling logging to allow logging changed user emails
|
||||
migrate_add_unique_email(migrator)
|
||||
|
||||
@ -250,7 +250,9 @@ async def completion(tenant_id, agent_id, session_id=None, **kwargs):
|
||||
session_id = get_uuid()
|
||||
canvas = Canvas(dsl, tenant_id, agent_id, canvas_id=cvs.id, custom_header=custom_header)
|
||||
canvas.reset()
|
||||
conv = {"id": session_id, "dialog_id": cvs.id, "user_id": user_id, "message": [], "source": "agent", "dsl": dsl, "reference": []}
|
||||
# Get the version title based on release_mode
|
||||
version_title = UserCanvasVersionService.get_latest_version_title(cvs.id, release_mode=release_mode == "true")
|
||||
conv = {"id": session_id, "dialog_id": cvs.id, "user_id": user_id, "message": [], "source": "agent", "dsl": dsl, "reference": [], "version_title": version_title}
|
||||
API4ConversationService.save(**conv)
|
||||
conv = API4Conversation(**conv)
|
||||
|
||||
|
||||
@ -91,15 +91,36 @@ class UserCanvasVersionService(CommonService):
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_latest_released(cls, user_canvas_id):
|
||||
def _get_latest_by_canvas_id(cls, user_canvas_id, only_released=False):
|
||||
"""Get the latest version for a canvas, optionally filtered by release status."""
|
||||
try:
|
||||
return cls.model.select().where((cls.model.user_canvas_id == user_canvas_id) & (cls.model.release)).order_by(cls.model.create_time.desc()).first()
|
||||
query = cls.model.select().where(cls.model.user_canvas_id == user_canvas_id)
|
||||
if only_released:
|
||||
query = query.where(cls.model.release)
|
||||
return query.order_by(cls.model.create_time.desc()).first()
|
||||
except DoesNotExist:
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_latest_released(cls, user_canvas_id):
|
||||
"""Get the latest released version for a canvas."""
|
||||
return cls._get_latest_by_canvas_id(user_canvas_id, only_released=True)
|
||||
|
||||
@classmethod
|
||||
def get_latest_version_title(cls, user_canvas_id, release_mode=False):
|
||||
"""Get the version title for a canvas based on release_mode.
|
||||
|
||||
Args:
|
||||
user_canvas_id: The canvas ID
|
||||
release_mode: If True, get the latest released version title;
|
||||
If False, get the latest version title (regardless of release status)
|
||||
"""
|
||||
latest = cls._get_latest_by_canvas_id(user_canvas_id, only_released=release_mode)
|
||||
return latest.title if latest else None
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def save_or_replace_latest(cls, user_canvas_id, dsl, title=None, description=None, release=None):
|
||||
@ -137,9 +158,9 @@ class UserCanvasVersionService(CommonService):
|
||||
return None, True
|
||||
|
||||
# Normal case: update existing version
|
||||
# DSL unchanged: do NOT update title to preserve version identity
|
||||
# Only update dsl (for normalization consistency), description, and release
|
||||
update_data = {"dsl": normalized_dsl}
|
||||
if title is not None:
|
||||
update_data["title"] = title
|
||||
if description is not None:
|
||||
update_data["description"] = description
|
||||
if release is not None:
|
||||
|
||||
@ -366,6 +366,7 @@ def _load_canvas_module(monkeypatch):
|
||||
get_by_id=lambda *_args, **_kwargs: (True, None),
|
||||
save_or_replace_latest=lambda *_args, **_kwargs: True,
|
||||
build_version_title=lambda *_args, **_kwargs: "stub_version_title",
|
||||
get_latest_version_title=lambda *_args, **_kwargs: "stub_version_title",
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.user_canvas_version", canvas_version_mod)
|
||||
|
||||
|
||||
@ -71,6 +71,8 @@ export const enum AgentApiAction {
|
||||
CreateAgentSession = 'createAgentSession',
|
||||
DeleteAgentSession = 'deleteAgentSession',
|
||||
FetchSessionByIdManually = 'fetchSessionByIdManually',
|
||||
FetchAgentLog = 'fetchAgentLog',
|
||||
FetchFlowDetailSSE = 'flowDetailSSE',
|
||||
}
|
||||
|
||||
export const useFetchAgentTemplates = () => {
|
||||
@ -599,7 +601,7 @@ export const useFetchAgentAvatar = (): {
|
||||
export const useFetchAgentLog = (searchParams: IAgentLogsRequest) => {
|
||||
const { id } = useParams();
|
||||
const { data, isFetching: loading } = useQuery<IAgentLogsResponse>({
|
||||
queryKey: ['fetchAgentLog', id, searchParams],
|
||||
queryKey: [AgentApiAction.FetchAgentLog, id, searchParams],
|
||||
initialData: {} as IAgentLogsResponse,
|
||||
gcTime: 0,
|
||||
queryFn: async () => {
|
||||
@ -803,7 +805,7 @@ export const useFetchFlowSSE = (): {
|
||||
isFetching: loading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ['flowDetailSSE'],
|
||||
queryKey: [AgentApiAction.FetchFlowDetailSSE],
|
||||
initialData: {} as IFlow,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
@ -970,3 +972,21 @@ export function useFetchSessionManually() {
|
||||
|
||||
return { data, loading, fetchSessionManually: mutateAsync };
|
||||
}
|
||||
|
||||
export const useExportAgentLog = () => {
|
||||
const { id } = useParams();
|
||||
const { mutateAsync, isPending: loading } = useMutation({
|
||||
mutationKey: [AgentApiAction.FetchAgentLog, 'export', id],
|
||||
mutationFn: async (searchParams: IAgentLogsRequest) => {
|
||||
const { data } = await fetchAgentLogsByCanvasId(id as string, {
|
||||
...searchParams,
|
||||
page: 1,
|
||||
page_size: 100000,
|
||||
});
|
||||
|
||||
return data?.data?.sessions ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
return { exportLogs: mutateAsync, loading };
|
||||
};
|
||||
|
||||
@ -97,10 +97,12 @@ export interface IFlowTemplate {
|
||||
description: {
|
||||
en: string;
|
||||
zh: string;
|
||||
de: string;
|
||||
};
|
||||
title: {
|
||||
en: string;
|
||||
zh: string;
|
||||
de: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -264,6 +266,7 @@ export interface IAgentLogResponse {
|
||||
dsl: string;
|
||||
reference: IReference;
|
||||
name: string;
|
||||
version_title: string;
|
||||
}
|
||||
export interface IAgentLogsResponse {
|
||||
total: number;
|
||||
|
||||
@ -2420,6 +2420,15 @@ Important structured information may include: names, dates, locations, events, k
|
||||
},
|
||||
saveToMemory: 'Save to memory',
|
||||
retrievalFrom: 'Retrieval from',
|
||||
id: 'ID',
|
||||
state: 'State',
|
||||
number: 'Number',
|
||||
latestDate: 'Latest date',
|
||||
createDate: 'Create date',
|
||||
noDataToExport: 'No data to export',
|
||||
success: 'Success',
|
||||
failed: 'Failed',
|
||||
logTitle: 'Title',
|
||||
},
|
||||
llmTools: {
|
||||
bad_calculator: {
|
||||
|
||||
@ -1260,6 +1260,12 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
consumerApp: '消费者应用',
|
||||
other: '其他',
|
||||
agents: '智能体',
|
||||
id: 'ID',
|
||||
logTitle: '标题',
|
||||
state: '状态',
|
||||
number: '轮数',
|
||||
latestDate: '最新日期',
|
||||
createDate: '创建日期',
|
||||
publishedAt: '发布于',
|
||||
beginInput: '开始输入',
|
||||
seconds: '秒',
|
||||
@ -1352,6 +1358,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
version: {
|
||||
details: '版本详情',
|
||||
download: '下载',
|
||||
version: '版本',
|
||||
},
|
||||
cite: '引用',
|
||||
citeTip: '引用',
|
||||
|
||||
@ -72,7 +72,7 @@ export function VersionDialog({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<section className="flex gap-8 relative">
|
||||
<div className="w-1/3 max-h-[60vh] overflow-auto min-h-[40vh]">
|
||||
<div className="w-72 max-h-[60vh] overflow-auto min-h-[40vh]">
|
||||
{loading ? (
|
||||
<Spin className="top-1/2"></Spin>
|
||||
) : (
|
||||
@ -100,7 +100,7 @@ export function VersionDialog({
|
||||
) : (
|
||||
<Card className="h-full">
|
||||
<CardContent className="h-full p-5 flex flex-col">
|
||||
<section className="flex justify-between">
|
||||
<section className="flex justify-between pb-2">
|
||||
<div>
|
||||
<div className="flex">
|
||||
<span className="pb-1 truncate">{agent?.title}</span>
|
||||
@ -143,6 +143,9 @@ export function VersionDialog({
|
||||
zoomOnDoubleClick={false}
|
||||
preventScrolling={true}
|
||||
minZoom={0.1}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
elementsSelectable={false}
|
||||
>
|
||||
<AgentBackground></AgentBackground>
|
||||
<Spotlight className="z-0" opcity={0.7} coverage={70} />
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
} from '../../components/ui/table';
|
||||
import { useFetchDataOnMount } from '../agent/hooks/use-fetch-data';
|
||||
import { AgentLogDetailModal } from './agent-log-detail-modal';
|
||||
import { useExportAgentLogToCSV } from './hooks/use-export-agent-log';
|
||||
const getStartOfToday = (): Date => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
@ -64,18 +65,18 @@ const AgentLogPage: React.FC = () => {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
title: t('flow.id'),
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: 'User ID',
|
||||
title: t('flow.userId'),
|
||||
dataIndex: 'user_id',
|
||||
key: 'user_id',
|
||||
render: (text: string) => <span>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: 'Title',
|
||||
title: t('flow.logTitle'),
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
render: (_text: string, record: IAgentLogResponse) => (
|
||||
@ -85,7 +86,7 @@ const AgentLogPage: React.FC = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'State',
|
||||
title: t('flow.state'),
|
||||
dataIndex: 'state',
|
||||
key: 'state',
|
||||
render: (_text: string, record: IAgentLogResponse) => (
|
||||
@ -96,31 +97,32 @@ const AgentLogPage: React.FC = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Number',
|
||||
title: t('flow.number'),
|
||||
dataIndex: 'round',
|
||||
key: 'round',
|
||||
},
|
||||
{
|
||||
title: 'Latest Date',
|
||||
title: t('flow.latestDate'),
|
||||
dataIndex: 'update_date',
|
||||
key: 'update_date',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
title: 'Create Date',
|
||||
title: t('flow.createDate'),
|
||||
dataIndex: 'create_date',
|
||||
key: 'create_date',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
title: 'Version',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
title: t('flow.version.version'),
|
||||
dataIndex: 'version_title',
|
||||
key: 'version_title',
|
||||
},
|
||||
];
|
||||
|
||||
const { data: logData, loading } = useFetchAgentLog(searchParams);
|
||||
const { sessions: data, total } = logData || {};
|
||||
const { handleExport, loading: exportLoading } = useExportAgentLogToCSV();
|
||||
const [currentDate, setCurrentDate] = useState<DateRange>({
|
||||
from: searchParams.from_date,
|
||||
to: searchParams.to_date,
|
||||
@ -158,7 +160,6 @@ const AgentLogPage: React.FC = () => {
|
||||
} | null>({ orderby: init.orderby, desc: init.desc ? true : false });
|
||||
|
||||
const handlePageChange = (current?: number, pageSize?: number) => {
|
||||
console.log('current', current, 'pageSize', pageSize);
|
||||
let page = current || 1;
|
||||
if (pagination.pageSize !== pageSize) {
|
||||
page = 1;
|
||||
@ -219,6 +220,16 @@ const AgentLogPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onExportClick = () => {
|
||||
handleExport({
|
||||
keywords: searchParams.keywords,
|
||||
from_date: searchParams.from_date,
|
||||
to_date: searchParams.to_date,
|
||||
orderby: searchParams.orderby,
|
||||
desc: searchParams.desc,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=" text-white">
|
||||
<PageHeader>
|
||||
@ -246,7 +257,9 @@ const AgentLogPage: React.FC = () => {
|
||||
|
||||
<div className="flex justify-end space-x-2 mb-4 text-foreground">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button>{t('flow.export')}</Button>
|
||||
<Button onClick={onExportClick} loading={exportLoading}>
|
||||
{t('flow.export')}
|
||||
</Button>
|
||||
<span>ID/Title</span>
|
||||
<SearchInput
|
||||
value={keywords}
|
||||
|
||||
81
web/src/pages/agents/hooks/use-export-agent-log.ts
Normal file
81
web/src/pages/agents/hooks/use-export-agent-log.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import message from '@/components/ui/message';
|
||||
import { useExportAgentLog } from '@/hooks/use-agent-request';
|
||||
import { IAgentLogResponse } from '@/interfaces/database/agent';
|
||||
import { downloadFileFromBlob } from '@/utils/file-util';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
interface ISearchParams {
|
||||
keywords?: string;
|
||||
from_date?: Date;
|
||||
to_date?: Date;
|
||||
orderby?: string;
|
||||
desc?: boolean;
|
||||
}
|
||||
|
||||
export const useExportAgentLogToCSV = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id: canvasId } = useParams();
|
||||
const { exportLogs, loading } = useExportAgentLog();
|
||||
|
||||
const convertToCSV = (data: IAgentLogResponse[]) => {
|
||||
const headers = [
|
||||
t('flow.id'),
|
||||
t('flow.userId'),
|
||||
t('flow.logTitle'),
|
||||
t('flow.state'),
|
||||
t('flow.number'),
|
||||
t('flow.latestDate'),
|
||||
t('flow.createDate'),
|
||||
t('flow.version.version'),
|
||||
];
|
||||
|
||||
const rows = data.map((item) => [
|
||||
item.id,
|
||||
item.user_id,
|
||||
item.message?.length ? item.message[0]?.content : '',
|
||||
item.errors ? t('flow.failed') : t('flow.success'),
|
||||
item.round,
|
||||
item.update_date,
|
||||
item.create_date,
|
||||
item.version_title,
|
||||
]);
|
||||
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map((row) =>
|
||||
row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','),
|
||||
),
|
||||
].join('\n');
|
||||
|
||||
return csvContent;
|
||||
};
|
||||
|
||||
const handleExport = async (searchParams: ISearchParams) => {
|
||||
const allData = await exportLogs({
|
||||
keywords: searchParams.keywords,
|
||||
from_date: searchParams.from_date,
|
||||
to_date: searchParams.to_date,
|
||||
orderby: searchParams.orderby,
|
||||
desc: searchParams.desc,
|
||||
});
|
||||
|
||||
if (allData.length === 0) {
|
||||
message.warning(t('flow.noDataToExport'));
|
||||
return;
|
||||
}
|
||||
|
||||
const csvContent = convertToCSV(allData);
|
||||
// Add BOM for Excel to correctly display UTF-8
|
||||
const BOM = '\uFEFF';
|
||||
const blob = new Blob([BOM + csvContent], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
downloadFileFromBlob(
|
||||
blob,
|
||||
`agent-logs-${canvasId}-${new Date().toISOString().split('T')[0]}.csv`,
|
||||
);
|
||||
};
|
||||
|
||||
return { handleExport, loading };
|
||||
};
|
||||
Reference in New Issue
Block a user