diff --git a/api/apps/canvas_app.py b/api/apps/canvas_app.py index 0c4abe0596..21eff42366 100644 --- a/api/apps/canvas_app.py +++ b/api/apps/canvas_app.py @@ -24,6 +24,7 @@ from api.db import CanvasCategory from api.db.services.canvas_service import CanvasTemplateService, UserCanvasService, API4ConversationService from api.db.services.document_service import DocumentService from api.db.services.file_service import FileService +from api.db.services.knowledgebase_service import KnowledgebaseService from api.db.services.pipeline_operation_log_service import PipelineOperationLogService from api.db.services.task_service import queue_dataflow, CANVAS_DEBUG_DOC_ID, TaskService from api.db.services.user_service import TenantService @@ -153,6 +154,11 @@ def get(canvas_id): c = c.to_dict() c["last_publish_time"] = last_publish_time + # For pipeline type, get associated datasets + if c.get("canvas_category") == CanvasCategory.DataFlow: + datasets = list(KnowledgebaseService.query(pipeline_id=canvas_id)) + c["datasets"] = [{"id": d.id, "name": d.name, "avatar": d.avatar} for d in datasets] + return get_json_result(data=c) diff --git a/api/apps/sdk/session.py b/api/apps/sdk/session.py index 722965812c..7938841838 100644 --- a/api/apps/sdk/session.py +++ b/api/apps/sdk/session.py @@ -87,19 +87,20 @@ async def create(tenant_id, chat_id): async def create_agent_session(tenant_id, agent_id): req = await get_request_json() user_id = req.get("user_id") or request.args.get("user_id", tenant_id) - release_mode = req.get("release", request.args.get("release", False)) - e, cvs = UserCanvasService.get_by_id(agent_id) - if not e: - return get_error_data_result("Agent not found.") + release_mode = bool(req.get("release", request.args.get("release", False))) + if not UserCanvasService.query(user_id=tenant_id, id=agent_id): return get_error_data_result("You cannot access the agent.") - if not isinstance(cvs.dsl, str): - cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False) - if release_mode and not bool(cvs.release): - raise PermissionError("No available published version") + try: + cvs, dsl = UserCanvasService.get_agent_dsl_with_release(agent_id, release_mode, tenant_id) + except LookupError: + return get_error_data_result("Agent not found.") + except PermissionError as e: + return get_error_data_result(str(e)) + session_id = get_uuid() - canvas = Canvas(cvs.dsl, tenant_id, agent_id, canvas_id=cvs.id) + canvas = Canvas(dsl, tenant_id, agent_id, canvas_id=cvs.id) canvas.reset() cvs.dsl = json.loads(str(canvas)) diff --git a/api/db/services/canvas_service.py b/api/db/services/canvas_service.py index f6aa41c4a4..bd655d122b 100644 --- a/api/db/services/canvas_service.py +++ b/api/db/services/canvas_service.py @@ -22,6 +22,7 @@ from api.db import CanvasCategory, TenantPermission 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 api.db.services.user_canvas_version import UserCanvasVersionService from common.misc_utils import get_uuid from api.utils.api_utils import get_data_openai import tiktoken @@ -204,6 +205,27 @@ class UserCanvasService(CommonService): return False return True + @classmethod + def get_agent_dsl_with_release(cls, agent_id, release_mode=False, tenant_id=None): + e, cvs = cls.get_by_id(agent_id) + if not e: + raise LookupError("Agent not found.") + if tenant_id and cvs.user_id != tenant_id: + raise PermissionError("You do not own the agent.") + + if release_mode: + released_version = UserCanvasVersionService.get_latest_released(agent_id) + if not released_version: + raise PermissionError("No available published version") + dsl = released_version.dsl + else: + dsl = cvs.dsl + + if not isinstance(dsl, str): + dsl = json.dumps(dsl, ensure_ascii=False) + + return cvs, dsl + async def completion(tenant_id, agent_id, session_id=None, **kwargs): query = kwargs.get("query", "") or kwargs.get("question", "") @@ -223,28 +245,12 @@ async def completion(tenant_id, agent_id, session_id=None, **kwargs): conv.dsl = json.dumps(conv.dsl, ensure_ascii=False) canvas = Canvas(conv.dsl, tenant_id, agent_id, canvas_id=agent_id, custom_header=custom_header) else: - e, cvs = UserCanvasService.get_by_id(agent_id) - if not e: - raise LookupError("Agent not found.") - if cvs.user_id != tenant_id: - raise PermissionError("You do not own the agent.") - if release_mode == "true" and not bool(cvs.release): - raise PermissionError("No available published version") - if not isinstance(cvs.dsl, str): - cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False) + cvs, dsl = UserCanvasService.get_agent_dsl_with_release(agent_id, release_mode=release_mode == "true", tenant_id=tenant_id) - session_id=get_uuid() - canvas = Canvas(cvs.dsl, tenant_id, agent_id, canvas_id=cvs.id, custom_header=custom_header) + 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": cvs.dsl, - "reference": [] - } + conv = {"id": session_id, "dialog_id": cvs.id, "user_id": user_id, "message": [], "source": "agent", "dsl": dsl, "reference": []} 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 d2861d576e..067122e9b7 100644 --- a/api/db/services/user_canvas_version.py +++ b/api/db/services/user_canvas_version.py @@ -89,6 +89,17 @@ class UserCanvasVersionService(CommonService): except Exception: return None + @classmethod + @DB.connection_context() + def get_latest_released(cls, user_canvas_id): + 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() + except DoesNotExist: + return None + except Exception as e: + logging.exception(e) + return None + @classmethod @DB.connection_context() def save_or_replace_latest(cls, user_canvas_id, dsl, title=None, description=None, release=None): diff --git a/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py b/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py index a83a3564d4..109889b4b6 100644 --- a/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py +++ b/test/testcases/test_http_api/test_session_management/test_session_sdk_routes_unit.py @@ -543,12 +543,15 @@ def test_create_and_update_guard_matrix(monkeypatch): assert "Fail to create a session" in res["message"] monkeypatch.setattr(module, "request", SimpleNamespace(args=_Args())) - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (False, None)) + monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [SimpleNamespace(id="agent-1")]) + + def _raise_lookup(*_args, **_kwargs): + raise LookupError("Agent not found.") + + monkeypatch.setattr(module.UserCanvasService, "get_agent_dsl_with_release", _raise_lookup) res = _run(inspect.unwrap(module.create_agent_session)("tenant-1", "agent-1")) assert res["message"] == "Agent not found." - canvas = SimpleNamespace(dsl="{}", id="agent-1") - monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, canvas)) monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: []) res = _run(inspect.unwrap(module.create_agent_session)("tenant-1", "agent-1")) assert res["message"] == "You cannot access the agent." 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 540453e415..4431575800 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 @@ -183,6 +183,14 @@ def _load_canvas_module(monkeypatch): api_pkg.__path__ = [str(repo_root / "api")] monkeypatch.setitem(sys.modules, "api", api_pkg) + db_pkg = ModuleType("api.db") + db_pkg.__path__ = [str(repo_root / "api" / "db")] + monkeypatch.setitem(sys.modules, "api.db", db_pkg) + + db_services_pkg = ModuleType("api.db.services") + db_services_pkg.__path__ = [str(repo_root / "api" / "db" / "services")] + monkeypatch.setitem(sys.modules, "api.db.services", db_services_pkg) + apps_mod = ModuleType("api.apps") apps_mod.__path__ = [] apps_mod.current_user = SimpleNamespace(id="user-1") @@ -327,6 +335,12 @@ def _load_canvas_module(monkeypatch): ) monkeypatch.setitem(sys.modules, "api.db.services.file_service", file_service_mod) + knowledgebase_service_mod = ModuleType("api.db.services.knowledgebase_service") + knowledgebase_service_mod.KnowledgebaseService = SimpleNamespace( + query=lambda **_kwargs: [], + ) + monkeypatch.setitem(sys.modules, "api.db.services.knowledgebase_service", knowledgebase_service_mod) + pipeline_log_service_mod = ModuleType("api.db.services.pipeline_operation_log_service") pipeline_log_service_mod.PipelineOperationLogService = SimpleNamespace( get_documents_info=lambda *_args, **_kwargs: [], diff --git a/web/src/components/home-card.tsx b/web/src/components/home-card.tsx index 9e57a355f3..cefd9434b6 100644 --- a/web/src/components/home-card.tsx +++ b/web/src/components/home-card.tsx @@ -17,12 +17,11 @@ interface IProps { sharedBadge?: ReactNode; icon?: React.ReactNode; testId?: string; + showReleaseTime?: boolean; } function Time({ time }: { time: string | number | undefined }) { - return ( -

{formatDate(time)}

- ); + return

{formatDate(time)}

; } export function HomeCard({ data, @@ -31,6 +30,7 @@ export function HomeCard({ sharedBadge, icon, testId, + showReleaseTime = false, }: IProps) { const { t } = useTranslation(); @@ -82,16 +82,18 @@ export function HomeCard({ {data.description}
- {data.release_time ? ( -
-
+ {showReleaseTime ? ( +
+
{`${t('flow.lastSavedAt')}:`}
-
- {`${t('flow.publishedAt')}:`} - -
+ {data.release_time && ( +
+ {`${t('flow.publishedAt')}:`} + +
+ )}
) : ( diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index c9a08b7f20..8ca9d81942 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -33,6 +33,7 @@ export interface ISwitchForm { import { AgentCategory } from '@/constants/agent'; import { Edge, Node } from '@xyflow/react'; import { IReference, Message } from './chat'; +import { IKnowledge } from './knowledge'; export type DSLComponents = Record; @@ -80,6 +81,7 @@ export declare interface IFlow { release?: boolean; release_time?: number; last_publish_time?: number; + datasets?: Pick[]; } export interface IFlowTemplate { diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 0671ac33a0..fc813671df 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -2196,9 +2196,11 @@ This process aggregates variables from multiple branches into a single variable 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', + confirmPublish: 'Confirm publish', + publishIngestionPipeline: + 'You are about to publish this Ingestion pipeline.', + publishAgent: 'You are about to publish this agent', + linkedDataset: 'Linked dataset:', lastPublished: 'Last published', createFromBlank: 'Create from blank', createFromTemplate: 'Create from template', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 3d49e89003..9a129df7c1 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -202,7 +202,7 @@ export default { // editMetadataForDataset: '查看和编辑元数据 ', restrictDefinedValues: '限制为已定义的值', metadataGenerationSettings: '元数据生成设置', - // manageMetadataForDataset: '管理此数据集的元数据', + // manageMetadataForDataset: '管理此知识库的元数据', manageMetadata: '管理元数据', metadata: '元数据', values: '值', @@ -247,7 +247,7 @@ export default { startDate: '开始时间', source: '来源', fileName: '文件名', - datasetLogs: '数据集', + datasetLogs: '知识库', fileLogs: '文件', overview: '日志', success: '成功', @@ -390,7 +390,7 @@ export default { knowledgeConfiguration: { randomSeedTip: '种子是伪随机算法的起点,它确保在不同运行中产生相同的输出,从而保证可重复性。', - datasetDescription: '你的数据集描述。', + datasetDescription: '你的知识库描述。', overlappedPercentTip: '相邻两个块之间的重叠百分比', settings: '设置', autoMetadataTip: @@ -430,13 +430,13 @@ export default { baseInfo: '基础信息', globalIndex: '全局索引', dataSource: '数据源', - linkSourceSetTip: '管理与此数据集的数据源链接', + linkSourceSetTip: '管理与此知识库的数据源链接', linkDataSource: '链接数据源', tocExtractionTip: '对于已有的chunk生成层级结构的目录信息(每个文件一个目录)。在查询时,激活`Page Index`后,系统会用大模型去判断用户问题和哪些目录项相关,从而找到相关的chunk。', deleteGenerateModalContent: `

删除生成的 {{type}} 结果 - 将从此数据集中移除所有派生实体和关系。 + 将从此知识库中移除所有派生实体和关系。 您的原始文件将保持不变。


是否要继续? @@ -449,7 +449,7 @@ export default { setDefaultTip: '', setDefault: '设置默认', editLinkDataPipeline: '编辑pipeline', - linkPipelineSetTip: '管理与此数据集的数据管道链接', + linkPipelineSetTip: '管理与此知识库的数据管道链接', default: '默认', dataPipeline: '切换或配置 ingestion pipeline。', linkDataPipeline: '关联pipeline', @@ -630,7 +630,7 @@ export default { tagSetTip: `

请选择一个或多个标签集或标签知识库,用于对知识库中的每个文本块进行标记。

对这些文本块的查询也将自动关联相应标签。

-

此功能基于文本相似度,能够为数据集的文本块批量添加更多领域知识,从而显著提高检索准确性。该功能还能提升大量文本块的操作效率。

+

此功能基于文本相似度,能够为知识库的文本块批量添加更多领域知识,从而显著提高检索准确性。该功能还能提升大量文本块的操作效率。

为了更好地理解标签集的作用,以下是标签集和关键词之间的主要区别:

  • 标签集是一个由用户定义和管理的封闭集,而自动生成的关键词属于开放集合。
  • @@ -704,7 +704,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 assistantAvatar: '助理头像', language: '语言', emptyResponse: '空回复', - emptyResponsePlaceholder: '在数据集中未找到您要寻找的答案!', + emptyResponsePlaceholder: '在知识库中未找到您要寻找的答案!', emptyResponseTip: `如果在知识库中没有检索到用户的问题,它将使用它作为答案。 如果您希望 LLM 在未检索到任何内容时提出自己的意见,请将此留空。`, emptyResponseMessage: `当知识库中未检索到任何相关信息时,将触发空响应。由于未选择任何知识库,因此请清除“空响应”。`, setAnOpener: '设置开场白', @@ -719,7 +719,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 systemPlaceholder: `你是一个智能助手,主要功能是基于提供的知识库严格回答问题。 **重要规则:** - - 你的回答必须**仅**来自此数据集:{knowledge}。 + - 你的回答必须**仅**来自此知识库:{knowledge}。 - **当信息可用时**: 总结内容以给出详细答案。 - **当信息不可用时**: 你的回答必须包含这句确切的话:"在知识库中未找到您要的答案!" - **始终考虑**整个对话历史。`, @@ -1893,6 +1893,11 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 release: '发布', production: '正式版', productionTooltip: '此版本已发布到生产环境。可通过 API 或嵌入页面访问。', + confirmPublish: '确认发布', + publishIngestionPipeline: '您即将发布此 Ingestion pipeline。', + publishAgent: '您即将发布此智能体', + linkedDataset: '已关联的知识库:', + lastPublished: '上次发布时间', createFromBlank: '从空白创建', createFromTemplate: '从模板创建', importJsonFile: '导入 JSON 文件', @@ -1920,7 +1925,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 regularExpressions: '正则表达式', overlappedPercent: '重叠百分比(%)', searchMethod: '搜索方法', - searchMethodTip: `决定该数据集启用的搜索方式,可选择全文、向量,或两者兼有。 + searchMethodTip: `决定该知识库启用的搜索方式,可选择全文、向量,或两者兼有。 Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, filenameEmbdWeight: '文件名嵌入权重', parserMethod: '解析方法', @@ -2178,7 +2183,7 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, changeStepModalCancelText: '取消', unlinkPipelineModalTitle: '解绑pipeline', unlinkPipelineModalContent: ` -

    一旦取消链接,该数据集将不再连接到当前数据管道。

    +

    一旦取消链接,该知识库将不再连接到当前数据管道。

    正在解析的文件将继续解析,直到完成。

    尚未解析的文件将不再被处理。


    你确定要继续吗?

    `, @@ -2217,8 +2222,8 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, noMCP: '暂无 MCP 服务器可用', agentTitle: '尚未创建智能体', notFoundAgent: '未查询到智能体', - datasetTitle: '尚未创建数据集', - notFoundDataset: '未查询到数据集', + datasetTitle: '尚未创建知识库', + notFoundDataset: '未查询到知识库', chatTitle: '尚未创建聊天应用', notFoundChat: '未查询到聊天应用', searchTitle: '尚未创建搜索应用', diff --git a/web/src/pages/agent/components/publish-confirm-dialog.tsx b/web/src/pages/agent/components/publish-confirm-dialog.tsx index 85fe297515..5fa9f03c98 100644 --- a/web/src/pages/agent/components/publish-confirm-dialog.tsx +++ b/web/src/pages/agent/components/publish-confirm-dialog.tsx @@ -1,3 +1,4 @@ +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; import { Button, ButtonLoading } from '@/components/ui/button'; import { Dialog, @@ -9,12 +10,12 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { IFlow } from '@/interfaces/database/agent'; -import { Operator } from '@/pages/agent/constant'; -import useGraphStore from '@/pages/agent/store'; +import { IKnowledge } from '@/interfaces/database/knowledge'; import { formatDate } from '@/utils/date'; import { BookPlus } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useIsPipeline } from '../hooks/use-is-pipeline'; interface PublishConfirmDialogProps { agentDetail: IFlow; @@ -22,6 +23,43 @@ interface PublishConfirmDialogProps { onPublish: () => void; } +function AssociatedDataset({ + associatedDatasets, +}: { + associatedDatasets: Pick[]; +}) { + const { t } = useTranslation(); + + return ( +
    +
    + {t('flow.linkedDataset')} +
    + {associatedDatasets.length > 0 ? ( +
    + {associatedDatasets.map((dataset) => ( +
    + + + {dataset.name} + +
    + ))} +
    + ) : ( +
    {t('common.noData')}
    + )} +
    + ); +} + export function PublishConfirmDialog({ agentDetail, loading, @@ -29,30 +67,28 @@ export function PublishConfirmDialog({ }: PublishConfirmDialogProps) { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const nodes = useGraphStore((state) => state.nodes); - - const linkedDatasets = useMemo(() => { - const datasets: string[] = []; - nodes.forEach((node) => { - if (node.data.label === Operator.Retrieval) { - const kbIds = node.data.form?.kb_ids || []; - datasets.push(...kbIds); - } - }); - return [...new Set(datasets)]; - }, [nodes]); + const isPipeline = useIsPipeline(); const lastPublished = useMemo(() => { if (agentDetail?.last_publish_time) { return formatDate(agentDetail.last_publish_time); } - return '-'; + return ''; }, [agentDetail?.update_time]); - const handleConfirmPublish = () => { + // Get datasets associated with this pipeline from API response + const associatedDatasets = useMemo(() => { + return agentDetail?.datasets || []; + }, [agentDetail?.datasets]); + + const handleConfirmPublish = useCallback(() => { onPublish(); setOpen(false); - }; + }, []); + + if (isPipeline) { + return null; + } return ( @@ -66,29 +102,39 @@ export function PublishConfirmDialog({ {t('flow.confirmPublish')} -
    -
    - - {agentDetail.title} - +
    +
    + {t( + `flow.${isPipeline ? 'publishIngestionPipeline' : 'publishAgent'}`, + )}
    + +
    +
    + + + {agentDetail.title} + +
    + + {isPipeline && ( + + )} +
    +
    -
    - - {t('flow.linkedDataset')} - - - {linkedDatasets.length > 0 - ? linkedDatasets.join(', ') - : t('common.none')} - -
    -
    - - {t('flow.lastPublished')} - - {lastPublished} -
    + {lastPublished && ( +
    + {t('flow.lastPublished')}: + {lastPublished} +
    + )}
    diff --git a/web/src/pages/agents/agent-card.tsx b/web/src/pages/agents/agent-card.tsx index a9ee760802..2126475b23 100644 --- a/web/src/pages/agents/agent-card.tsx +++ b/web/src/pages/agents/agent-card.tsx @@ -44,6 +44,7 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) { ) } + showReleaseTime /> ); }