Merge branch 'main' of https://github.com/infiniflow/ragflow into refa/clean_agent_with_tools_sync

This commit is contained in:
yongtenglei 2025-12-04 13:40:54 +08:00
commit ed073c005e
29 changed files with 468 additions and 85 deletions

View file

@ -91,9 +91,6 @@ class Graph:
def load(self): def load(self):
self.components = self.dsl["components"] self.components = self.dsl["components"]
cpn_nms = set([]) cpn_nms = set([])
for k, cpn in self.components.items():
cpn_nms.add(cpn["obj"]["component_name"])
for k, cpn in self.components.items(): for k, cpn in self.components.items():
cpn_nms.add(cpn["obj"]["component_name"]) cpn_nms.add(cpn["obj"]["component_name"])
param = component_class(cpn["obj"]["component_name"] + "Param")() param = component_class(cpn["obj"]["component_name"] + "Param")()

View file

@ -321,9 +321,7 @@ async def update_doc(tenant_id, dataset_id, document_id):
try: try:
if not DocumentService.update_by_id(doc.id, {"status": str(status)}): if not DocumentService.update_by_id(doc.id, {"status": str(status)}):
return get_error_data_result(message="Database error (Document update)!") return get_error_data_result(message="Database error (Document update)!")
settings.docStoreConn.update({"doc_id": doc.id}, {"available_int": status}, search.index_name(kb.tenant_id), doc.kb_id) settings.docStoreConn.update({"doc_id": doc.id}, {"available_int": status}, search.index_name(kb.tenant_id), doc.kb_id)
return get_result(data=True)
except Exception as e: except Exception as e:
return server_error_response(e) return server_error_response(e)
@ -350,12 +348,10 @@ async def update_doc(tenant_id, dataset_id, document_id):
} }
renamed_doc = {} renamed_doc = {}
for key, value in doc.to_dict().items(): for key, value in doc.to_dict().items():
if key == "run":
renamed_doc["run"] = run_mapping.get(str(value))
new_key = key_mapping.get(key, key) new_key = key_mapping.get(key, key)
renamed_doc[new_key] = value renamed_doc[new_key] = value
if key == "run": if key == "run":
renamed_doc["run"] = run_mapping.get(value) renamed_doc["run"] = run_mapping.get(str(value))
return get_result(data=renamed_doc) return get_result(data=renamed_doc)

View file

@ -148,6 +148,7 @@ class Storage(Enum):
AWS_S3 = 4 AWS_S3 = 4
OSS = 5 OSS = 5
OPENDAL = 6 OPENDAL = 6
GCS = 7
# environment # environment
# ENV_STRONG_TEST_COUNT = "STRONG_TEST_COUNT" # ENV_STRONG_TEST_COUNT = "STRONG_TEST_COUNT"

View file

@ -31,6 +31,7 @@ import rag.utils.ob_conn
import rag.utils.opensearch_conn import rag.utils.opensearch_conn
from rag.utils.azure_sas_conn import RAGFlowAzureSasBlob from rag.utils.azure_sas_conn import RAGFlowAzureSasBlob
from rag.utils.azure_spn_conn import RAGFlowAzureSpnBlob from rag.utils.azure_spn_conn import RAGFlowAzureSpnBlob
from rag.utils.gcs_conn import RAGFlowGCS
from rag.utils.minio_conn import RAGFlowMinio from rag.utils.minio_conn import RAGFlowMinio
from rag.utils.opendal_conn import OpenDALStorage from rag.utils.opendal_conn import OpenDALStorage
from rag.utils.s3_conn import RAGFlowS3 from rag.utils.s3_conn import RAGFlowS3
@ -109,6 +110,7 @@ MINIO = {}
OB = {} OB = {}
OSS = {} OSS = {}
OS = {} OS = {}
GCS = {}
DOC_MAXIMUM_SIZE: int = 128 * 1024 * 1024 DOC_MAXIMUM_SIZE: int = 128 * 1024 * 1024
DOC_BULK_SIZE: int = 4 DOC_BULK_SIZE: int = 4
@ -151,7 +153,8 @@ class StorageFactory:
Storage.AZURE_SAS: RAGFlowAzureSasBlob, Storage.AZURE_SAS: RAGFlowAzureSasBlob,
Storage.AWS_S3: RAGFlowS3, Storage.AWS_S3: RAGFlowS3,
Storage.OSS: RAGFlowOSS, Storage.OSS: RAGFlowOSS,
Storage.OPENDAL: OpenDALStorage Storage.OPENDAL: OpenDALStorage,
Storage.GCS: RAGFlowGCS,
} }
@classmethod @classmethod
@ -250,7 +253,7 @@ def init_settings():
else: else:
raise Exception(f"Not supported doc engine: {DOC_ENGINE}") raise Exception(f"Not supported doc engine: {DOC_ENGINE}")
global AZURE, S3, MINIO, OSS global AZURE, S3, MINIO, OSS, GCS
if STORAGE_IMPL_TYPE in ['AZURE_SPN', 'AZURE_SAS']: if STORAGE_IMPL_TYPE in ['AZURE_SPN', 'AZURE_SAS']:
AZURE = get_base_config("azure", {}) AZURE = get_base_config("azure", {})
elif STORAGE_IMPL_TYPE == 'AWS_S3': elif STORAGE_IMPL_TYPE == 'AWS_S3':
@ -259,6 +262,8 @@ def init_settings():
MINIO = decrypt_database_config(name="minio") MINIO = decrypt_database_config(name="minio")
elif STORAGE_IMPL_TYPE == 'OSS': elif STORAGE_IMPL_TYPE == 'OSS':
OSS = get_base_config("oss", {}) OSS = get_base_config("oss", {})
elif STORAGE_IMPL_TYPE == 'GCS':
GCS = get_base_config("gcs", {})
global STORAGE_IMPL global STORAGE_IMPL
STORAGE_IMPL = StorageFactory.create(Storage[STORAGE_IMPL_TYPE]) STORAGE_IMPL = StorageFactory.create(Storage[STORAGE_IMPL_TYPE])

View file

@ -60,6 +60,8 @@ user_default_llm:
# access_key: 'access_key' # access_key: 'access_key'
# secret_key: 'secret_key' # secret_key: 'secret_key'
# region: 'region' # region: 'region'
#gcs:
# bucket: 'bridgtl-edm-d-bucket-ragflow'
# oss: # oss:
# access_key: 'access_key' # access_key: 'access_key'
# secret_key: 'secret_key' # secret_key: 'secret_key'

View file

@ -86,9 +86,11 @@ class Pdf(PdfParser):
# (A) Add text # (A) Add text
for b in self.boxes: for b in self.boxes:
if not (from_page < b["page_number"] <= to_page + from_page): # b["page_number"] is relative page numbermust + from_page
global_page_num = b["page_number"] + from_page
if not (from_page < global_page_num <= to_page + from_page):
continue continue
page_items[b["page_number"]].append({ page_items[global_page_num].append({
"top": b["top"], "top": b["top"],
"x0": b["x0"], "x0": b["x0"],
"text": b["text"], "text": b["text"],
@ -100,7 +102,6 @@ class Pdf(PdfParser):
if not positions: if not positions:
continue continue
# Handle content type (list vs str)
if isinstance(content, list): if isinstance(content, list):
final_text = "\n".join(content) final_text = "\n".join(content)
elif isinstance(content, str): elif isinstance(content, str):
@ -109,10 +110,11 @@ class Pdf(PdfParser):
final_text = str(content) final_text = str(content)
try: try:
# Parse positions
pn_index = positions[0][0] pn_index = positions[0][0]
if isinstance(pn_index, list): if isinstance(pn_index, list):
pn_index = pn_index[0] pn_index = pn_index[0]
# pn_index in tbls is absolute page number
current_page_num = int(pn_index) + 1 current_page_num = int(pn_index) + 1
except Exception as e: except Exception as e:
print(f"Error parsing position: {e}") print(f"Error parsing position: {e}")

207
rag/utils/gcs_conn.py Normal file
View file

@ -0,0 +1,207 @@
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging
import time
import datetime
from io import BytesIO
from google.cloud import storage
from google.api_core.exceptions import NotFound
from common.decorator import singleton
from common import settings
@singleton
class RAGFlowGCS:
def __init__(self):
self.client = None
self.bucket_name = None
self.__open__()
def __open__(self):
try:
if self.client:
self.client = None
except Exception:
pass
try:
self.client = storage.Client()
self.bucket_name = settings.GCS["bucket"]
except Exception:
logging.exception("Fail to connect to GCS")
def _get_blob_path(self, folder, filename):
"""Helper to construct the path: folder/filename"""
if not folder:
return filename
return f"{folder}/{filename}"
def health(self):
folder, fnm, binary = "ragflow-health", "health_check", b"_t@@@1"
try:
bucket_obj = self.client.bucket(self.bucket_name)
if not bucket_obj.exists():
logging.error(f"Health check failed: Main bucket '{self.bucket_name}' does not exist.")
return False
blob_path = self._get_blob_path(folder, fnm)
blob = bucket_obj.blob(blob_path)
blob.upload_from_file(BytesIO(binary), content_type='application/octet-stream')
return True
except Exception as e:
logging.exception(f"Health check failed: {e}")
return False
def put(self, bucket, fnm, binary, tenant_id=None):
# RENAMED PARAMETER: bucket_name -> bucket (to match interface)
for _ in range(3):
try:
bucket_obj = self.client.bucket(self.bucket_name)
blob_path = self._get_blob_path(bucket, fnm)
blob = bucket_obj.blob(blob_path)
blob.upload_from_file(BytesIO(binary), content_type='application/octet-stream')
return True
except NotFound:
logging.error(f"Fail to put: Main bucket {self.bucket_name} does not exist.")
return False
except Exception:
logging.exception(f"Fail to put {bucket}/{fnm}:")
self.__open__()
time.sleep(1)
return False
def rm(self, bucket, fnm, tenant_id=None):
# RENAMED PARAMETER: bucket_name -> bucket
try:
bucket_obj = self.client.bucket(self.bucket_name)
blob_path = self._get_blob_path(bucket, fnm)
blob = bucket_obj.blob(blob_path)
blob.delete()
except NotFound:
pass
except Exception:
logging.exception(f"Fail to remove {bucket}/{fnm}:")
def get(self, bucket, filename, tenant_id=None):
# RENAMED PARAMETER: bucket_name -> bucket
for _ in range(1):
try:
bucket_obj = self.client.bucket(self.bucket_name)
blob_path = self._get_blob_path(bucket, filename)
blob = bucket_obj.blob(blob_path)
return blob.download_as_bytes()
except NotFound:
logging.warning(f"File not found {bucket}/{filename} in {self.bucket_name}")
return None
except Exception:
logging.exception(f"Fail to get {bucket}/{filename}")
self.__open__()
time.sleep(1)
return None
def obj_exist(self, bucket, filename, tenant_id=None):
# RENAMED PARAMETER: bucket_name -> bucket
try:
bucket_obj = self.client.bucket(self.bucket_name)
blob_path = self._get_blob_path(bucket, filename)
blob = bucket_obj.blob(blob_path)
return blob.exists()
except Exception:
logging.exception(f"obj_exist {bucket}/{filename} got exception")
return False
def bucket_exists(self, bucket):
# RENAMED PARAMETER: bucket_name -> bucket
try:
bucket_obj = self.client.bucket(self.bucket_name)
return bucket_obj.exists()
except Exception:
logging.exception(f"bucket_exist check for {self.bucket_name} got exception")
return False
def get_presigned_url(self, bucket, fnm, expires, tenant_id=None):
# RENAMED PARAMETER: bucket_name -> bucket
for _ in range(10):
try:
bucket_obj = self.client.bucket(self.bucket_name)
blob_path = self._get_blob_path(bucket, fnm)
blob = bucket_obj.blob(blob_path)
expiration = expires
if isinstance(expires, int):
expiration = datetime.timedelta(seconds=expires)
url = blob.generate_signed_url(
version="v4",
expiration=expiration,
method="GET"
)
return url
except Exception:
logging.exception(f"Fail to get_presigned {bucket}/{fnm}:")
self.__open__()
time.sleep(1)
return None
def remove_bucket(self, bucket):
# RENAMED PARAMETER: bucket_name -> bucket
try:
bucket_obj = self.client.bucket(self.bucket_name)
prefix = f"{bucket}/"
blobs = list(self.client.list_blobs(self.bucket_name, prefix=prefix))
if blobs:
bucket_obj.delete_blobs(blobs)
except Exception:
logging.exception(f"Fail to remove virtual bucket (folder) {bucket}")
def copy(self, src_bucket, src_path, dest_bucket, dest_path):
# RENAMED PARAMETERS to match original interface
try:
bucket_obj = self.client.bucket(self.bucket_name)
src_blob_path = self._get_blob_path(src_bucket, src_path)
dest_blob_path = self._get_blob_path(dest_bucket, dest_path)
src_blob = bucket_obj.blob(src_blob_path)
if not src_blob.exists():
logging.error(f"Source object not found: {src_blob_path}")
return False
bucket_obj.copy_blob(src_blob, bucket_obj, dest_blob_path)
return True
except NotFound:
logging.error(f"Copy failed: Main bucket {self.bucket_name} does not exist.")
return False
except Exception:
logging.exception(f"Fail to copy {src_bucket}/{src_path} -> {dest_bucket}/{dest_path}")
return False
def move(self, src_bucket, src_path, dest_bucket, dest_path):
try:
if self.copy(src_bucket, src_path, dest_bucket, dest_path):
self.rm(src_bucket, src_path)
return True
else:
logging.error(f"Copy failed, move aborted: {src_bucket}/{src_path}")
return False
except Exception:
logging.exception(f"Fail to move {src_bucket}/{src_path} -> {dest_bucket}/{dest_path}")
return False

View file

@ -1,8 +1,9 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import './css/cloud9_night.less'; import './css/cloud9_night.less';
import './css/index.less'; import './css/index.less';
import { JsonEditorOptions, JsonEditorProps } from './interface'; import { JsonEditorOptions, JsonEditorProps } from './interface';
const defaultConfig: JsonEditorOptions = { const defaultConfig: JsonEditorOptions = {
mode: 'code', mode: 'code',
modes: ['tree', 'code'], modes: ['tree', 'code'],
@ -14,6 +15,7 @@ const defaultConfig: JsonEditorOptions = {
enableTransform: false, enableTransform: false,
indentation: 2, indentation: 2,
}; };
const JsonEditor: React.FC<JsonEditorProps> = ({ const JsonEditor: React.FC<JsonEditorProps> = ({
value, value,
onChange, onChange,
@ -25,43 +27,62 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const currentLanguageRef = useRef<string>(i18n.language); const currentLanguageRef = useRef<string>(i18n.language);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { let isMounted = true;
const JSONEditor = require('jsoneditor');
import('jsoneditor/dist/jsoneditor.min.css');
if (containerRef.current) { const initEditor = async () => {
// Default configuration options if (typeof window !== 'undefined') {
const defaultOptions: JsonEditorOptions = { try {
...defaultConfig, const JSONEditorModule = await import('jsoneditor');
language: i18n.language === 'zh' ? 'zh-CN' : 'en', const JSONEditor = JSONEditorModule.default || JSONEditorModule;
onChange: () => {
if (editorRef.current && onChange) { await import('jsoneditor/dist/jsoneditor.min.css');
try {
const updatedJson = editorRef.current.get(); if (isMounted && containerRef.current) {
onChange(updatedJson); // Default configuration options
} catch (err) { const defaultOptions: JsonEditorOptions = {
// Do not trigger onChange when parsing error occurs ...defaultConfig,
console.error(err); language: i18n.language === 'zh' ? 'zh-CN' : 'en',
} onChange: () => {
if (editorRef.current && onChange) {
try {
const updatedJson = editorRef.current.get();
onChange(updatedJson);
} catch (err) {
// Do not trigger onChange when parsing error occurs
console.error(err);
}
}
},
...options, // Merge user provided options with defaults
};
editorRef.current = new JSONEditor(
containerRef.current,
defaultOptions,
);
if (value) {
editorRef.current.set(value);
} }
},
...options, // Merge user provided options with defaults
};
editorRef.current = new JSONEditor( setIsLoading(false);
containerRef.current, }
defaultOptions, } catch (error) {
); console.error('Failed to load jsoneditor:', error);
if (isMounted) {
if (value) { setIsLoading(false);
editorRef.current.set(value); }
} }
} }
} };
initEditor();
return () => { return () => {
isMounted = false;
if (editorRef.current) { if (editorRef.current) {
if (typeof editorRef.current.destroy === 'function') { if (typeof editorRef.current.destroy === 'function') {
editorRef.current.destroy(); editorRef.current.destroy();
@ -92,26 +113,38 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
} }
// Recreate the editor with new language // Recreate the editor with new language
const JSONEditor = require('jsoneditor'); const initEditorWithNewLanguage = async () => {
try {
const JSONEditorModule = await import('jsoneditor');
const JSONEditor = JSONEditorModule.default || JSONEditorModule;
const newOptions: JsonEditorOptions = { const newOptions: JsonEditorOptions = {
...defaultConfig, ...defaultConfig,
language: i18n.language === 'zh' ? 'zh-CN' : 'en', language: i18n.language === 'zh' ? 'zh-CN' : 'en',
onChange: () => { onChange: () => {
if (editorRef.current && onChange) { if (editorRef.current && onChange) {
try { try {
const updatedJson = editorRef.current.get(); const updatedJson = editorRef.current.get();
onChange(updatedJson); onChange(updatedJson);
} catch (err) { } catch (err) {
// Do not trigger onChange when parsing error occurs // Do not trigger onChange when parsing error occurs
} }
} }
}, },
...options, // Merge user provided options with defaults ...options, // Merge user provided options with defaults
};
editorRef.current = new JSONEditor(containerRef.current, newOptions);
editorRef.current.set(currentData);
} catch (error) {
console.error(
'Failed to reload jsoneditor with new language:',
error,
);
}
}; };
editorRef.current = new JSONEditor(containerRef.current, newOptions); initEditorWithNewLanguage();
editorRef.current.set(currentData);
} }
}, [i18n.language, value, onChange, options]); }, [i18n.language, value, onChange, options]);
@ -135,7 +168,13 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
ref={containerRef} ref={containerRef}
style={{ height }} style={{ height }}
className={`ace-tomorrow-night w-full border border-border-button rounded-lg overflow-hidden bg-bg-input ${className} `} className={`ace-tomorrow-night w-full border border-border-button rounded-lg overflow-hidden bg-bg-input ${className} `}
/> >
{isLoading && (
<div className="flex items-center justify-center h-full">
<div className="text-text-secondary">Loading editor...</div>
</div>
)}
</div>
); );
}; };

6
web/src/custom.d.ts vendored
View file

@ -2,3 +2,9 @@ declare module '*.md' {
const content: string; const content: string;
export default content; export default content;
} }
declare module 'jsoneditor' {
const JSONEditor: any;
export default JSONEditor;
export = JSONEditor;
}

View file

@ -40,6 +40,7 @@ import { useDropdownManager } from './context';
import { AgentBackground } from '@/components/canvas/background'; import { AgentBackground } from '@/components/canvas/background';
import Spotlight from '@/components/spotlight'; import Spotlight from '@/components/spotlight';
import { useNodeLoading } from '../hooks/use-node-loading';
import { import {
useHideFormSheetOnNodeDeletion, useHideFormSheetOnNodeDeletion,
useShowDrawer, useShowDrawer,
@ -166,6 +167,8 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
}); });
const [lastSendLoading, setLastSendLoading] = useState(false); const [lastSendLoading, setLastSendLoading] = useState(false);
const [currentSendLoading, setCurrentSendLoading] = useState(false);
const { handleBeforeDelete } = useBeforeDelete(); const { handleBeforeDelete } = useBeforeDelete();
const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance); const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance);
@ -182,6 +185,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
}, [chatVisible, clearEventList, currentTaskId, stopMessage]); }, [chatVisible, clearEventList, currentTaskId, stopMessage]);
const setLastSendLoadingFunc = (loading: boolean, messageId: string) => { const setLastSendLoadingFunc = (loading: boolean, messageId: string) => {
setCurrentSendLoading(!!loading);
if (messageId === currentMessageId) { if (messageId === currentMessageId) {
setLastSendLoading(loading); setLastSendLoading(loading);
} else { } else {
@ -249,7 +253,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
clearActiveDropdown, clearActiveDropdown,
removePlaceholderNode, removePlaceholderNode,
]); ]);
const { lastNode, setDerivedMessages, startButNotFinishedNodeIds } =
useNodeLoading({
currentEventListWithoutMessageById,
});
return ( return (
<div className={cn(styles.canvasWrapper, 'px-5 pb-5')}> <div className={cn(styles.canvasWrapper, 'px-5 pb-5')}>
<svg <svg
@ -285,7 +292,15 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
</marker> </marker>
</defs> </defs>
</svg> </svg>
<AgentInstanceContext.Provider value={{ addCanvasNode, showFormDrawer }}> <AgentInstanceContext.Provider
value={{
addCanvasNode,
showFormDrawer,
lastNode,
currentSendLoading,
startButNotFinishedNodeIds,
}}
>
<ReactFlow <ReactFlow
connectionMode={ConnectionMode.Loose} connectionMode={ConnectionMode.Loose}
nodes={nodes} nodes={nodes}
@ -380,9 +395,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
></FormSheet> ></FormSheet>
</AgentInstanceContext.Provider> </AgentInstanceContext.Provider>
)} )}
{chatVisible && ( {chatVisible && (
<AgentChatContext.Provider <AgentChatContext.Provider
value={{ showLogSheet, setLastSendLoadingFunc }} value={{ showLogSheet, setLastSendLoadingFunc, setDerivedMessages }}
> >
<AgentChatLogContext.Provider <AgentChatLogContext.Provider
value={{ addEventList, setCurrentMessageId }} value={{ addEventList, setCurrentMessageId }}

View file

@ -44,7 +44,7 @@ function InnerAgentNode({
return ( return (
<ToolBar selected={selected} id={id} label={data.label}> <ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
{isHeadAgent && ( {isHeadAgent && (
<> <>
<LeftEndHandle></LeftEndHandle> <LeftEndHandle></LeftEndHandle>

View file

@ -24,7 +24,7 @@ function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {}); const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {});
return ( return (
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<CommonHandle <CommonHandle
type="source" type="source"
position={Position.Right} position={Position.Right}

View file

@ -18,7 +18,7 @@ export function InnerCategorizeNode({
const { positions } = useBuildCategorizeHandlePositions({ data, id }); const { positions } = useBuildCategorizeHandlePositions({ data, id });
return ( return (
<ToolBar selected={selected} id={id} label={data.label}> <ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<LeftEndHandle></LeftEndHandle> <LeftEndHandle></LeftEndHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>

View file

@ -14,7 +14,7 @@ export function ExitLoopNode({ id, data, selected }: NodeProps<BaseNode<any>>) {
showRun={false} showRun={false}
showCopy={false} showCopy={false}
> >
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<LeftEndHandle></LeftEndHandle> <LeftEndHandle></LeftEndHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</NodeWrapper> </NodeWrapper>

View file

@ -23,7 +23,7 @@ function InnerFileNode({ data, id, selected }: NodeProps<IBeginNode>) {
const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {}); const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {});
return ( return (
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<CommonHandle <CommonHandle
type="source" type="source"
position={Position.Right} position={Position.Right}

View file

@ -26,7 +26,7 @@ function InnerRagNode({
showRun={needsSingleStepDebugging(data.label)} showRun={needsSingleStepDebugging(data.label)}
showCopy={showCopyIcon(data.label)} showCopy={showCopyIcon(data.label)}
> >
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<LeftEndHandle></LeftEndHandle> <LeftEndHandle></LeftEndHandle>
<CommonHandle <CommonHandle
type="source" type="source"

View file

@ -16,7 +16,7 @@ function InnerMessageNode({ id, data, selected }: NodeProps<IMessageNode>) {
const messages: string[] = get(data, 'form.content', []); const messages: string[] = get(data, 'form.content', []);
return ( return (
<ToolBar selected={selected} id={id} label={data.label}> <ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<LeftEndHandle></LeftEndHandle> <LeftEndHandle></LeftEndHandle>
<NodeHeader <NodeHeader
id={id} id={id}

View file

@ -1,9 +1,13 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { HTMLAttributes } from 'react'; import { Loader } from 'lucide-react';
import { HTMLAttributes, useContext } from 'react';
import { AgentInstanceContext } from '../../context';
type IProps = HTMLAttributes<HTMLDivElement> & { selected?: boolean }; type IProps = HTMLAttributes<HTMLDivElement> & { selected?: boolean };
export function NodeWrapper({ children, className, selected }: IProps) { export function NodeWrapper({ children, className, selected, id }: IProps) {
const { currentSendLoading, startButNotFinishedNodeIds = [] } =
useContext(AgentInstanceContext);
return ( return (
<section <section
className={cn( className={cn(
@ -12,6 +16,13 @@ export function NodeWrapper({ children, className, selected }: IProps) {
className, className,
)} )}
> >
{id &&
startButNotFinishedNodeIds.indexOf(id as string) > -1 &&
currentSendLoading && (
<div className=" absolute right-0 left-0 top-0 flex items-start justify-end p-2">
<Loader size={12} className=" animate-spin" />
</div>
)}
{children} {children}
</section> </section>
); );

View file

@ -19,7 +19,7 @@ function ParserNode({
}: NodeProps<BaseNode<ParserFormSchemaType>>) { }: NodeProps<BaseNode<ParserFormSchemaType>>) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<CommonHandle <CommonHandle
id={NodeHandleId.End} id={NodeHandleId.End}
type="target" type="target"

View file

@ -27,7 +27,7 @@ function InnerRetrievalNode({
return ( return (
<ToolBar selected={selected} id={id} label={data.label}> <ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<LeftEndHandle></LeftEndHandle> <LeftEndHandle></LeftEndHandle>
<CommonHandle <CommonHandle
id={NodeHandleId.Start} id={NodeHandleId.Start}

View file

@ -25,7 +25,7 @@ function InnerSplitterNode({
showCopy={false} showCopy={false}
showRun={false} showRun={false}
> >
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<CommonHandle <CommonHandle
id={NodeHandleId.End} id={NodeHandleId.End}
type="target" type="target"

View file

@ -65,7 +65,7 @@ function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
const { positions } = useBuildSwitchHandlePositions({ data, id }); const { positions } = useBuildSwitchHandlePositions({ data, id });
return ( return (
<ToolBar selected={selected} id={id} label={data.label} showRun={false}> <ToolBar selected={selected} id={id} label={data.label} showRun={false}>
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<LeftEndHandle></LeftEndHandle> <LeftEndHandle></LeftEndHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<section className="gap-2.5 flex flex-col"> <section className="gap-2.5 flex flex-col">

View file

@ -27,7 +27,7 @@ function TokenizerNode({
showRun={false} showRun={false}
showCopy={false} showCopy={false}
> >
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<CommonHandle <CommonHandle
id={NodeHandleId.End} id={NodeHandleId.End}
type="target" type="target"

View file

@ -44,7 +44,7 @@ function InnerToolNode({
); );
return ( return (
<NodeWrapper selected={selected}> <NodeWrapper selected={selected} id={id}>
<Handle <Handle
id={NodeHandleId.End} id={NodeHandleId.End}
type="target" type="target"

View file

@ -13,8 +13,9 @@ import {
} from '@/hooks/use-agent-request'; } from '@/hooks/use-agent-request';
import { useFetchUserInfo } from '@/hooks/use-user-setting-request'; import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
import { buildMessageUuidWithRole } from '@/utils/chat'; import { buildMessageUuidWithRole } from '@/utils/chat';
import { memo, useCallback } from 'react'; import { memo, useCallback, useContext } from 'react';
import { useParams } from 'umi'; import { useParams } from 'umi';
import { AgentChatContext } from '../context';
import DebugContent from '../debug-content'; import DebugContent from '../debug-content';
import { useAwaitCompentData } from '../hooks/use-chat-logic'; import { useAwaitCompentData } from '../hooks/use-chat-logic';
import { useIsTaskMode } from '../hooks/use-get-begin-query'; import { useIsTaskMode } from '../hooks/use-get-begin-query';
@ -49,6 +50,9 @@ function AgentChatBox() {
canvasId: canvasId as string, canvasId: canvasId as string,
}); });
const { setDerivedMessages } = useContext(AgentChatContext);
setDerivedMessages?.(derivedMessages);
const isTaskMode = useIsTaskMode(); const isTaskMode = useIsTaskMode();
const handleUploadFile: NonNullable<FileUploadProps['onUpload']> = const handleUploadFile: NonNullable<FileUploadProps['onUpload']> =

View file

@ -1,6 +1,8 @@
import { INodeEvent } from '@/hooks/use-send-message';
import { IMessage } from '@/interfaces/database/chat';
import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { HandleType, Position } from '@xyflow/react'; import { HandleType, Position } from '@xyflow/react';
import { createContext } from 'react'; import { Dispatch, SetStateAction, createContext } from 'react';
import { useAddNode } from './hooks/use-add-node'; import { useAddNode } from './hooks/use-add-node';
import { useCacheChatLog } from './hooks/use-cache-chat-log'; import { useCacheChatLog } from './hooks/use-cache-chat-log';
import { useShowFormDrawer, useShowLogSheet } from './hooks/use-show-drawer'; import { useShowFormDrawer, useShowLogSheet } from './hooks/use-show-drawer';
@ -13,7 +15,11 @@ type AgentInstanceContextType = Pick<
ReturnType<typeof useAddNode>, ReturnType<typeof useAddNode>,
'addCanvasNode' 'addCanvasNode'
> & > &
Pick<ReturnType<typeof useShowFormDrawer>, 'showFormDrawer'>; Pick<ReturnType<typeof useShowFormDrawer>, 'showFormDrawer'> & {
lastNode: INodeEvent | null;
currentSendLoading: boolean;
startButNotFinishedNodeIds: string[];
};
export const AgentInstanceContext = createContext<AgentInstanceContextType>( export const AgentInstanceContext = createContext<AgentInstanceContextType>(
{} as AgentInstanceContextType, {} as AgentInstanceContextType,
@ -22,7 +28,10 @@ export const AgentInstanceContext = createContext<AgentInstanceContextType>(
type AgentChatContextType = Pick< type AgentChatContextType = Pick<
ReturnType<typeof useShowLogSheet>, ReturnType<typeof useShowLogSheet>,
'showLogSheet' 'showLogSheet'
> & { setLastSendLoadingFunc: (loading: boolean, messageId: string) => void }; > & {
setLastSendLoadingFunc: (loading: boolean, messageId: string) => void;
setDerivedMessages: Dispatch<SetStateAction<IMessage[] | undefined>>;
};
export const AgentChatContext = createContext<AgentChatContextType>( export const AgentChatContext = createContext<AgentChatContextType>(
{} as AgentChatContextType, {} as AgentChatContextType,

View file

@ -55,7 +55,7 @@ const FormSheet = ({
<Sheet open={visible} modal={false}> <Sheet open={visible} modal={false}>
<SheetContent <SheetContent
className={cn('top-20 p-0 flex flex-col pb-20', { className={cn('top-20 p-0 flex flex-col pb-20', {
'right-[620px]': chatVisible, 'right-[clamp(0px,34%,620px)]': chatVisible,
})} })}
closeIcon={false} closeIcon={false}
> >

View file

@ -0,0 +1,88 @@
import {
INodeData,
INodeEvent,
MessageEventType,
} from '@/hooks/use-send-message';
import { IMessage } from '@/interfaces/database/chat';
import { useCallback, useMemo, useState } from 'react';
export const useNodeLoading = ({
currentEventListWithoutMessageById,
}: {
currentEventListWithoutMessageById: (messageId: string) => INodeEvent[];
}) => {
const [derivedMessages, setDerivedMessages] = useState<IMessage[]>();
const lastMessageId = useMemo(() => {
return derivedMessages?.[derivedMessages?.length - 1]?.id;
}, [derivedMessages]);
const currentEventListWithoutMessage = useMemo(() => {
if (!lastMessageId) {
return [];
}
return currentEventListWithoutMessageById(lastMessageId);
}, [currentEventListWithoutMessageById, lastMessageId]);
const startedNodeList = useMemo(() => {
const duplicateList = currentEventListWithoutMessage?.filter(
(x) => x.event === MessageEventType.NodeStarted,
) as INodeEvent[];
// Remove duplicate nodes
return duplicateList?.reduce<Array<INodeEvent>>((pre, cur) => {
if (pre.every((x) => x.data.component_id !== cur.data.component_id)) {
pre.push(cur);
}
return pre;
}, []);
}, [currentEventListWithoutMessage]);
const filterFinishedNodeList = useCallback(() => {
const nodeEventList = currentEventListWithoutMessage
.filter(
(x) => x.event === MessageEventType.NodeFinished,
// x.event === MessageEventType.NodeFinished &&
// (x.data as INodeData)?.component_id === componentId,
)
.map((x) => x.data);
return nodeEventList;
}, [currentEventListWithoutMessage]);
const lastNode = useMemo(() => {
if (!startedNodeList) {
return null;
}
return startedNodeList[startedNodeList.length - 1];
}, [startedNodeList]);
const startNodeIds = useMemo(() => {
if (!startedNodeList) {
return [];
}
return startedNodeList.map((x) => x.data.component_id);
}, [startedNodeList]);
const finishNodeIds = useMemo(() => {
if (!lastNode) {
return [];
}
const nodeDataList = filterFinishedNodeList();
const finishNodeIdsTemp = nodeDataList.map(
(x: INodeData) => x.component_id,
);
return Array.from(new Set(finishNodeIdsTemp));
}, [lastNode, filterFinishedNodeList]);
const startButNotFinishedNodeIds = useMemo(() => {
return startNodeIds.filter((x) => !finishNodeIds.includes(x));
}, [finishNodeIds, startNodeIds]);
return {
lastNode,
startButNotFinishedNodeIds,
filterFinishedNodeList,
setDerivedMessages,
};
};

View file

@ -26,7 +26,7 @@ export function LogSheet({
return ( return (
<Sheet open onOpenChange={hideModal} modal={false}> <Sheet open onOpenChange={hideModal} modal={false}>
<SheetContent <SheetContent
className={cn('top-20 right-[620px]')} className={cn('top-20 right-[clamp(0px,34%,620px)]')}
onInteractOutside={(e) => e.preventDefault()} onInteractOutside={(e) => e.preventDefault()}
> >
<SheetHeader> <SheetHeader>