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):
self.components = self.dsl["components"]
cpn_nms = set([])
for k, cpn in self.components.items():
cpn_nms.add(cpn["obj"]["component_name"])
for k, cpn in self.components.items():
cpn_nms.add(cpn["obj"]["component_name"])
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:
if not DocumentService.update_by_id(doc.id, {"status": str(status)}):
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)
return get_result(data=True)
except Exception as e:
return server_error_response(e)
@ -350,12 +348,10 @@ async def update_doc(tenant_id, dataset_id, document_id):
}
renamed_doc = {}
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)
renamed_doc[new_key] = value
if key == "run":
renamed_doc["run"] = run_mapping.get(value)
renamed_doc["run"] = run_mapping.get(str(value))
return get_result(data=renamed_doc)

View file

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

View file

@ -31,6 +31,7 @@ import rag.utils.ob_conn
import rag.utils.opensearch_conn
from rag.utils.azure_sas_conn import RAGFlowAzureSasBlob
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.opendal_conn import OpenDALStorage
from rag.utils.s3_conn import RAGFlowS3
@ -109,6 +110,7 @@ MINIO = {}
OB = {}
OSS = {}
OS = {}
GCS = {}
DOC_MAXIMUM_SIZE: int = 128 * 1024 * 1024
DOC_BULK_SIZE: int = 4
@ -151,7 +153,8 @@ class StorageFactory:
Storage.AZURE_SAS: RAGFlowAzureSasBlob,
Storage.AWS_S3: RAGFlowS3,
Storage.OSS: RAGFlowOSS,
Storage.OPENDAL: OpenDALStorage
Storage.OPENDAL: OpenDALStorage,
Storage.GCS: RAGFlowGCS,
}
@classmethod
@ -250,7 +253,7 @@ def init_settings():
else:
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']:
AZURE = get_base_config("azure", {})
elif STORAGE_IMPL_TYPE == 'AWS_S3':
@ -259,6 +262,8 @@ def init_settings():
MINIO = decrypt_database_config(name="minio")
elif STORAGE_IMPL_TYPE == 'OSS':
OSS = get_base_config("oss", {})
elif STORAGE_IMPL_TYPE == 'GCS':
GCS = get_base_config("gcs", {})
global STORAGE_IMPL
STORAGE_IMPL = StorageFactory.create(Storage[STORAGE_IMPL_TYPE])

View file

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

View file

@ -86,9 +86,11 @@ class Pdf(PdfParser):
# (A) Add text
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
page_items[b["page_number"]].append({
page_items[global_page_num].append({
"top": b["top"],
"x0": b["x0"],
"text": b["text"],
@ -100,7 +102,6 @@ class Pdf(PdfParser):
if not positions:
continue
# Handle content type (list vs str)
if isinstance(content, list):
final_text = "\n".join(content)
elif isinstance(content, str):
@ -109,10 +110,11 @@ class Pdf(PdfParser):
final_text = str(content)
try:
# Parse positions
pn_index = positions[0][0]
if isinstance(pn_index, list):
pn_index = pn_index[0]
# pn_index in tbls is absolute page number
current_page_num = int(pn_index) + 1
except Exception as 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 './css/cloud9_night.less';
import './css/index.less';
import { JsonEditorOptions, JsonEditorProps } from './interface';
const defaultConfig: JsonEditorOptions = {
mode: 'code',
modes: ['tree', 'code'],
@ -14,6 +15,7 @@ const defaultConfig: JsonEditorOptions = {
enableTransform: false,
indentation: 2,
};
const JsonEditor: React.FC<JsonEditorProps> = ({
value,
onChange,
@ -25,43 +27,62 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
const editorRef = useRef<any>(null);
const { i18n } = useTranslation();
const currentLanguageRef = useRef<string>(i18n.language);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (typeof window !== 'undefined') {
const JSONEditor = require('jsoneditor');
import('jsoneditor/dist/jsoneditor.min.css');
let isMounted = true;
if (containerRef.current) {
// Default configuration options
const defaultOptions: JsonEditorOptions = {
...defaultConfig,
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);
}
const initEditor = async () => {
if (typeof window !== 'undefined') {
try {
const JSONEditorModule = await import('jsoneditor');
const JSONEditor = JSONEditorModule.default || JSONEditorModule;
await import('jsoneditor/dist/jsoneditor.min.css');
if (isMounted && containerRef.current) {
// Default configuration options
const defaultOptions: JsonEditorOptions = {
...defaultConfig,
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(
containerRef.current,
defaultOptions,
);
if (value) {
editorRef.current.set(value);
setIsLoading(false);
}
} catch (error) {
console.error('Failed to load jsoneditor:', error);
if (isMounted) {
setIsLoading(false);
}
}
}
}
};
initEditor();
return () => {
isMounted = false;
if (editorRef.current) {
if (typeof editorRef.current.destroy === 'function') {
editorRef.current.destroy();
@ -92,26 +113,38 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
}
// 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 = {
...defaultConfig,
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
}
}
},
...options, // Merge user provided options with defaults
const newOptions: JsonEditorOptions = {
...defaultConfig,
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
}
}
},
...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);
editorRef.current.set(currentData);
initEditorWithNewLanguage();
}
}, [i18n.language, value, onChange, options]);
@ -135,7 +168,13 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
ref={containerRef}
style={{ height }}
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;
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 Spotlight from '@/components/spotlight';
import { useNodeLoading } from '../hooks/use-node-loading';
import {
useHideFormSheetOnNodeDeletion,
useShowDrawer,
@ -166,6 +167,8 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
});
const [lastSendLoading, setLastSendLoading] = useState(false);
const [currentSendLoading, setCurrentSendLoading] = useState(false);
const { handleBeforeDelete } = useBeforeDelete();
const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance);
@ -182,6 +185,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
}, [chatVisible, clearEventList, currentTaskId, stopMessage]);
const setLastSendLoadingFunc = (loading: boolean, messageId: string) => {
setCurrentSendLoading(!!loading);
if (messageId === currentMessageId) {
setLastSendLoading(loading);
} else {
@ -249,7 +253,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
clearActiveDropdown,
removePlaceholderNode,
]);
const { lastNode, setDerivedMessages, startButNotFinishedNodeIds } =
useNodeLoading({
currentEventListWithoutMessageById,
});
return (
<div className={cn(styles.canvasWrapper, 'px-5 pb-5')}>
<svg
@ -285,7 +292,15 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
</marker>
</defs>
</svg>
<AgentInstanceContext.Provider value={{ addCanvasNode, showFormDrawer }}>
<AgentInstanceContext.Provider
value={{
addCanvasNode,
showFormDrawer,
lastNode,
currentSendLoading,
startButNotFinishedNodeIds,
}}
>
<ReactFlow
connectionMode={ConnectionMode.Loose}
nodes={nodes}
@ -380,9 +395,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
></FormSheet>
</AgentInstanceContext.Provider>
)}
{chatVisible && (
<AgentChatContext.Provider
value={{ showLogSheet, setLastSendLoadingFunc }}
value={{ showLogSheet, setLastSendLoadingFunc, setDerivedMessages }}
>
<AgentChatLogContext.Provider
value={{ addEventList, setCurrentMessageId }}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,13 @@
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 };
export function NodeWrapper({ children, className, selected }: IProps) {
export function NodeWrapper({ children, className, selected, id }: IProps) {
const { currentSendLoading, startButNotFinishedNodeIds = [] } =
useContext(AgentInstanceContext);
return (
<section
className={cn(
@ -12,6 +16,13 @@ export function NodeWrapper({ children, className, selected }: IProps) {
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}
</section>
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,8 +13,9 @@ import {
} from '@/hooks/use-agent-request';
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { memo, useCallback } from 'react';
import { memo, useCallback, useContext } from 'react';
import { useParams } from 'umi';
import { AgentChatContext } from '../context';
import DebugContent from '../debug-content';
import { useAwaitCompentData } from '../hooks/use-chat-logic';
import { useIsTaskMode } from '../hooks/use-get-begin-query';
@ -49,6 +50,9 @@ function AgentChatBox() {
canvasId: canvasId as string,
});
const { setDerivedMessages } = useContext(AgentChatContext);
setDerivedMessages?.(derivedMessages);
const isTaskMode = useIsTaskMode();
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 { HandleType, Position } from '@xyflow/react';
import { createContext } from 'react';
import { Dispatch, SetStateAction, createContext } from 'react';
import { useAddNode } from './hooks/use-add-node';
import { useCacheChatLog } from './hooks/use-cache-chat-log';
import { useShowFormDrawer, useShowLogSheet } from './hooks/use-show-drawer';
@ -13,7 +15,11 @@ type AgentInstanceContextType = Pick<
ReturnType<typeof useAddNode>,
'addCanvasNode'
> &
Pick<ReturnType<typeof useShowFormDrawer>, 'showFormDrawer'>;
Pick<ReturnType<typeof useShowFormDrawer>, 'showFormDrawer'> & {
lastNode: INodeEvent | null;
currentSendLoading: boolean;
startButNotFinishedNodeIds: string[];
};
export const AgentInstanceContext = createContext<AgentInstanceContextType>(
{} as AgentInstanceContextType,
@ -22,7 +28,10 @@ export const AgentInstanceContext = createContext<AgentInstanceContextType>(
type AgentChatContextType = Pick<
ReturnType<typeof useShowLogSheet>,
'showLogSheet'
> & { setLastSendLoadingFunc: (loading: boolean, messageId: string) => void };
> & {
setLastSendLoadingFunc: (loading: boolean, messageId: string) => void;
setDerivedMessages: Dispatch<SetStateAction<IMessage[] | undefined>>;
};
export const AgentChatContext = createContext<AgentChatContextType>(
{} as AgentChatContextType,

View file

@ -55,7 +55,7 @@ const FormSheet = ({
<Sheet open={visible} modal={false}>
<SheetContent
className={cn('top-20 p-0 flex flex-col pb-20', {
'right-[620px]': chatVisible,
'right-[clamp(0px,34%,620px)]': chatVisible,
})}
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 (
<Sheet open onOpenChange={hideModal} modal={false}>
<SheetContent
className={cn('top-20 right-[620px]')}
className={cn('top-20 right-[clamp(0px,34%,620px)]')}
onInteractOutside={(e) => e.preventDefault()}
>
<SheetHeader>