mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-03-16 20:37:50 +08:00
Feat: Display release status in agent version history. (#13479)
### What problem does this PR solve? Feat: Display release status in agent version history. ### 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:
@ -99,6 +99,7 @@ async def save():
|
||||
user_canvas_id=req["id"],
|
||||
dsl=req["dsl"],
|
||||
title=UserCanvasVersionService.build_version_title(getattr(current_user, "nickname", current_user.id), req.get("title")),
|
||||
release=req.get("release"),
|
||||
)
|
||||
replica_ok = CanvasReplicaService.replace_for_set(
|
||||
canvas_id=req["id"],
|
||||
@ -133,6 +134,25 @@ def get(canvas_id):
|
||||
)
|
||||
except ValueError as e:
|
||||
return get_data_error_result(message=str(e))
|
||||
|
||||
# Get the last publication time (latest released version's update_time)
|
||||
last_publish_time = None
|
||||
versions = UserCanvasVersionService.list_by_canvas_id(canvas_id)
|
||||
if versions:
|
||||
released_versions = [v for v in versions if v.release]
|
||||
if released_versions:
|
||||
# Sort by update_time descending and get the latest
|
||||
released_versions.sort(key=lambda x: x.update_time, reverse=True)
|
||||
last_publish_time = released_versions[0].update_time
|
||||
|
||||
# Add last_publish_time to response data
|
||||
if isinstance(c, dict):
|
||||
c["last_publish_time"] = last_publish_time
|
||||
else:
|
||||
# If c is a model object, convert to dict first
|
||||
c = c.to_dict()
|
||||
c["last_publish_time"] = last_publish_time
|
||||
|
||||
return get_json_result(data=c)
|
||||
|
||||
|
||||
|
||||
@ -1075,6 +1075,7 @@ class UserCanvasVersion(DataBaseModel):
|
||||
|
||||
title = CharField(max_length=255, null=True, help_text="Canvas title")
|
||||
description = TextField(null=True, help_text="Canvas description")
|
||||
release = BooleanField(null=False, help_text="is released", default=False, index=True)
|
||||
dsl = JSONField(null=True, default={})
|
||||
|
||||
class Meta:
|
||||
@ -1538,6 +1539,7 @@ def migrate_db():
|
||||
alter_db_add_column(migrator, "dialog", "tenant_rerank_id", IntegerField(null=True, help_text="id in tenant_llm", index=True))
|
||||
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))
|
||||
logging.disable(logging.NOTSET)
|
||||
# this is after re-enabling logging to allow logging changed user emails
|
||||
migrate_add_unique_email(migrator)
|
||||
|
||||
@ -19,7 +19,7 @@ import time
|
||||
from uuid import uuid4
|
||||
from agent.canvas import Canvas
|
||||
from api.db import CanvasCategory, TenantPermission
|
||||
from api.db.db_models import DB, CanvasTemplate, User, UserCanvas, API4Conversation
|
||||
from api.db.db_models import DB, CanvasTemplate, User, UserCanvas, API4Conversation, UserCanvasVersion
|
||||
from api.db.services.api_service import API4ConversationService
|
||||
from api.db.services.common_service import CommonService
|
||||
from common.misc_utils import get_uuid
|
||||
@ -173,7 +173,23 @@ class UserCanvasService(CommonService):
|
||||
count = agents.count()
|
||||
if page_number and items_per_page:
|
||||
agents = agents.paginate(page_number, items_per_page)
|
||||
return list(agents.dicts()), count
|
||||
|
||||
agents_list = list(agents.dicts())
|
||||
|
||||
# Get latest release time for each canvas
|
||||
if agents_list:
|
||||
canvas_ids = [a['id'] for a in agents_list]
|
||||
release_times = (
|
||||
UserCanvasVersion.select(UserCanvasVersion.user_canvas_id, fn.MAX(UserCanvasVersion.create_time).alias("release_time"))
|
||||
.where((UserCanvasVersion.user_canvas_id.in_(canvas_ids)) & (UserCanvasVersion.release))
|
||||
.group_by(UserCanvasVersion.user_canvas_id)
|
||||
)
|
||||
release_time_map = {r.user_canvas_id: r.release_time for r in release_times}
|
||||
|
||||
for agent in agents_list:
|
||||
agent['release_time'] = release_time_map.get(agent['id'])
|
||||
|
||||
return agents_list, count
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
|
||||
@ -45,7 +45,8 @@ class UserCanvasVersionService(CommonService):
|
||||
cls.model.create_date,
|
||||
cls.model.update_date,
|
||||
cls.model.user_canvas_id,
|
||||
cls.model.update_time]
|
||||
cls.model.update_time,
|
||||
cls.model.release]
|
||||
).where(cls.model.user_canvas_id == user_canvas_id)
|
||||
return user_canvas_version
|
||||
except DoesNotExist:
|
||||
@ -74,14 +75,14 @@ class UserCanvasVersionService(CommonService):
|
||||
@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:
|
||||
delete_ids = []
|
||||
for i in range(20, user_canvas_version.count()):
|
||||
delete_ids.append(user_canvas_version[i].id)
|
||||
# Only get unpublished versions (False or None), keep all released versions
|
||||
unpublished = cls.model.select().where(cls.model.user_canvas_id == user_canvas_id, (~cls.model.release) | (cls.model.release.is_null(True))).order_by(cls.model.create_time.desc())
|
||||
|
||||
# Only delete old unpublished versions beyond the limit
|
||||
if unpublished.count() > 20:
|
||||
delete_ids = [v.id for v in unpublished[20:]]
|
||||
cls.delete_by_ids(delete_ids)
|
||||
|
||||
return True
|
||||
except DoesNotExist:
|
||||
return None
|
||||
@ -90,12 +91,15 @@ class UserCanvasVersionService(CommonService):
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def save_or_replace_latest(cls, user_canvas_id, dsl, title=None, description=None):
|
||||
def save_or_replace_latest(cls, user_canvas_id, dsl, title=None, description=None, release=None):
|
||||
"""
|
||||
Persist a canvas snapshot into version history.
|
||||
|
||||
If the latest version has the same DSL content, update that version in place
|
||||
instead of creating a new row.
|
||||
|
||||
Exception: If the latest version is released (release=True) and current save is not,
|
||||
create a new version to protect the released version.
|
||||
"""
|
||||
try:
|
||||
normalized_dsl = cls._normalize_dsl(dsl)
|
||||
@ -107,11 +111,28 @@ class UserCanvasVersionService(CommonService):
|
||||
)
|
||||
|
||||
if latest and cls._normalize_dsl(latest.dsl) == normalized_dsl:
|
||||
# Protect released version: if latest is released and current is not,
|
||||
# create a new version instead of updating
|
||||
if latest.release and not release:
|
||||
insert_data = {"user_canvas_id": user_canvas_id, "dsl": normalized_dsl}
|
||||
if title is not None:
|
||||
insert_data["title"] = title
|
||||
if description is not None:
|
||||
insert_data["description"] = description
|
||||
if release is not None:
|
||||
insert_data["release"] = release
|
||||
cls.insert(**insert_data)
|
||||
cls.delete_all_versions(user_canvas_id)
|
||||
return None, True
|
||||
|
||||
# Normal case: update existing version
|
||||
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:
|
||||
update_data["release"] = release
|
||||
cls.update_by_id(latest.id, update_data)
|
||||
cls.delete_all_versions(user_canvas_id)
|
||||
return latest.id, False
|
||||
@ -121,6 +142,8 @@ class UserCanvasVersionService(CommonService):
|
||||
insert_data["title"] = title
|
||||
if description is not None:
|
||||
insert_data["description"] = description
|
||||
if release is not None:
|
||||
insert_data["release"] = release
|
||||
cls.insert(**insert_data)
|
||||
cls.delete_all_versions(user_canvas_id)
|
||||
return None, True
|
||||
|
||||
@ -2,6 +2,7 @@ import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface IProps {
|
||||
data: {
|
||||
@ -9,6 +10,7 @@ interface IProps {
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
update_time?: string | number;
|
||||
release_time?: number;
|
||||
};
|
||||
onClick?: () => void;
|
||||
moreDropdown: React.ReactNode;
|
||||
@ -16,6 +18,12 @@ interface IProps {
|
||||
icon?: React.ReactNode;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
function Time({ time }: { time: string | number | undefined }) {
|
||||
return (
|
||||
<p className="text-sm opacity-80 whitespace-nowrap">{formatDate(time)}</p>
|
||||
);
|
||||
}
|
||||
export function HomeCard({
|
||||
data,
|
||||
onClick,
|
||||
@ -24,6 +32,8 @@ export function HomeCard({
|
||||
icon,
|
||||
testId,
|
||||
}: IProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card
|
||||
as="article"
|
||||
@ -72,9 +82,20 @@ export function HomeCard({
|
||||
{data.description}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm opacity-80 whitespace-nowrap">
|
||||
{formatDate(data.update_time)}
|
||||
</p>
|
||||
{data.release_time ? (
|
||||
<section>
|
||||
<div className="flex items-center gap-2 text-sm opacity-80">
|
||||
{`${t('flow.lastSavedAt')}:`}
|
||||
<Time time={data.update_time}></Time>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm opacity-80">
|
||||
{`${t('flow.publishedAt')}:`}
|
||||
<Time time={data.release_time}></Time>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<Time time={data.update_time}></Time>
|
||||
)}
|
||||
{sharedBadge}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
2
web/src/custom.d.ts
vendored
2
web/src/custom.d.ts
vendored
@ -1,3 +1,5 @@
|
||||
type Nullable<T> = T | null;
|
||||
|
||||
declare module '*.md' {
|
||||
const content: string;
|
||||
export default content;
|
||||
|
||||
@ -529,7 +529,7 @@ export const useFetchInputForm = (componentId?: string) => {
|
||||
export const useFetchVersionList = () => {
|
||||
const { id } = useParams();
|
||||
const { data, isFetching: loading } = useQuery<
|
||||
Array<{ created_at: string; title: string; id: string }>
|
||||
Array<{ created_at: string; title: string; id: string; release?: boolean }>
|
||||
>({
|
||||
queryKey: [AgentApiAction.FetchVersionList],
|
||||
initialData: [],
|
||||
|
||||
@ -77,6 +77,9 @@ export declare interface IFlow {
|
||||
nickname: string;
|
||||
operator_permission: number;
|
||||
canvas_category: string;
|
||||
release?: boolean;
|
||||
release_time?: number;
|
||||
last_publish_time?: number;
|
||||
}
|
||||
|
||||
export interface IFlowTemplate {
|
||||
|
||||
@ -41,6 +41,7 @@ export declare interface IFlow {
|
||||
user_id: string;
|
||||
permission: string;
|
||||
nickname: string;
|
||||
release?: boolean;
|
||||
}
|
||||
|
||||
export interface IFlowTemplate {
|
||||
|
||||
@ -1500,6 +1500,7 @@ Example: Virtual Hosted Style`,
|
||||
other: 'Other',
|
||||
ingestionPipeline: 'Ingestion pipeline',
|
||||
agents: 'Agents',
|
||||
publishedAt: 'Published at',
|
||||
days: 'Days',
|
||||
beginInput: 'Begin input',
|
||||
ref: 'Variable',
|
||||
@ -1581,6 +1582,7 @@ Example: Virtual Hosted Style`,
|
||||
citeTip: 'citeTip',
|
||||
name: 'Name',
|
||||
nameMessage: 'Please input name',
|
||||
lastSavedAt: 'Last saved at',
|
||||
description: 'Description',
|
||||
descriptionMessage: 'This is an agent for a specific task.',
|
||||
examples: 'Examples',
|
||||
@ -2185,6 +2187,9 @@ This process aggregates variables from multiple branches into a single variable
|
||||
'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.',
|
||||
frameworkPrompts: 'Framework',
|
||||
release: 'Publish',
|
||||
production: 'Production',
|
||||
productionTooltip:
|
||||
'This version is published to production. Access it via the API or the embedded page.',
|
||||
confirmPublish: 'Confirm Publish',
|
||||
publishDescription: 'You are about to publish this data pipeline.',
|
||||
linkedDataset: 'Linked dataset',
|
||||
|
||||
@ -919,8 +919,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
githubDescription:
|
||||
'连接 GitHub,可同步 Pull Request 与 Issue 内容用于检索。',
|
||||
airtableDescription: '连接 Airtable,同步指定工作区下指定表格中的文件。',
|
||||
dingtalkAITableDescription:
|
||||
'连接钉钉AI表格,同步指定表格中的记录。',
|
||||
dingtalkAITableDescription: '连接钉钉AI表格,同步指定表格中的记录。',
|
||||
gitlabDescription:
|
||||
'连接 GitLab,同步仓库、Issue、合并请求(MR)及相关文档内容。',
|
||||
asanaDescription: '连接 Asana,同步工作区中的文件。',
|
||||
@ -1250,6 +1249,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
consumerApp: '消费者应用',
|
||||
other: '其他',
|
||||
agents: '智能体',
|
||||
publishedAt: '发布于',
|
||||
beginInput: '开始输入',
|
||||
seconds: '秒',
|
||||
ref: '引用变量',
|
||||
@ -1345,6 +1345,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
cite: '引用',
|
||||
citeTip: '引用',
|
||||
nameMessage: '请输入名称',
|
||||
lastSavedAt: '上次保存于',
|
||||
description: '描述',
|
||||
examples: '示例',
|
||||
to: '下一步',
|
||||
@ -1888,6 +1889,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
'在此处编写您的 SQL 查询。您可以使用变量、原始 SQL,或使用变量语法混合使用两者。',
|
||||
frameworkPrompts: '框架',
|
||||
release: '发布',
|
||||
production: '正式版',
|
||||
productionTooltip: '此版本已发布到生产环境。可通过 API 或嵌入页面访问。',
|
||||
createFromBlank: '从空白创建',
|
||||
createFromTemplate: '从模板创建',
|
||||
importJsonFile: '导入 JSON 文件',
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { IFlow } from '@/interfaces/database/agent';
|
||||
import { Operator } from '@/pages/agent/constant';
|
||||
import useGraphStore from '@/pages/agent/store';
|
||||
import { formatDate } from '@/utils/date';
|
||||
@ -16,7 +17,7 @@ import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface PublishConfirmDialogProps {
|
||||
agentDetail: { title: string; update_time?: number };
|
||||
agentDetail: IFlow;
|
||||
loading: boolean;
|
||||
onPublish: () => void;
|
||||
}
|
||||
@ -42,8 +43,8 @@ export function PublishConfirmDialog({
|
||||
}, [nodes]);
|
||||
|
||||
const lastPublished = useMemo(() => {
|
||||
if (agentDetail?.update_time) {
|
||||
return formatDate(agentDetail.update_time);
|
||||
if (agentDetail?.last_publish_time) {
|
||||
return formatDate(agentDetail.last_publish_time);
|
||||
}
|
||||
return '-';
|
||||
}, [agentDetail?.update_time]);
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
|
||||
import { Spin } from '@/components/ui/spin';
|
||||
import { RAGFlowTooltip } from '@/components/ui/tooltip';
|
||||
import { useClientPagination } from '@/hooks/logic-hooks/use-pagination';
|
||||
import {
|
||||
useFetchVersion,
|
||||
@ -25,6 +26,12 @@ import { ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { nodeTypes } from '../canvas';
|
||||
|
||||
function Dot() {
|
||||
return (
|
||||
<span className="w-2 h-2 inline-block rounded-full bg-accent-primary flex-shrink-0" />
|
||||
);
|
||||
}
|
||||
|
||||
export function VersionDialog({
|
||||
hideModal,
|
||||
}: IModalProps<any> & { initialName?: string; title?: ReactNode }) {
|
||||
@ -58,7 +65,7 @@ export function VersionDialog({
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent className="max-w-[60vw]">
|
||||
<DialogContent className="max-w-[900px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">
|
||||
{t('flow.historyVersion')}
|
||||
@ -78,7 +85,10 @@ export function VersionDialog({
|
||||
})}
|
||||
onClick={handleClick(x.id)}
|
||||
>
|
||||
{x.title}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate">{x.title}</span>
|
||||
{x.release && <Dot></Dot>}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -92,11 +102,24 @@ export function VersionDialog({
|
||||
<CardContent className="h-full p-5 flex flex-col">
|
||||
<section className="flex justify-between">
|
||||
<div>
|
||||
<div className="pb-1 truncate">{agent?.title}</div>
|
||||
<div className="flex">
|
||||
<span className="pb-1 truncate">{agent?.title}</span>
|
||||
{agent?.release && (
|
||||
<RAGFlowTooltip tooltip={t('flow.productionTooltip')}>
|
||||
<Button className="bg-accent-primary-5 ml-3">
|
||||
<Dot></Dot>
|
||||
<span className="text-accent-primary pl-2 rounded">
|
||||
{t('flow.production')}
|
||||
</span>
|
||||
</Button>
|
||||
</RAGFlowTooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-text-secondary text-xs">
|
||||
Created: {formatDate(agent?.create_date)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button variant={'ghost'} onClick={downloadFile}>
|
||||
<ArrowDownToLine />
|
||||
</Button>
|
||||
|
||||
@ -19,7 +19,12 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
|
||||
return (
|
||||
<HomeCard
|
||||
testId="agent-card"
|
||||
data={{ ...data, name: data.title, description: data.description || '' }}
|
||||
data={{
|
||||
...data,
|
||||
name: data.title,
|
||||
description: data.description || '',
|
||||
release_time: data.release_time,
|
||||
}}
|
||||
moreDropdown={
|
||||
<AgentDropdown showAgentRenameModal={showAgentRenameModal} agent={data}>
|
||||
<MoreButton></MoreButton>
|
||||
|
||||
Reference in New Issue
Block a user