diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 21eff42366..3585f81a21 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -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) diff --git a/api/apps/sdk/session.py b/api/apps/sdk/session.py index 190314fa57..7c3395d3a5 100644 --- a/api/apps/sdk/session.py +++ b/api/apps/sdk/session.py @@ -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) diff --git a/api/db/db_models.py b/api/db/db_models.py index bf265f5855..9ad52d4312 100644 --- a/api/db/db_models.py +++ b/api/db/db_models.py @@ -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) diff --git a/api/db/services/canvas_service.py b/api/db/services/canvas_service.py index bd655d122b..98925fa246 100644 --- a/api/db/services/canvas_service.py +++ b/api/db/services/canvas_service.py @@ -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) diff --git a/api/db/services/user_canvas_version.py b/api/db/services/user_canvas_version.py index 067122e9b7..292b18811d 100644 --- a/api/db/services/user_canvas_version.py +++ b/api/db/services/user_canvas_version.py @@ -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: diff --git a/test/testcases/test_web_api/test_canvas_app/test_canvas_routes_unit.py b/test/testcases/test_web_api/test_canvas_app/test_canvas_routes_unit.py index 4431575800..a97fa4e8c0 100644 --- a/test/testcases/test_web_api/test_canvas_app/test_canvas_routes_unit.py +++ b/test/testcases/test_web_api/test_canvas_app/test_canvas_routes_unit.py @@ -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) diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 5efd47975f..4e14c0f212 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -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({ - 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 }; +}; diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index 8ca9d81942..6620c7fd32 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -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; diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 0b19997283..6e4a4534ae 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -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: { diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index dfdeba399f..dce6018037 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -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: '引用', diff --git a/web/src/pages/agent/version-dialog/index.tsx b/web/src/pages/agent/version-dialog/index.tsx index a1d8d32b9c..51d7a0254f 100644 --- a/web/src/pages/agent/version-dialog/index.tsx +++ b/web/src/pages/agent/version-dialog/index.tsx @@ -72,7 +72,7 @@ export function VersionDialog({
-
+
{loading ? ( ) : ( @@ -100,7 +100,7 @@ export function VersionDialog({ ) : ( -
+
{agent?.title} @@ -143,6 +143,9 @@ export function VersionDialog({ zoomOnDoubleClick={false} preventScrolling={true} minZoom={0.1} + nodesDraggable={false} + nodesConnectable={false} + elementsSelectable={false} > diff --git a/web/src/pages/agents/agent-log-page.tsx b/web/src/pages/agents/agent-log-page.tsx index 70728cc79e..ca84aeacc4 100644 --- a/web/src/pages/agents/agent-log-page.tsx +++ b/web/src/pages/agents/agent-log-page.tsx @@ -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) => {text}, }, { - 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({ 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 (
@@ -246,7 +257,9 @@ const AgentLogPage: React.FC = () => {
- + ID/Title { + 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 }; +};