mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-04-20 13:10:05 +08:00
Feat: support agent version history. (#6130)
### What problem does this PR solve? Add history version save - Allows users to view and download agent files by version revision history  _Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._ ### Type of change - [ ] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): --------- Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
This commit is contained in:
parent
e689532e6e
commit
53ac27c3ff
@ -18,13 +18,14 @@ import traceback
|
||||
from flask import request, Response
|
||||
from flask_login import login_required, current_user
|
||||
from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService
|
||||
from api.db.services.user_canvas_version import UserCanvasVersionService
|
||||
from api.settings import RetCode
|
||||
from api.utils import get_uuid
|
||||
from api.utils.api_utils import get_json_result, server_error_response, validate_request, get_data_error_result
|
||||
from agent.canvas import Canvas
|
||||
from peewee import MySQLDatabase, PostgresqlDatabase
|
||||
from api.db.db_models import APIToken
|
||||
|
||||
import time
|
||||
|
||||
@manager.route('/templates', methods=['GET']) # noqa: F821
|
||||
@login_required
|
||||
@ -61,7 +62,6 @@ def save():
|
||||
req["user_id"] = current_user.id
|
||||
if not isinstance(req["dsl"], str):
|
||||
req["dsl"] = json.dumps(req["dsl"], ensure_ascii=False)
|
||||
|
||||
req["dsl"] = json.loads(req["dsl"])
|
||||
if "id" not in req:
|
||||
if UserCanvasService.query(user_id=current_user.id, title=req["title"].strip()):
|
||||
@ -75,9 +75,14 @@ def save():
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
UserCanvasService.update_by_id(req["id"], req)
|
||||
# save version
|
||||
UserCanvasVersionService.insert( user_canvas_id=req["id"], dsl=req["dsl"], title="{0}_{1}".format(req["title"], time.strftime("%Y_%m_%d_%H_%M_%S")))
|
||||
UserCanvasVersionService.delete_all_versions(req["id"])
|
||||
return get_json_result(data=req)
|
||||
|
||||
|
||||
|
||||
|
||||
@manager.route('/get/<canvas_id>', methods=['GET']) # noqa: F821
|
||||
@login_required
|
||||
def get(canvas_id):
|
||||
@ -284,3 +289,27 @@ def test_db_connect():
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
|
||||
|
||||
#api get list version dsl of canvas
|
||||
@manager.route('/getlistversion/<canvas_id>', methods=['GET']) # noqa: F821
|
||||
@login_required
|
||||
def getlistversion(canvas_id):
|
||||
try:
|
||||
list =sorted([c.to_dict() for c in UserCanvasVersionService.list_by_canvas_id(canvas_id)], key=lambda x: x["update_time"]*-1)
|
||||
return get_json_result(data=list)
|
||||
except Exception as e:
|
||||
return get_data_error_result(message=f"Error getting history files: {e}")
|
||||
|
||||
#api get version dsl of canvas
|
||||
@manager.route('/getversion/<version_id>', methods=['GET']) # noqa: F821
|
||||
@login_required
|
||||
def getversion( version_id):
|
||||
try:
|
||||
|
||||
e, version = UserCanvasVersionService.get_by_id(version_id)
|
||||
if version:
|
||||
return get_json_result(data=version.to_dict())
|
||||
except Exception as e:
|
||||
return get_json_result(data=f"Error getting history file: {e}")
|
||||
|
@ -988,6 +988,16 @@ class CanvasTemplate(DataBaseModel):
|
||||
class Meta:
|
||||
db_table = "canvas_template"
|
||||
|
||||
class UserCanvasVersion(DataBaseModel):
|
||||
id = CharField(max_length=32, primary_key=True)
|
||||
user_canvas_id = CharField(max_length=255, null=False, help_text="user_canvas_id", index=True)
|
||||
|
||||
title = CharField(max_length=255, null=True, help_text="Canvas title")
|
||||
description = TextField(null=True, help_text="Canvas description")
|
||||
dsl = JSONField(null=True, default={})
|
||||
|
||||
class Meta:
|
||||
db_table = "user_canvas_version"
|
||||
|
||||
def migrate_db():
|
||||
with DB.transaction():
|
||||
|
43
api/db/services/user_canvas_version.py
Normal file
43
api/db/services/user_canvas_version.py
Normal file
@ -0,0 +1,43 @@
|
||||
from api.db.db_models import UserCanvasVersion, DB
|
||||
from api.db.services.common_service import CommonService
|
||||
from peewee import DoesNotExist
|
||||
|
||||
class UserCanvasVersionService(CommonService):
|
||||
model = UserCanvasVersion
|
||||
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def list_by_canvas_id(cls, user_canvas_id):
|
||||
try:
|
||||
user_canvas_version = cls.model.select(
|
||||
*[cls.model.id,
|
||||
cls.model.create_time,
|
||||
cls.model.title,
|
||||
cls.model.create_date,
|
||||
cls.model.update_date,
|
||||
cls.model.user_canvas_id,
|
||||
cls.model.update_time]
|
||||
).where(cls.model.user_canvas_id == user_canvas_id)
|
||||
return user_canvas_version
|
||||
except DoesNotExist:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def delete_all_versions(cls, user_canvas_id):
|
||||
try:
|
||||
user_canvas_version = cls.model.select().where(cls.model.user_canvas_id == user_canvas_id).order_by(cls.model.create_time.desc())
|
||||
if user_canvas_version.count() > 20:
|
||||
for i in range(20, user_canvas_version.count()):
|
||||
cls.delete(user_canvas_version[i].id)
|
||||
return True
|
||||
except DoesNotExist:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
|
@ -17,6 +17,8 @@ services:
|
||||
- ./nginx/ragflow.conf:/etc/nginx/conf.d/ragflow.conf
|
||||
- ./nginx/proxy.conf:/etc/nginx/proxy.conf
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ../history_data_agent:/ragflow/history_data_agent
|
||||
|
||||
env_file: .env
|
||||
environment:
|
||||
- TZ=${TIMEZONE}
|
||||
|
@ -90,6 +90,53 @@ export const useFetchFlowList = (): { data: IFlow[]; loading: boolean } => {
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const useFetchListVersion = (
|
||||
canvas_id: string,
|
||||
): {
|
||||
data: {
|
||||
created_at: string;
|
||||
title: string;
|
||||
id: string;
|
||||
}[];
|
||||
loading: boolean;
|
||||
} => {
|
||||
const { data, isFetching: loading } = useQuery({
|
||||
queryKey: ['fetchListVersion'],
|
||||
initialData: [],
|
||||
gcTime: 0,
|
||||
queryFn: async () => {
|
||||
const { data } = await flowService.getListVersion({}, canvas_id);
|
||||
|
||||
return data?.data ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const useFetchVersion = (
|
||||
version_id?: string,
|
||||
): {
|
||||
data?: IFlow;
|
||||
loading: boolean;
|
||||
} => {
|
||||
const { data, isFetching: loading } = useQuery({
|
||||
queryKey: ['fetchVersion', version_id],
|
||||
initialData: undefined,
|
||||
gcTime: 0,
|
||||
enabled: !!version_id, // Only call API when both values are provided
|
||||
queryFn: async () => {
|
||||
if (!version_id) return undefined;
|
||||
|
||||
const { data } = await flowService.getVersion({}, version_id);
|
||||
|
||||
return data?.data ?? undefined;
|
||||
},
|
||||
});
|
||||
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
export const useFetchFlow = (): {
|
||||
data: IFlow;
|
||||
loading: boolean;
|
||||
|
@ -1194,6 +1194,16 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
nextStep: 'Next step',
|
||||
datatype: 'MINE type of the HTTP request',
|
||||
insertVariableTip: `Enter / Insert variables`,
|
||||
historyversion: 'History version',
|
||||
filename: 'File name',
|
||||
version: {
|
||||
created: 'Created',
|
||||
details: 'Version details',
|
||||
dsl: 'DSL',
|
||||
download: 'Download',
|
||||
version: 'Version',
|
||||
select: 'No version selected',
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
profile: 'All rights reserved @ React',
|
||||
|
@ -46,7 +46,7 @@ import { RewriteNode } from './node/rewrite-node';
|
||||
import { SwitchNode } from './node/switch-node';
|
||||
import { TemplateNode } from './node/template-node';
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
export const nodeTypes: NodeTypes = {
|
||||
ragNode: RagNode,
|
||||
categorizeNode: CategorizeNode,
|
||||
beginNode: BeginNode,
|
||||
@ -66,7 +66,7 @@ const nodeTypes: NodeTypes = {
|
||||
iterationStartNode: IterationStartNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
export const edgeTypes = {
|
||||
buttonEdge: ButtonEdge,
|
||||
};
|
||||
|
||||
|
@ -18,6 +18,10 @@ import {
|
||||
} from '../hooks/use-save-graph';
|
||||
import { BeginQuery } from '../interface';
|
||||
|
||||
import {
|
||||
HistoryVersionModal,
|
||||
useHistoryVersionModal,
|
||||
} from '../history-version-modal';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
@ -36,7 +40,8 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
|
||||
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
|
||||
useShowEmbedModal();
|
||||
const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe();
|
||||
|
||||
const { setVisibleHistoryVersionModal, visibleHistoryVersionModal } =
|
||||
useHistoryVersionModal();
|
||||
const handleShowEmbedModal = useCallback(() => {
|
||||
showEmbedModal();
|
||||
}, [showEmbedModal]);
|
||||
@ -50,6 +55,9 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
|
||||
}
|
||||
}, [getBeginNodeDataQuery, handleRun, showChatDrawer]);
|
||||
|
||||
const showListVersion = useCallback(() => {
|
||||
setVisibleHistoryVersionModal(true);
|
||||
}, [setVisibleHistoryVersionModal]);
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
@ -83,6 +91,9 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
|
||||
>
|
||||
<b>{t('embedIntoSite', { keyPrefix: 'common' })}</b>
|
||||
</Button>
|
||||
<Button type="primary" onClick={showListVersion}>
|
||||
<b>{t('historyversion')}</b>
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
{embedVisible && (
|
||||
@ -95,6 +106,13 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
|
||||
isAgent
|
||||
></EmbedModal>
|
||||
)}
|
||||
{visibleHistoryVersionModal && (
|
||||
<HistoryVersionModal
|
||||
id={id || ''}
|
||||
visible={visibleHistoryVersionModal}
|
||||
hideModal={() => setVisibleHistoryVersionModal(false)}
|
||||
></HistoryVersionModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
184
web/src/pages/flow/history-version-modal/index.tsx
Normal file
184
web/src/pages/flow/history-version-modal/index.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFetchListVersion, useFetchVersion } from '@/hooks/flow-hooks';
|
||||
import {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
} from '@xyflow/react';
|
||||
import { Card, Col, Empty, List, Modal, Row, Spin, Typography } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { nodeTypes } from '../canvas';
|
||||
|
||||
export function useHistoryVersionModal() {
|
||||
const [visibleHistoryVersionModal, setVisibleHistoryVersionModal] =
|
||||
React.useState(false);
|
||||
|
||||
return {
|
||||
visibleHistoryVersionModal,
|
||||
setVisibleHistoryVersionModal,
|
||||
};
|
||||
}
|
||||
|
||||
type HistoryVersionModalProps = {
|
||||
visible: boolean;
|
||||
hideModal: () => void;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function HistoryVersionModal({
|
||||
visible,
|
||||
hideModal,
|
||||
id,
|
||||
}: HistoryVersionModalProps) {
|
||||
const { t } = useTranslate('flow');
|
||||
const { data, loading } = useFetchListVersion(id);
|
||||
const [selectedVersion, setSelectedVersion] = useState<any>(null);
|
||||
const { data: flow, loading: loadingVersion } = useFetchVersion(
|
||||
selectedVersion?.id,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!loading && data?.length > 0 && !selectedVersion) {
|
||||
setSelectedVersion(data[0]);
|
||||
}
|
||||
}, [data, loading, selectedVersion]);
|
||||
|
||||
const downloadfile = React.useCallback(
|
||||
function (e: any) {
|
||||
e.stopPropagation();
|
||||
console.log('Restore version:', selectedVersion);
|
||||
// Create a JSON blob and trigger download
|
||||
const jsonContent = JSON.stringify(flow?.dsl.graph, null, 2);
|
||||
const blob = new Blob([jsonContent], {
|
||||
type: 'application/json',
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${selectedVersion.filename || 'flow-version'}-${selectedVersion.id}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
[selectedVersion, flow?.dsl],
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Modal
|
||||
title={t('historyversion')}
|
||||
open={visible}
|
||||
width={'80vw'}
|
||||
onCancel={hideModal}
|
||||
footer={null}
|
||||
getContainer={() => document.body}
|
||||
>
|
||||
<Row gutter={16} style={{ height: '60vh' }}>
|
||||
<Col span={10} style={{ height: '100%', overflowY: 'auto' }}>
|
||||
{loading && <Spin />}
|
||||
{!loading && data.length === 0 && (
|
||||
<Empty description="No versions found" />
|
||||
)}
|
||||
{!loading && data.length > 0 && (
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={data}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
simple: true,
|
||||
}}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedVersion(item);
|
||||
}}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
background:
|
||||
selectedVersion?.id === item.id ? '#f0f5ff' : 'inherit',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={`${t('filename')}: ${item.title || '-'}`}
|
||||
description={item.created_at}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{/* Right panel - Version details */}
|
||||
<Col span={14} style={{ height: '100%', overflowY: 'auto' }}>
|
||||
{selectedVersion ? (
|
||||
<Card title={t('version.details')} bordered={false}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* Add actions for the selected version (restore, download, etc.) */}
|
||||
<Col span={24}>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Typography.Link onClick={downloadfile}>
|
||||
{t('version.download')}
|
||||
</Typography.Link>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Typography.Title level={4}>
|
||||
{selectedVersion.title || '-'}
|
||||
</Typography.Title>
|
||||
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ display: 'block', marginBottom: 16 }}
|
||||
>
|
||||
{t('version.created')}: {selectedVersion.create_date}
|
||||
</Typography.Text>
|
||||
|
||||
{/*render dsl form api*/}
|
||||
{loadingVersion && <Spin />}
|
||||
{!loadingVersion && flow?.dsl && (
|
||||
<ReactFlowProvider key={`flow-${selectedVersion.id}`}>
|
||||
<div
|
||||
style={{
|
||||
height: '400px',
|
||||
position: 'relative',
|
||||
zIndex: 0,
|
||||
}}
|
||||
>
|
||||
<ReactFlow
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
nodes={flow?.dsl.graph?.nodes || []}
|
||||
edges={
|
||||
flow?.dsl.graph?.edges.flatMap((x) => ({
|
||||
...x,
|
||||
type: 'default',
|
||||
})) || []
|
||||
}
|
||||
fitView
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={{}}
|
||||
zoomOnScroll={true}
|
||||
panOnDrag={true}
|
||||
zoomOnDoubleClick={false}
|
||||
preventScrolling={true}
|
||||
minZoom={0.1}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<Empty description={t('version.select')} />
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@ -6,6 +6,8 @@ const {
|
||||
getCanvas,
|
||||
getCanvasSSE,
|
||||
setCanvas,
|
||||
getListVersion,
|
||||
getVersion,
|
||||
listCanvas,
|
||||
resetCanvas,
|
||||
removeCanvas,
|
||||
@ -29,6 +31,14 @@ const methods = {
|
||||
url: setCanvas,
|
||||
method: 'post',
|
||||
},
|
||||
getListVersion: {
|
||||
url: getListVersion,
|
||||
method: 'get',
|
||||
},
|
||||
getVersion: {
|
||||
url: getVersion,
|
||||
method: 'get',
|
||||
},
|
||||
listCanvas: {
|
||||
url: listCanvas,
|
||||
method: 'get',
|
||||
@ -63,6 +73,6 @@ const methods = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const chatService = registerServer<keyof typeof methods>(methods, request);
|
||||
const flowService = registerServer<keyof typeof methods>(methods, request);
|
||||
|
||||
export default chatService;
|
||||
export default flowService;
|
||||
|
@ -127,6 +127,8 @@ export default {
|
||||
getCanvasSSE: `${api_host}/canvas/getsse`,
|
||||
removeCanvas: `${api_host}/canvas/rm`,
|
||||
setCanvas: `${api_host}/canvas/set`,
|
||||
getListVersion: `${api_host}/canvas/getlistversion`,
|
||||
getVersion: `${api_host}/canvas/getversion`,
|
||||
resetCanvas: `${api_host}/canvas/reset`,
|
||||
runCanvas: `${api_host}/canvas/completion`,
|
||||
testDbConnect: `${api_host}/canvas/test_db_connect`,
|
||||
|
Loading…
x
Reference in New Issue
Block a user