mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Feat: Display the pipeline on the agent canvas #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -53,6 +53,10 @@ export enum AgentCategory {
|
|||||||
DataflowCanvas = 'dataflow_canvas',
|
DataflowCanvas = 'dataflow_canvas',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AgentQuery {
|
||||||
|
Category = 'category',
|
||||||
|
}
|
||||||
|
|
||||||
export enum DataflowOperator {
|
export enum DataflowOperator {
|
||||||
Begin = 'File',
|
Begin = 'File',
|
||||||
Note = 'Note',
|
Note = 'Note',
|
||||||
@ -62,3 +66,55 @@ export enum DataflowOperator {
|
|||||||
HierarchicalMerger = 'HierarchicalMerger',
|
HierarchicalMerger = 'HierarchicalMerger',
|
||||||
Extractor = 'Extractor',
|
Extractor = 'Extractor',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Operator {
|
||||||
|
Begin = 'Begin',
|
||||||
|
Retrieval = 'Retrieval',
|
||||||
|
Categorize = 'Categorize',
|
||||||
|
Message = 'Message',
|
||||||
|
Relevant = 'Relevant',
|
||||||
|
RewriteQuestion = 'RewriteQuestion',
|
||||||
|
KeywordExtract = 'KeywordExtract',
|
||||||
|
Baidu = 'Baidu',
|
||||||
|
DuckDuckGo = 'DuckDuckGo',
|
||||||
|
Wikipedia = 'Wikipedia',
|
||||||
|
PubMed = 'PubMed',
|
||||||
|
ArXiv = 'ArXiv',
|
||||||
|
Google = 'Google',
|
||||||
|
Bing = 'Bing',
|
||||||
|
GoogleScholar = 'GoogleScholar',
|
||||||
|
DeepL = 'DeepL',
|
||||||
|
GitHub = 'GitHub',
|
||||||
|
BaiduFanyi = 'BaiduFanyi',
|
||||||
|
QWeather = 'QWeather',
|
||||||
|
ExeSQL = 'ExeSQL',
|
||||||
|
Switch = 'Switch',
|
||||||
|
WenCai = 'WenCai',
|
||||||
|
AkShare = 'AkShare',
|
||||||
|
YahooFinance = 'YahooFinance',
|
||||||
|
Jin10 = 'Jin10',
|
||||||
|
TuShare = 'TuShare',
|
||||||
|
Note = 'Note',
|
||||||
|
Crawler = 'Crawler',
|
||||||
|
Invoke = 'Invoke',
|
||||||
|
Email = 'Email',
|
||||||
|
Iteration = 'Iteration',
|
||||||
|
IterationStart = 'IterationItem',
|
||||||
|
Code = 'CodeExec',
|
||||||
|
WaitingDialogue = 'WaitingDialogue',
|
||||||
|
Agent = 'Agent',
|
||||||
|
Tool = 'Tool',
|
||||||
|
TavilySearch = 'TavilySearch',
|
||||||
|
TavilyExtract = 'TavilyExtract',
|
||||||
|
UserFillUp = 'UserFillUp',
|
||||||
|
StringTransform = 'StringTransform',
|
||||||
|
SearXNG = 'SearXNG',
|
||||||
|
Placeholder = 'Placeholder',
|
||||||
|
File = 'File', // pipeline
|
||||||
|
Parser = 'Parser',
|
||||||
|
Tokenizer = 'Tokenizer',
|
||||||
|
Splitter = 'Splitter',
|
||||||
|
HierarchicalMerger = 'HierarchicalMerger',
|
||||||
|
Extractor = 'Extractor',
|
||||||
|
Generate = 'Generate',
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { AgentCategory, AgentQuery } from '@/constants/agent';
|
||||||
import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
|
import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
|
||||||
import { Routes } from '@/routes';
|
import { Routes } from '@/routes';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@ -70,8 +71,8 @@ export const useNavigatePage = () => {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const navigateToAgent = useCallback(
|
const navigateToAgent = useCallback(
|
||||||
(id: string) => () => {
|
(id: string, category?: AgentCategory) => () => {
|
||||||
navigate(`${Routes.Agent}/${id}`);
|
navigate(`${Routes.Agent}/${id}?${AgentQuery.Category}=${category}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigate],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -56,19 +56,24 @@ import { RagNode } from './node';
|
|||||||
import { AgentNode } from './node/agent-node';
|
import { AgentNode } from './node/agent-node';
|
||||||
import { BeginNode } from './node/begin-node';
|
import { BeginNode } from './node/begin-node';
|
||||||
import { CategorizeNode } from './node/categorize-node';
|
import { CategorizeNode } from './node/categorize-node';
|
||||||
import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
|
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
|
||||||
|
import { ExtractorNode } from './node/extractor-node';
|
||||||
|
import { FileNode } from './node/file-node';
|
||||||
import { GenerateNode } from './node/generate-node';
|
import { GenerateNode } from './node/generate-node';
|
||||||
import { InvokeNode } from './node/invoke-node';
|
import { InvokeNode } from './node/invoke-node';
|
||||||
import { IterationNode, IterationStartNode } from './node/iteration-node';
|
import { IterationNode, IterationStartNode } from './node/iteration-node';
|
||||||
import { KeywordNode } from './node/keyword-node';
|
import { KeywordNode } from './node/keyword-node';
|
||||||
import { MessageNode } from './node/message-node';
|
import { MessageNode } from './node/message-node';
|
||||||
import NoteNode from './node/note-node';
|
import NoteNode from './node/note-node';
|
||||||
|
import ParserNode from './node/parser-node';
|
||||||
import { PlaceholderNode } from './node/placeholder-node';
|
import { PlaceholderNode } from './node/placeholder-node';
|
||||||
import { RelevantNode } from './node/relevant-node';
|
import { RelevantNode } from './node/relevant-node';
|
||||||
import { RetrievalNode } from './node/retrieval-node';
|
import { RetrievalNode } from './node/retrieval-node';
|
||||||
import { RewriteNode } from './node/rewrite-node';
|
import { RewriteNode } from './node/rewrite-node';
|
||||||
|
import { SplitterNode } from './node/splitter-node';
|
||||||
import { SwitchNode } from './node/switch-node';
|
import { SwitchNode } from './node/switch-node';
|
||||||
import { TemplateNode } from './node/template-node';
|
import { TemplateNode } from './node/template-node';
|
||||||
|
import TokenizerNode from './node/tokenizer-node';
|
||||||
import { ToolNode } from './node/tool-node';
|
import { ToolNode } from './node/tool-node';
|
||||||
|
|
||||||
export const nodeTypes: NodeTypes = {
|
export const nodeTypes: NodeTypes = {
|
||||||
@ -91,6 +96,11 @@ export const nodeTypes: NodeTypes = {
|
|||||||
iterationStartNode: IterationStartNode,
|
iterationStartNode: IterationStartNode,
|
||||||
agentNode: AgentNode,
|
agentNode: AgentNode,
|
||||||
toolNode: ToolNode,
|
toolNode: ToolNode,
|
||||||
|
fileNode: FileNode,
|
||||||
|
parserNode: ParserNode,
|
||||||
|
tokenizerNode: TokenizerNode,
|
||||||
|
splitterNode: SplitterNode,
|
||||||
|
contextNode: ExtractorNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
const edgeTypes = {
|
const edgeTypes = {
|
||||||
@ -194,6 +204,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
|||||||
getConnectionStartContext,
|
getConnectionStartContext,
|
||||||
shouldPreventClose,
|
shouldPreventClose,
|
||||||
onMove,
|
onMove,
|
||||||
|
nodeId,
|
||||||
} = useConnectionDrag(
|
} = useConnectionDrag(
|
||||||
reactFlowInstance,
|
reactFlowInstance,
|
||||||
originalOnConnect,
|
originalOnConnect,
|
||||||
@ -312,7 +323,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<InnerNextStepDropdown
|
<NextStepDropdown
|
||||||
hideModal={() => {
|
hideModal={() => {
|
||||||
removePlaceholderNode();
|
removePlaceholderNode();
|
||||||
hideModal();
|
hideModal();
|
||||||
@ -320,9 +331,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
|||||||
}}
|
}}
|
||||||
position={dropdownPosition}
|
position={dropdownPosition}
|
||||||
onNodeCreated={onNodeCreated}
|
onNodeCreated={onNodeCreated}
|
||||||
|
nodeId={nodeId}
|
||||||
>
|
>
|
||||||
<span></span>
|
<span></span>
|
||||||
</InnerNextStepDropdown>
|
</NextStepDropdown>
|
||||||
</HandleContext.Provider>
|
</HandleContext.Provider>
|
||||||
)}
|
)}
|
||||||
</AgentInstanceContext.Provider>
|
</AgentInstanceContext.Provider>
|
||||||
|
|||||||
@ -17,6 +17,9 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
export function CardWithForm() {
|
export function CardWithForm() {
|
||||||
return (
|
return (
|
||||||
<Card className="w-[350px]">
|
<Card className="w-[350px]">
|
||||||
@ -55,3 +58,13 @@ export function CardWithForm() {
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LabelCardProps = {
|
||||||
|
className?: string;
|
||||||
|
} & PropsWithChildren;
|
||||||
|
|
||||||
|
export function LabelCard({ children, className }: LabelCardProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('bg-bg-card rounded-sm p-1', className)}>{children}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
196
web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx
Normal file
196
web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@/components/ui/accordion';
|
||||||
|
import { Operator } from '@/constants/agent';
|
||||||
|
import useGraphStore from '@/pages/agent/store';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { OperatorItemList } from './operator-item-list';
|
||||||
|
|
||||||
|
export function AccordionOperators({
|
||||||
|
isCustomDropdown = false,
|
||||||
|
mousePosition,
|
||||||
|
}: {
|
||||||
|
isCustomDropdown?: boolean;
|
||||||
|
mousePosition?: { x: number; y: number };
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
type="multiple"
|
||||||
|
className="px-2 text-text-title max-h-[45vh] overflow-auto scrollbar-none"
|
||||||
|
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
|
||||||
|
>
|
||||||
|
<AccordionItem value="item-1">
|
||||||
|
<AccordionTrigger className="text-xl">
|
||||||
|
{t('flow.foundation')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
|
<OperatorItemList
|
||||||
|
operators={[Operator.Agent, Operator.Retrieval]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
|
></OperatorItemList>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="item-2">
|
||||||
|
<AccordionTrigger className="text-xl">
|
||||||
|
{t('flow.dialog')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
|
<OperatorItemList
|
||||||
|
operators={[Operator.Message, Operator.UserFillUp]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
|
></OperatorItemList>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="item-3">
|
||||||
|
<AccordionTrigger className="text-xl">
|
||||||
|
{t('flow.flow')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
|
<OperatorItemList
|
||||||
|
operators={[
|
||||||
|
Operator.Switch,
|
||||||
|
Operator.Iteration,
|
||||||
|
Operator.Categorize,
|
||||||
|
]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
|
></OperatorItemList>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="item-4">
|
||||||
|
<AccordionTrigger className="text-xl">
|
||||||
|
{t('flow.dataManipulation')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
|
<OperatorItemList
|
||||||
|
operators={[Operator.Code, Operator.StringTransform]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
|
></OperatorItemList>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="item-5">
|
||||||
|
<AccordionTrigger className="text-xl">
|
||||||
|
{t('flow.tools')}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
|
<OperatorItemList
|
||||||
|
operators={[
|
||||||
|
Operator.TavilySearch,
|
||||||
|
Operator.TavilyExtract,
|
||||||
|
Operator.ExeSQL,
|
||||||
|
Operator.Google,
|
||||||
|
Operator.YahooFinance,
|
||||||
|
Operator.Email,
|
||||||
|
Operator.DuckDuckGo,
|
||||||
|
Operator.Wikipedia,
|
||||||
|
Operator.GoogleScholar,
|
||||||
|
Operator.ArXiv,
|
||||||
|
Operator.PubMed,
|
||||||
|
Operator.GitHub,
|
||||||
|
Operator.Invoke,
|
||||||
|
Operator.WenCai,
|
||||||
|
Operator.SearXNG,
|
||||||
|
]}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
|
></OperatorItemList>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit the number of operators of a certain type on the canvas to only one
|
||||||
|
function useRestrictSingleOperatorOnCanvas() {
|
||||||
|
const { findNodeByName } = useGraphStore((state) => state);
|
||||||
|
|
||||||
|
const restrictSingleOperatorOnCanvas = useCallback(
|
||||||
|
(singleOperators: Operator[]) => {
|
||||||
|
const list: Operator[] = [];
|
||||||
|
singleOperators.forEach((operator) => {
|
||||||
|
if (!findNodeByName(operator)) {
|
||||||
|
list.push(operator);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
[findNodeByName],
|
||||||
|
);
|
||||||
|
|
||||||
|
return restrictSingleOperatorOnCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PipelineAccordionOperators({
|
||||||
|
isCustomDropdown = false,
|
||||||
|
mousePosition,
|
||||||
|
nodeId,
|
||||||
|
}: {
|
||||||
|
isCustomDropdown?: boolean;
|
||||||
|
mousePosition?: { x: number; y: number };
|
||||||
|
nodeId?: string;
|
||||||
|
}) {
|
||||||
|
const restrictSingleOperatorOnCanvas = useRestrictSingleOperatorOnCanvas();
|
||||||
|
const { getOperatorTypeFromId } = useGraphStore((state) => state);
|
||||||
|
|
||||||
|
const operators = useMemo(() => {
|
||||||
|
let list = [
|
||||||
|
...restrictSingleOperatorOnCanvas([Operator.Parser, Operator.Tokenizer]),
|
||||||
|
];
|
||||||
|
list.push(Operator.Extractor);
|
||||||
|
return list;
|
||||||
|
}, [restrictSingleOperatorOnCanvas]);
|
||||||
|
|
||||||
|
const chunkerOperators = useMemo(() => {
|
||||||
|
return [
|
||||||
|
...restrictSingleOperatorOnCanvas([
|
||||||
|
Operator.Splitter,
|
||||||
|
Operator.HierarchicalMerger,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}, [restrictSingleOperatorOnCanvas]);
|
||||||
|
|
||||||
|
const showChunker = useMemo(() => {
|
||||||
|
return (
|
||||||
|
getOperatorTypeFromId(nodeId) !== Operator.Extractor &&
|
||||||
|
chunkerOperators.length > 0
|
||||||
|
);
|
||||||
|
}, [chunkerOperators.length, getOperatorTypeFromId, nodeId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OperatorItemList
|
||||||
|
operators={operators}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
|
></OperatorItemList>
|
||||||
|
{showChunker && (
|
||||||
|
<Accordion
|
||||||
|
type="single"
|
||||||
|
collapsible
|
||||||
|
className="w-full px-4"
|
||||||
|
defaultValue="item-1"
|
||||||
|
>
|
||||||
|
<AccordionItem value="item-1">
|
||||||
|
<AccordionTrigger>Chunker</AccordionTrigger>
|
||||||
|
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||||
|
<OperatorItemList
|
||||||
|
operators={chunkerOperators}
|
||||||
|
isCustomDropdown={isCustomDropdown}
|
||||||
|
mousePosition={mousePosition}
|
||||||
|
></OperatorItemList>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,230 +1,33 @@
|
|||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from '@/components/ui/accordion';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip';
|
|
||||||
import { IModalProps } from '@/interfaces/common';
|
import { IModalProps } from '@/interfaces/common';
|
||||||
import { Operator } from '@/pages/agent/constant';
|
import { useIsPipeline } from '@/pages/agent/hooks/use-is-pipeline';
|
||||||
import { AgentInstanceContext, HandleContext } from '@/pages/agent/context';
|
|
||||||
import OperatorIcon from '@/pages/agent/operator-icon';
|
|
||||||
import { Position } from '@xyflow/react';
|
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { lowerFirst } from 'lodash';
|
import { PropsWithChildren, memo, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
PropsWithChildren,
|
AccordionOperators,
|
||||||
createContext,
|
PipelineAccordionOperators,
|
||||||
memo,
|
} from './accordion-operators';
|
||||||
useContext,
|
import { HideModalContext, OnNodeCreatedContext } from './operator-item-list';
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
} from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type OperatorItemProps = {
|
|
||||||
operators: Operator[];
|
|
||||||
isCustomDropdown?: boolean;
|
|
||||||
mousePosition?: { x: number; y: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
const HideModalContext = createContext<IModalProps<any>['showModal']>(() => {});
|
|
||||||
const OnNodeCreatedContext = createContext<
|
|
||||||
((newNodeId: string) => void) | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
function OperatorItemList({
|
|
||||||
operators,
|
|
||||||
isCustomDropdown = false,
|
|
||||||
mousePosition,
|
|
||||||
}: OperatorItemProps) {
|
|
||||||
const { addCanvasNode } = useContext(AgentInstanceContext);
|
|
||||||
const handleContext = useContext(HandleContext);
|
|
||||||
const hideModal = useContext(HideModalContext);
|
|
||||||
const onNodeCreated = useContext(OnNodeCreatedContext);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleClick =
|
|
||||||
(operator: Operator): React.MouseEventHandler<HTMLElement> =>
|
|
||||||
(e) => {
|
|
||||||
const contextData = handleContext || {
|
|
||||||
nodeId: '',
|
|
||||||
id: '',
|
|
||||||
type: 'source' as const,
|
|
||||||
position: Position.Right,
|
|
||||||
isFromConnectionDrag: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockEvent = mousePosition
|
|
||||||
? {
|
|
||||||
clientX: mousePosition.x,
|
|
||||||
clientY: mousePosition.y,
|
|
||||||
}
|
|
||||||
: e;
|
|
||||||
|
|
||||||
const newNodeId = addCanvasNode(operator, contextData)(mockEvent);
|
|
||||||
|
|
||||||
if (onNodeCreated && newNodeId) {
|
|
||||||
onNodeCreated(newNodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
hideModal?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderOperatorItem = (operator: Operator) => {
|
|
||||||
const commonContent = (
|
|
||||||
<div className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start">
|
|
||||||
<OperatorIcon name={operator} />
|
|
||||||
{t(`flow.${lowerFirst(operator)}`)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip key={operator}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
{isCustomDropdown ? (
|
|
||||||
<li onClick={handleClick(operator)}>{commonContent}</li>
|
|
||||||
) : (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={operator}
|
|
||||||
className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
|
|
||||||
onClick={handleClick(operator)}
|
|
||||||
onSelect={() => hideModal?.()}
|
|
||||||
>
|
|
||||||
<OperatorIcon name={operator} />
|
|
||||||
{t(`flow.${lowerFirst(operator)}`)}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" sideOffset={24}>
|
|
||||||
<p>{t(`flow.${lowerFirst(operator)}Description`)}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccordionOperators({
|
|
||||||
isCustomDropdown = false,
|
|
||||||
mousePosition,
|
|
||||||
}: {
|
|
||||||
isCustomDropdown?: boolean;
|
|
||||||
mousePosition?: { x: number; y: number };
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Accordion
|
|
||||||
type="multiple"
|
|
||||||
className="px-2 text-text-title max-h-[45vh] overflow-auto scrollbar-none"
|
|
||||||
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
|
|
||||||
>
|
|
||||||
<AccordionItem value="item-1">
|
|
||||||
<AccordionTrigger className="text-xl">
|
|
||||||
{t('flow.foundation')}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
|
||||||
<OperatorItemList
|
|
||||||
operators={[Operator.Agent, Operator.Retrieval]}
|
|
||||||
isCustomDropdown={isCustomDropdown}
|
|
||||||
mousePosition={mousePosition}
|
|
||||||
></OperatorItemList>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="item-2">
|
|
||||||
<AccordionTrigger className="text-xl">
|
|
||||||
{t('flow.dialog')}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
|
||||||
<OperatorItemList
|
|
||||||
operators={[Operator.Message, Operator.UserFillUp]}
|
|
||||||
isCustomDropdown={isCustomDropdown}
|
|
||||||
mousePosition={mousePosition}
|
|
||||||
></OperatorItemList>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="item-3">
|
|
||||||
<AccordionTrigger className="text-xl">
|
|
||||||
{t('flow.flow')}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
|
||||||
<OperatorItemList
|
|
||||||
operators={[
|
|
||||||
Operator.Switch,
|
|
||||||
Operator.Iteration,
|
|
||||||
Operator.Categorize,
|
|
||||||
]}
|
|
||||||
isCustomDropdown={isCustomDropdown}
|
|
||||||
mousePosition={mousePosition}
|
|
||||||
></OperatorItemList>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="item-4">
|
|
||||||
<AccordionTrigger className="text-xl">
|
|
||||||
{t('flow.dataManipulation')}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
|
||||||
<OperatorItemList
|
|
||||||
operators={[Operator.Code, Operator.StringTransform]}
|
|
||||||
isCustomDropdown={isCustomDropdown}
|
|
||||||
mousePosition={mousePosition}
|
|
||||||
></OperatorItemList>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="item-5">
|
|
||||||
<AccordionTrigger className="text-xl">
|
|
||||||
{t('flow.tools')}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
|
||||||
<OperatorItemList
|
|
||||||
operators={[
|
|
||||||
Operator.TavilySearch,
|
|
||||||
Operator.TavilyExtract,
|
|
||||||
Operator.ExeSQL,
|
|
||||||
Operator.Google,
|
|
||||||
Operator.YahooFinance,
|
|
||||||
Operator.Email,
|
|
||||||
Operator.DuckDuckGo,
|
|
||||||
Operator.Wikipedia,
|
|
||||||
Operator.GoogleScholar,
|
|
||||||
Operator.ArXiv,
|
|
||||||
Operator.PubMed,
|
|
||||||
Operator.GitHub,
|
|
||||||
Operator.Invoke,
|
|
||||||
Operator.WenCai,
|
|
||||||
Operator.SearXNG,
|
|
||||||
]}
|
|
||||||
isCustomDropdown={isCustomDropdown}
|
|
||||||
mousePosition={mousePosition}
|
|
||||||
></OperatorItemList>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InnerNextStepDropdown({
|
export function InnerNextStepDropdown({
|
||||||
children,
|
children,
|
||||||
hideModal,
|
hideModal,
|
||||||
position,
|
position,
|
||||||
onNodeCreated,
|
onNodeCreated,
|
||||||
|
nodeId,
|
||||||
}: PropsWithChildren &
|
}: PropsWithChildren &
|
||||||
IModalProps<any> & {
|
IModalProps<any> & {
|
||||||
position?: { x: number; y: number };
|
position?: { x: number; y: number };
|
||||||
onNodeCreated?: (newNodeId: string) => void;
|
onNodeCreated?: (newNodeId: string) => void;
|
||||||
|
nodeId?: string;
|
||||||
}) {
|
}) {
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isPipeline = useIsPipeline();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (position && hideModal) {
|
if (position && hideModal) {
|
||||||
@ -260,10 +63,18 @@ export function InnerNextStepDropdown({
|
|||||||
</div>
|
</div>
|
||||||
<HideModalContext.Provider value={hideModal}>
|
<HideModalContext.Provider value={hideModal}>
|
||||||
<OnNodeCreatedContext.Provider value={onNodeCreated}>
|
<OnNodeCreatedContext.Provider value={onNodeCreated}>
|
||||||
|
{isPipeline ? (
|
||||||
|
<PipelineAccordionOperators
|
||||||
|
isCustomDropdown={true}
|
||||||
|
mousePosition={position}
|
||||||
|
nodeId={nodeId}
|
||||||
|
></PipelineAccordionOperators>
|
||||||
|
) : (
|
||||||
<AccordionOperators
|
<AccordionOperators
|
||||||
isCustomDropdown={true}
|
isCustomDropdown={true}
|
||||||
mousePosition={position}
|
mousePosition={position}
|
||||||
></AccordionOperators>
|
></AccordionOperators>
|
||||||
|
)}
|
||||||
</OnNodeCreatedContext.Provider>
|
</OnNodeCreatedContext.Provider>
|
||||||
</HideModalContext.Provider>
|
</HideModalContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
@ -287,7 +98,11 @@ export function InnerNextStepDropdown({
|
|||||||
>
|
>
|
||||||
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
|
<DropdownMenuLabel>{t('flow.nextStep')}</DropdownMenuLabel>
|
||||||
<HideModalContext.Provider value={hideModal}>
|
<HideModalContext.Provider value={hideModal}>
|
||||||
|
{isPipeline ? (
|
||||||
|
<PipelineAccordionOperators></PipelineAccordionOperators>
|
||||||
|
) : (
|
||||||
<AccordionOperators></AccordionOperators>
|
<AccordionOperators></AccordionOperators>
|
||||||
|
)}
|
||||||
</HideModalContext.Provider>
|
</HideModalContext.Provider>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
100
web/src/pages/agent/canvas/node/dropdown/operator-item-list.tsx
Normal file
100
web/src/pages/agent/canvas/node/dropdown/operator-item-list.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { Operator } from '@/constants/agent';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { AgentInstanceContext, HandleContext } from '@/pages/agent/context';
|
||||||
|
import OperatorIcon from '@/pages/agent/operator-icon';
|
||||||
|
import { Position } from '@xyflow/react';
|
||||||
|
import { lowerFirst } from 'lodash';
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export type OperatorItemProps = {
|
||||||
|
operators: Operator[];
|
||||||
|
isCustomDropdown?: boolean;
|
||||||
|
mousePosition?: { x: number; y: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HideModalContext = createContext<IModalProps<any>['showModal']>(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
export const OnNodeCreatedContext = createContext<
|
||||||
|
((newNodeId: string) => void) | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export function OperatorItemList({
|
||||||
|
operators,
|
||||||
|
isCustomDropdown = false,
|
||||||
|
mousePosition,
|
||||||
|
}: OperatorItemProps) {
|
||||||
|
const { addCanvasNode } = useContext(AgentInstanceContext);
|
||||||
|
const handleContext = useContext(HandleContext);
|
||||||
|
const hideModal = useContext(HideModalContext);
|
||||||
|
const onNodeCreated = useContext(OnNodeCreatedContext);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleClick =
|
||||||
|
(operator: Operator): React.MouseEventHandler<HTMLElement> =>
|
||||||
|
(e) => {
|
||||||
|
const contextData = handleContext || {
|
||||||
|
nodeId: '',
|
||||||
|
id: '',
|
||||||
|
type: 'source' as const,
|
||||||
|
position: Position.Right,
|
||||||
|
isFromConnectionDrag: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockEvent = mousePosition
|
||||||
|
? {
|
||||||
|
clientX: mousePosition.x,
|
||||||
|
clientY: mousePosition.y,
|
||||||
|
}
|
||||||
|
: e;
|
||||||
|
|
||||||
|
const newNodeId = addCanvasNode(operator, contextData)(mockEvent);
|
||||||
|
|
||||||
|
if (onNodeCreated && newNodeId) {
|
||||||
|
onNodeCreated(newNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideModal?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderOperatorItem = (operator: Operator) => {
|
||||||
|
const commonContent = (
|
||||||
|
<div className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start">
|
||||||
|
<OperatorIcon name={operator} />
|
||||||
|
{t(`flow.${lowerFirst(operator)}`)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={operator}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
{isCustomDropdown ? (
|
||||||
|
<li onClick={handleClick(operator)}>{commonContent}</li>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={operator}
|
||||||
|
className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
|
||||||
|
onClick={handleClick(operator)}
|
||||||
|
onSelect={() => hideModal?.()}
|
||||||
|
>
|
||||||
|
<OperatorIcon name={operator} />
|
||||||
|
{t(`flow.${lowerFirst(operator)}`)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" sideOffset={24}>
|
||||||
|
<p>{t(`flow.${lowerFirst(operator)}Description`)}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
|
||||||
|
}
|
||||||
18
web/src/pages/agent/canvas/node/extractor-node.tsx
Normal file
18
web/src/pages/agent/canvas/node/extractor-node.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import LLMLabel from '@/components/llm-select/llm-label';
|
||||||
|
import { IRagNode } from '@/interfaces/database/agent';
|
||||||
|
import { NodeProps } from '@xyflow/react';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { LabelCard } from './card';
|
||||||
|
import { RagNode } from './index';
|
||||||
|
|
||||||
|
export function ExtractorNode({ ...props }: NodeProps<IRagNode>) {
|
||||||
|
const { data } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RagNode {...props}>
|
||||||
|
<LabelCard>
|
||||||
|
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||||
|
</LabelCard>
|
||||||
|
</RagNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
62
web/src/pages/agent/canvas/node/file-node.tsx
Normal file
62
web/src/pages/agent/canvas/node/file-node.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { IBeginNode } from '@/interfaces/database/flow';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
BeginQueryType,
|
||||||
|
BeginQueryTypeIconMap,
|
||||||
|
NodeHandleId,
|
||||||
|
Operator,
|
||||||
|
} from '../../constant';
|
||||||
|
import { BeginQuery } from '../../interface';
|
||||||
|
import OperatorIcon from '../../operator-icon';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { RightHandleStyle } from './handle-icon';
|
||||||
|
import styles from './index.less';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
|
||||||
|
// TODO: do not allow other nodes to connect to this node
|
||||||
|
function InnerFileNode({ data, id, selected }: NodeProps<IBeginNode>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable
|
||||||
|
style={RightHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
id={NodeHandleId.Start}
|
||||||
|
></CommonHandle>
|
||||||
|
|
||||||
|
<section className="flex items-center gap-2">
|
||||||
|
<OperatorIcon name={data.label as Operator}></OperatorIcon>
|
||||||
|
<div className="truncate text-center font-semibold text-sm">
|
||||||
|
{t(`dataflow.begin`)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className={cn(styles.generateParameters, 'flex gap-2 flex-col')}>
|
||||||
|
{Object.entries(inputs).map(([key, val], idx) => {
|
||||||
|
const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={cn(styles.conditionBlock, 'flex gap-1.5 items-center')}
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
<label htmlFor="">{key}</label>
|
||||||
|
<span className={styles.parameterValue}>{val.name}</span>
|
||||||
|
<span className="flex-1">{val.optional ? 'Yes' : 'No'}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</NodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileNode = memo(InnerFileNode);
|
||||||
@ -6,7 +6,7 @@ import { useMemo } from 'react';
|
|||||||
import { NodeHandleId } from '../../constant';
|
import { NodeHandleId } from '../../constant';
|
||||||
import { HandleContext } from '../../context';
|
import { HandleContext } from '../../context';
|
||||||
import { useDropdownManager } from '../context';
|
import { useDropdownManager } from '../context';
|
||||||
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
|
import { NextStepDropdown } from './dropdown/next-step-dropdown';
|
||||||
|
|
||||||
export function CommonHandle({
|
export function CommonHandle({
|
||||||
className,
|
className,
|
||||||
@ -50,14 +50,15 @@ export function CommonHandle({
|
|||||||
>
|
>
|
||||||
<Plus className="size-3 pointer-events-none text-white hidden group-hover:inline-block" />
|
<Plus className="size-3 pointer-events-none text-white hidden group-hover:inline-block" />
|
||||||
{visible && (
|
{visible && (
|
||||||
<InnerNextStepDropdown
|
<NextStepDropdown
|
||||||
|
nodeId={nodeId}
|
||||||
hideModal={() => {
|
hideModal={() => {
|
||||||
hideModal();
|
hideModal();
|
||||||
clearActiveDropdown();
|
clearActiveDropdown();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span></span>
|
<span></span>
|
||||||
</InnerNextStepDropdown>
|
</NextStepDropdown>
|
||||||
)}
|
)}
|
||||||
</Handle>
|
</Handle>
|
||||||
</HandleContext.Provider>
|
</HandleContext.Provider>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { IRagNode } from '@/interfaces/database/flow';
|
|||||||
import { NodeProps, Position } from '@xyflow/react';
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { NodeHandleId } from '../../constant';
|
import { NodeHandleId } from '../../constant';
|
||||||
import { needsSingleStepDebugging } from '../../utils';
|
import { needsSingleStepDebugging, showCopyIcon } from '../../utils';
|
||||||
import { CommonHandle, LeftEndHandle } from './handle';
|
import { CommonHandle, LeftEndHandle } from './handle';
|
||||||
import { RightHandleStyle } from './handle-icon';
|
import { RightHandleStyle } from './handle-icon';
|
||||||
import NodeHeader from './node-header';
|
import NodeHeader from './node-header';
|
||||||
@ -21,6 +21,7 @@ function InnerRagNode({
|
|||||||
id={id}
|
id={id}
|
||||||
label={data.label}
|
label={data.label}
|
||||||
showRun={needsSingleStepDebugging(data.label)}
|
showRun={needsSingleStepDebugging(data.label)}
|
||||||
|
showCopy={showCopyIcon(data.label)}
|
||||||
>
|
>
|
||||||
<NodeWrapper selected={selected}>
|
<NodeWrapper selected={selected}>
|
||||||
<LeftEndHandle></LeftEndHandle>
|
<LeftEndHandle></LeftEndHandle>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ interface IProps {
|
|||||||
gap?: number;
|
gap?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerNodeHeader = ({
|
const InnerNodeHeader = ({
|
||||||
@ -16,11 +17,12 @@ const InnerNodeHeader = ({
|
|||||||
name,
|
name,
|
||||||
className,
|
className,
|
||||||
wrapperClassName,
|
wrapperClassName,
|
||||||
|
icon,
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
return (
|
return (
|
||||||
<section className={cn(wrapperClassName, 'pb-2')}>
|
<section className={cn(wrapperClassName, 'pb-2')}>
|
||||||
<div className={cn(className, 'flex gap-2.5')}>
|
<div className={cn(className, 'flex gap-2.5')}>
|
||||||
<OperatorIcon name={label as Operator}></OperatorIcon>
|
{icon || <OperatorIcon name={label as Operator}></OperatorIcon>}
|
||||||
<span className="truncate text-center font-semibold text-sm">
|
<span className="truncate text-center font-semibold text-sm">
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
57
web/src/pages/agent/canvas/node/parser-node.tsx
Normal file
57
web/src/pages/agent/canvas/node/parser-node.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { NodeCollapsible } from '@/components/collapse';
|
||||||
|
import { BaseNode } from '@/interfaces/database/agent';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { NodeHandleId } from '../../constant';
|
||||||
|
import { ParserFormSchemaType } from '../../form/parser-form';
|
||||||
|
import { LabelCard } from './card';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
|
||||||
|
function ParserNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<BaseNode<ParserFormSchemaType>>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
<CommonHandle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
id={NodeHandleId.Start}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
></CommonHandle>
|
||||||
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
|
|
||||||
|
<NodeCollapsible items={data.form?.setups}>
|
||||||
|
{(x, idx) => (
|
||||||
|
<LabelCard
|
||||||
|
key={idx}
|
||||||
|
className="flex flex-col text-text-primary gap-1"
|
||||||
|
>
|
||||||
|
<span className="text-text-secondary">Parser {idx + 1}</span>
|
||||||
|
{t(`dataflow.fileFormatOptions.${x.fileFormat}`)}
|
||||||
|
</LabelCard>
|
||||||
|
)}
|
||||||
|
</NodeCollapsible>
|
||||||
|
</NodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ParserNode);
|
||||||
58
web/src/pages/agent/canvas/node/splitter-node.tsx
Normal file
58
web/src/pages/agent/canvas/node/splitter-node.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { IRagNode } from '@/interfaces/database/flow';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { PropsWithChildren, memo } from 'react';
|
||||||
|
import { NodeHandleId, Operator } from '../../constant';
|
||||||
|
import OperatorIcon from '../../operator-icon';
|
||||||
|
import { LabelCard } from './card';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
|
||||||
|
type RagNodeProps = NodeProps<IRagNode> & PropsWithChildren;
|
||||||
|
function InnerSplitterNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: RagNodeProps) {
|
||||||
|
return (
|
||||||
|
<ToolBar
|
||||||
|
selected={selected}
|
||||||
|
id={id}
|
||||||
|
label={data.label}
|
||||||
|
showCopy={false}
|
||||||
|
showRun={false}
|
||||||
|
>
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
<CommonHandle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
id={NodeHandleId.Start}
|
||||||
|
style={RightHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
isConnectableEnd={false}
|
||||||
|
></CommonHandle>
|
||||||
|
<NodeHeader
|
||||||
|
id={id}
|
||||||
|
name={'Chunker'}
|
||||||
|
label={data.label}
|
||||||
|
icon={<OperatorIcon name={Operator.Splitter}></OperatorIcon>}
|
||||||
|
></NodeHeader>
|
||||||
|
<LabelCard>{data.name}</LabelCard>
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SplitterNode = memo(InnerSplitterNode);
|
||||||
55
web/src/pages/agent/canvas/node/tokenizer-node.tsx
Normal file
55
web/src/pages/agent/canvas/node/tokenizer-node.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { BaseNode } from '@/interfaces/database/agent';
|
||||||
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { NodeHandleId } from '../../constant';
|
||||||
|
import { TokenizerFormSchemaType } from '../../form/tokenizer-form';
|
||||||
|
import { LabelCard } from './card';
|
||||||
|
import { CommonHandle } from './handle';
|
||||||
|
import { LeftHandleStyle } from './handle-icon';
|
||||||
|
import NodeHeader from './node-header';
|
||||||
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
import { ToolBar } from './toolbar';
|
||||||
|
|
||||||
|
function TokenizerNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
isConnectable = true,
|
||||||
|
selected,
|
||||||
|
}: NodeProps<BaseNode<TokenizerFormSchemaType>>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolBar
|
||||||
|
selected={selected}
|
||||||
|
id={id}
|
||||||
|
label={data.label}
|
||||||
|
showRun={false}
|
||||||
|
showCopy={false}
|
||||||
|
>
|
||||||
|
<NodeWrapper selected={selected}>
|
||||||
|
<CommonHandle
|
||||||
|
id={NodeHandleId.End}
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
style={LeftHandleStyle}
|
||||||
|
nodeId={id}
|
||||||
|
></CommonHandle>
|
||||||
|
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||||
|
<LabelCard className="text-text-primary flex justify-between flex-col gap-1">
|
||||||
|
<span className="text-text-secondary">
|
||||||
|
{t('dataflow.searchMethod')}
|
||||||
|
</span>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{data.form?.search_method.map((x) => (
|
||||||
|
<li key={x}>{t(`dataflow.tokenizerSearchMethodOptions.${x}`)}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</LabelCard>
|
||||||
|
</NodeWrapper>
|
||||||
|
</ToolBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TokenizerNode);
|
||||||
@ -28,6 +28,7 @@ type ToolBarProps = {
|
|||||||
label: string;
|
label: string;
|
||||||
id: string;
|
id: string;
|
||||||
showRun?: boolean;
|
showRun?: boolean;
|
||||||
|
showCopy?: boolean;
|
||||||
} & PropsWithChildren;
|
} & PropsWithChildren;
|
||||||
|
|
||||||
export function ToolBar({
|
export function ToolBar({
|
||||||
@ -36,6 +37,7 @@ export function ToolBar({
|
|||||||
label,
|
label,
|
||||||
id,
|
id,
|
||||||
showRun = true,
|
showRun = true,
|
||||||
|
showCopy = true,
|
||||||
}: ToolBarProps) {
|
}: ToolBarProps) {
|
||||||
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
||||||
const deleteIterationNodeById = useGraphStore(
|
const deleteIterationNodeById = useGraphStore(
|
||||||
@ -74,10 +76,12 @@ export function ToolBar({
|
|||||||
<IconWrapper>
|
<IconWrapper>
|
||||||
<Play className="size-3.5" data-play />
|
<Play className="size-3.5" data-play />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
)}{' '}
|
)}
|
||||||
|
{showCopy && (
|
||||||
<IconWrapper onClick={handleDuplicate}>
|
<IconWrapper onClick={handleDuplicate}>
|
||||||
<Copy className="size-3.5" />
|
<Copy className="size-3.5" />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
|
)}
|
||||||
<IconWrapper onClick={deleteNode}>
|
<IconWrapper onClick={deleteNode}>
|
||||||
<Trash2 className="size-3.5" />
|
<Trash2 className="size-3.5" />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
|
|||||||
@ -6,9 +6,13 @@ import {
|
|||||||
AgentGlobals,
|
AgentGlobals,
|
||||||
AgentGlobalsSysQueryWithBrace,
|
AgentGlobalsSysQueryWithBrace,
|
||||||
CodeTemplateStrMap,
|
CodeTemplateStrMap,
|
||||||
|
Operator,
|
||||||
ProgrammingLanguage,
|
ProgrammingLanguage,
|
||||||
initialLlmBaseValues,
|
initialLlmBaseValues,
|
||||||
} from '@/constants/agent';
|
} from '@/constants/agent';
|
||||||
|
export { Operator } from '@/constants/agent';
|
||||||
|
|
||||||
|
export * from './pipeline';
|
||||||
|
|
||||||
export enum AgentDialogueMode {
|
export enum AgentDialogueMode {
|
||||||
Conversational = 'conversational',
|
Conversational = 'conversational',
|
||||||
@ -43,51 +47,6 @@ import {
|
|||||||
|
|
||||||
export const BeginId = 'begin';
|
export const BeginId = 'begin';
|
||||||
|
|
||||||
export enum Operator {
|
|
||||||
Begin = 'Begin',
|
|
||||||
Retrieval = 'Retrieval',
|
|
||||||
Categorize = 'Categorize',
|
|
||||||
Message = 'Message',
|
|
||||||
Relevant = 'Relevant',
|
|
||||||
RewriteQuestion = 'RewriteQuestion',
|
|
||||||
KeywordExtract = 'KeywordExtract',
|
|
||||||
Baidu = 'Baidu',
|
|
||||||
DuckDuckGo = 'DuckDuckGo',
|
|
||||||
Wikipedia = 'Wikipedia',
|
|
||||||
PubMed = 'PubMed',
|
|
||||||
ArXiv = 'ArXiv',
|
|
||||||
Google = 'Google',
|
|
||||||
Bing = 'Bing',
|
|
||||||
GoogleScholar = 'GoogleScholar',
|
|
||||||
DeepL = 'DeepL',
|
|
||||||
GitHub = 'GitHub',
|
|
||||||
BaiduFanyi = 'BaiduFanyi',
|
|
||||||
QWeather = 'QWeather',
|
|
||||||
ExeSQL = 'ExeSQL',
|
|
||||||
Switch = 'Switch',
|
|
||||||
WenCai = 'WenCai',
|
|
||||||
AkShare = 'AkShare',
|
|
||||||
YahooFinance = 'YahooFinance',
|
|
||||||
Jin10 = 'Jin10',
|
|
||||||
TuShare = 'TuShare',
|
|
||||||
Note = 'Note',
|
|
||||||
Crawler = 'Crawler',
|
|
||||||
Invoke = 'Invoke',
|
|
||||||
Email = 'Email',
|
|
||||||
Iteration = 'Iteration',
|
|
||||||
IterationStart = 'IterationItem',
|
|
||||||
Code = 'CodeExec',
|
|
||||||
WaitingDialogue = 'WaitingDialogue',
|
|
||||||
Agent = 'Agent',
|
|
||||||
Tool = 'Tool',
|
|
||||||
TavilySearch = 'TavilySearch',
|
|
||||||
TavilyExtract = 'TavilyExtract',
|
|
||||||
UserFillUp = 'UserFillUp',
|
|
||||||
StringTransform = 'StringTransform',
|
|
||||||
SearXNG = 'SearXNG',
|
|
||||||
Placeholder = 'Placeholder',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SwitchLogicOperatorOptions = ['and', 'or'];
|
export const SwitchLogicOperatorOptions = ['and', 'or'];
|
||||||
|
|
||||||
export const CommonOperatorList = Object.values(Operator).filter(
|
export const CommonOperatorList = Object.values(Operator).filter(
|
||||||
@ -833,6 +792,11 @@ export const RestrictedUpstreamMap = {
|
|||||||
[Operator.UserFillUp]: [Operator.Begin],
|
[Operator.UserFillUp]: [Operator.Begin],
|
||||||
[Operator.Tool]: [Operator.Begin],
|
[Operator.Tool]: [Operator.Begin],
|
||||||
[Operator.Placeholder]: [Operator.Begin],
|
[Operator.Placeholder]: [Operator.Begin],
|
||||||
|
[Operator.Parser]: [Operator.Begin],
|
||||||
|
[Operator.Splitter]: [Operator.Begin],
|
||||||
|
[Operator.HierarchicalMerger]: [Operator.Begin],
|
||||||
|
[Operator.Tokenizer]: [Operator.Begin],
|
||||||
|
[Operator.Extractor]: [Operator.Begin],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NodeMap = {
|
export const NodeMap = {
|
||||||
@ -878,6 +842,12 @@ export const NodeMap = {
|
|||||||
[Operator.StringTransform]: 'ragNode',
|
[Operator.StringTransform]: 'ragNode',
|
||||||
[Operator.TavilyExtract]: 'ragNode',
|
[Operator.TavilyExtract]: 'ragNode',
|
||||||
[Operator.Placeholder]: 'placeholderNode',
|
[Operator.Placeholder]: 'placeholderNode',
|
||||||
|
[Operator.File]: 'fileNode',
|
||||||
|
[Operator.Parser]: 'parserNode',
|
||||||
|
[Operator.Tokenizer]: 'tokenizerNode',
|
||||||
|
[Operator.Splitter]: 'splitterNode',
|
||||||
|
[Operator.HierarchicalMerger]: 'splitterNode',
|
||||||
|
[Operator.Extractor]: 'contextNode',
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum BeginQueryType {
|
export enum BeginQueryType {
|
||||||
@ -906,6 +876,21 @@ export const NoDebugOperatorsList = [
|
|||||||
Operator.Iteration,
|
Operator.Iteration,
|
||||||
Operator.UserFillUp,
|
Operator.UserFillUp,
|
||||||
Operator.IterationStart,
|
Operator.IterationStart,
|
||||||
|
Operator.File,
|
||||||
|
Operator.Parser,
|
||||||
|
Operator.Tokenizer,
|
||||||
|
Operator.Splitter,
|
||||||
|
Operator.HierarchicalMerger,
|
||||||
|
Operator.Extractor,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NoCopyOperatorsList = [
|
||||||
|
Operator.File,
|
||||||
|
Operator.Parser,
|
||||||
|
Operator.Tokenizer,
|
||||||
|
Operator.Splitter,
|
||||||
|
Operator.HierarchicalMerger,
|
||||||
|
Operator.Extractor,
|
||||||
];
|
];
|
||||||
|
|
||||||
export enum NodeHandleId {
|
export enum NodeHandleId {
|
||||||
272
web/src/pages/agent/constant/pipeline.tsx
Normal file
272
web/src/pages/agent/constant/pipeline.tsx
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
|
||||||
|
import {
|
||||||
|
initialLlmBaseValues,
|
||||||
|
DataflowOperator as Operator,
|
||||||
|
} from '@/constants/agent';
|
||||||
|
|
||||||
|
export enum FileType {
|
||||||
|
PDF = 'pdf',
|
||||||
|
Spreadsheet = 'spreadsheet',
|
||||||
|
Image = 'image',
|
||||||
|
Email = 'email',
|
||||||
|
TextMarkdown = 'text&markdown',
|
||||||
|
Docx = 'word',
|
||||||
|
PowerPoint = 'slides',
|
||||||
|
Video = 'video',
|
||||||
|
Audio = 'audio',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PdfOutputFormat {
|
||||||
|
Json = 'json',
|
||||||
|
Markdown = 'markdown',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SpreadsheetOutputFormat {
|
||||||
|
Json = 'json',
|
||||||
|
Html = 'html',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ImageOutputFormat {
|
||||||
|
Text = 'text',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EmailOutputFormat {
|
||||||
|
Json = 'json',
|
||||||
|
Text = 'text',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TextMarkdownOutputFormat {
|
||||||
|
Text = 'text',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DocxOutputFormat {
|
||||||
|
Markdown = 'markdown',
|
||||||
|
Json = 'json',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PptOutputFormat {
|
||||||
|
Json = 'json',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VideoOutputFormat {
|
||||||
|
Json = 'json',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AudioOutputFormat {
|
||||||
|
Text = 'text',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OutputFormatMap = {
|
||||||
|
[FileType.PDF]: PdfOutputFormat,
|
||||||
|
[FileType.Spreadsheet]: SpreadsheetOutputFormat,
|
||||||
|
[FileType.Image]: ImageOutputFormat,
|
||||||
|
[FileType.Email]: EmailOutputFormat,
|
||||||
|
[FileType.TextMarkdown]: TextMarkdownOutputFormat,
|
||||||
|
[FileType.Docx]: DocxOutputFormat,
|
||||||
|
[FileType.PowerPoint]: PptOutputFormat,
|
||||||
|
[FileType.Video]: VideoOutputFormat,
|
||||||
|
[FileType.Audio]: AudioOutputFormat,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InitialOutputFormatMap = {
|
||||||
|
[FileType.PDF]: PdfOutputFormat.Json,
|
||||||
|
[FileType.Spreadsheet]: SpreadsheetOutputFormat.Html,
|
||||||
|
[FileType.Image]: ImageOutputFormat.Text,
|
||||||
|
[FileType.Email]: EmailOutputFormat.Text,
|
||||||
|
[FileType.TextMarkdown]: TextMarkdownOutputFormat.Text,
|
||||||
|
[FileType.Docx]: DocxOutputFormat.Json,
|
||||||
|
[FileType.PowerPoint]: PptOutputFormat.Json,
|
||||||
|
[FileType.Video]: VideoOutputFormat.Json,
|
||||||
|
[FileType.Audio]: AudioOutputFormat.Text,
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ContextGeneratorFieldName {
|
||||||
|
Summary = 'summary',
|
||||||
|
Keywords = 'keywords',
|
||||||
|
Questions = 'questions',
|
||||||
|
Metadata = 'metadata',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileId = 'File'; // BeginId
|
||||||
|
|
||||||
|
export enum TokenizerSearchMethod {
|
||||||
|
Embedding = 'embedding',
|
||||||
|
FullText = 'full_text',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ImageParseMethod {
|
||||||
|
OCR = 'ocr',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TokenizerFields {
|
||||||
|
Text = 'text',
|
||||||
|
Questions = 'questions',
|
||||||
|
Summary = 'summary',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ParserFields {
|
||||||
|
From = 'from',
|
||||||
|
To = 'to',
|
||||||
|
Cc = 'cc',
|
||||||
|
Bcc = 'bcc',
|
||||||
|
Date = 'date',
|
||||||
|
Subject = 'subject',
|
||||||
|
Body = 'body',
|
||||||
|
Attachments = 'attachments',
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialBeginValues
|
||||||
|
export const initialFileValues = {
|
||||||
|
outputs: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
type: 'Object',
|
||||||
|
value: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialTokenizerValues = {
|
||||||
|
search_method: [
|
||||||
|
TokenizerSearchMethod.Embedding,
|
||||||
|
TokenizerSearchMethod.FullText,
|
||||||
|
],
|
||||||
|
filename_embd_weight: 0.1,
|
||||||
|
fields: TokenizerFields.Text,
|
||||||
|
outputs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum StringTransformMethod {
|
||||||
|
Merge = 'merge',
|
||||||
|
Split = 'split',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StringTransformDelimiter {
|
||||||
|
Comma = ',',
|
||||||
|
Semicolon = ';',
|
||||||
|
Period = '.',
|
||||||
|
LineBreak = '\n',
|
||||||
|
Tab = '\t',
|
||||||
|
Space = ' ',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialParserValues = {
|
||||||
|
outputs: {
|
||||||
|
markdown: { type: 'string', value: '' },
|
||||||
|
text: { type: 'string', value: '' },
|
||||||
|
html: { type: 'string', value: '' },
|
||||||
|
json: { type: 'Array<object>', value: [] },
|
||||||
|
},
|
||||||
|
setups: [
|
||||||
|
{
|
||||||
|
fileFormat: FileType.PDF,
|
||||||
|
output_format: PdfOutputFormat.Json,
|
||||||
|
parse_method: ParseDocumentType.DeepDOC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileFormat: FileType.Spreadsheet,
|
||||||
|
output_format: SpreadsheetOutputFormat.Html,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileFormat: FileType.Image,
|
||||||
|
output_format: ImageOutputFormat.Text,
|
||||||
|
parse_method: ImageParseMethod.OCR,
|
||||||
|
system_prompt: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileFormat: FileType.Email,
|
||||||
|
fields: Object.values(ParserFields),
|
||||||
|
output_format: EmailOutputFormat.Text,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileFormat: FileType.TextMarkdown,
|
||||||
|
output_format: TextMarkdownOutputFormat.Text,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileFormat: FileType.Docx,
|
||||||
|
output_format: DocxOutputFormat.Json,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileFormat: FileType.PowerPoint,
|
||||||
|
output_format: PptOutputFormat.Json,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialSplitterValues = {
|
||||||
|
outputs: {
|
||||||
|
chunks: { type: 'Array<Object>', value: [] },
|
||||||
|
},
|
||||||
|
chunk_token_size: 512,
|
||||||
|
overlapped_percent: 0,
|
||||||
|
delimiters: [{ value: '\n' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum Hierarchy {
|
||||||
|
H1 = '1',
|
||||||
|
H2 = '2',
|
||||||
|
H3 = '3',
|
||||||
|
H4 = '4',
|
||||||
|
H5 = '5',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialHierarchicalMergerValues = {
|
||||||
|
outputs: {
|
||||||
|
chunks: { type: 'Array<Object>', value: [] },
|
||||||
|
},
|
||||||
|
hierarchy: Hierarchy.H3,
|
||||||
|
levels: [
|
||||||
|
{ expressions: [{ expression: '^#[^#]' }] },
|
||||||
|
{ expressions: [{ expression: '^##[^#]' }] },
|
||||||
|
{ expressions: [{ expression: '^###[^#]' }] },
|
||||||
|
{ expressions: [{ expression: '^####[^#]' }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialExtractorValues = {
|
||||||
|
...initialLlmBaseValues,
|
||||||
|
field_name: ContextGeneratorFieldName.Summary,
|
||||||
|
outputs: {
|
||||||
|
chunks: { type: 'Array<Object>', value: [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoDebugOperatorsList = [Operator.Begin];
|
||||||
|
|
||||||
|
export const FileTypeSuffixMap = {
|
||||||
|
[FileType.PDF]: ['pdf'],
|
||||||
|
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],
|
||||||
|
[FileType.Image]: ['jpg', 'jpeg', 'png', 'gif'],
|
||||||
|
[FileType.Email]: ['eml', 'msg'],
|
||||||
|
[FileType.TextMarkdown]: ['md', 'markdown', 'mdx', 'txt'],
|
||||||
|
[FileType.Docx]: ['doc', 'docx'],
|
||||||
|
[FileType.PowerPoint]: ['pptx'],
|
||||||
|
[FileType.Video]: [],
|
||||||
|
[FileType.Audio]: [
|
||||||
|
'da',
|
||||||
|
'wave',
|
||||||
|
'wav',
|
||||||
|
'mp3',
|
||||||
|
'aac',
|
||||||
|
'flac',
|
||||||
|
'ogg',
|
||||||
|
'aiff',
|
||||||
|
'au',
|
||||||
|
'midi',
|
||||||
|
'wma',
|
||||||
|
'realaudio',
|
||||||
|
'vqf',
|
||||||
|
'oggvorbis',
|
||||||
|
'ape',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SingleOperators = [
|
||||||
|
Operator.Tokenizer,
|
||||||
|
Operator.Splitter,
|
||||||
|
Operator.HierarchicalMerger,
|
||||||
|
Operator.Parser,
|
||||||
|
];
|
||||||
@ -48,3 +48,13 @@ export type HandleContextType = {
|
|||||||
export const HandleContext = createContext<HandleContextType>(
|
export const HandleContext = createContext<HandleContextType>(
|
||||||
{} as HandleContextType,
|
{} as HandleContextType,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type PipelineLogContextType = {
|
||||||
|
messageId: string;
|
||||||
|
setMessageId: (messageId: string) => void;
|
||||||
|
setUploadedFileData: (data: Record<string, any>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PipelineLogContext = createContext<PipelineLogContextType>(
|
||||||
|
{} as PipelineLogContextType,
|
||||||
|
);
|
||||||
|
|||||||
@ -13,25 +13,30 @@ import DeepLForm from '../form/deepl-form';
|
|||||||
import DuckDuckGoForm from '../form/duckduckgo-form';
|
import DuckDuckGoForm from '../form/duckduckgo-form';
|
||||||
import EmailForm from '../form/email-form';
|
import EmailForm from '../form/email-form';
|
||||||
import ExeSQLForm from '../form/exesql-form';
|
import ExeSQLForm from '../form/exesql-form';
|
||||||
|
import ExtractorForm from '../form/extractor-form';
|
||||||
import GithubForm from '../form/github-form';
|
import GithubForm from '../form/github-form';
|
||||||
import GoogleForm from '../form/google-form';
|
import GoogleForm from '../form/google-form';
|
||||||
import GoogleScholarForm from '../form/google-scholar-form';
|
import GoogleScholarForm from '../form/google-scholar-form';
|
||||||
|
import HierarchicalMergerForm from '../form/hierarchical-merger-form';
|
||||||
import InvokeForm from '../form/invoke-form';
|
import InvokeForm from '../form/invoke-form';
|
||||||
import IterationForm from '../form/iteration-form';
|
import IterationForm from '../form/iteration-form';
|
||||||
import IterationStartForm from '../form/iteration-start-from';
|
import IterationStartForm from '../form/iteration-start-from';
|
||||||
import Jin10Form from '../form/jin10-form';
|
import Jin10Form from '../form/jin10-form';
|
||||||
import KeywordExtractForm from '../form/keyword-extract-form';
|
import KeywordExtractForm from '../form/keyword-extract-form';
|
||||||
import MessageForm from '../form/message-form';
|
import MessageForm from '../form/message-form';
|
||||||
|
import ParserForm from '../form/parser-form';
|
||||||
import PubMedForm from '../form/pubmed-form';
|
import PubMedForm from '../form/pubmed-form';
|
||||||
import QWeatherForm from '../form/qweather-form';
|
import QWeatherForm from '../form/qweather-form';
|
||||||
import RelevantForm from '../form/relevant-form';
|
import RelevantForm from '../form/relevant-form';
|
||||||
import RetrievalForm from '../form/retrieval-form/next';
|
import RetrievalForm from '../form/retrieval-form/next';
|
||||||
import RewriteQuestionForm from '../form/rewrite-question-form';
|
import RewriteQuestionForm from '../form/rewrite-question-form';
|
||||||
import SearXNGForm from '../form/searxng-form';
|
import SearXNGForm from '../form/searxng-form';
|
||||||
|
import SplitterForm from '../form/splitter-form';
|
||||||
import StringTransformForm from '../form/string-transform-form';
|
import StringTransformForm from '../form/string-transform-form';
|
||||||
import SwitchForm from '../form/switch-form';
|
import SwitchForm from '../form/switch-form';
|
||||||
import TavilyExtractForm from '../form/tavily-extract-form';
|
import TavilyExtractForm from '../form/tavily-extract-form';
|
||||||
import TavilyForm from '../form/tavily-form';
|
import TavilyForm from '../form/tavily-form';
|
||||||
|
import TokenizerForm from '../form/tokenizer-form';
|
||||||
import ToolForm from '../form/tool-form';
|
import ToolForm from '../form/tool-form';
|
||||||
import TuShareForm from '../form/tushare-form';
|
import TuShareForm from '../form/tushare-form';
|
||||||
import UserFillUpForm from '../form/user-fill-up-form';
|
import UserFillUpForm from '../form/user-fill-up-form';
|
||||||
@ -163,4 +168,26 @@ export const FormConfigMap = {
|
|||||||
[Operator.TavilyExtract]: {
|
[Operator.TavilyExtract]: {
|
||||||
component: TavilyExtractForm,
|
component: TavilyExtractForm,
|
||||||
},
|
},
|
||||||
|
[Operator.Placeholder]: {
|
||||||
|
component: () => <></>,
|
||||||
|
},
|
||||||
|
// pipeline
|
||||||
|
[Operator.File]: {
|
||||||
|
component: () => <></>,
|
||||||
|
},
|
||||||
|
[Operator.Parser]: {
|
||||||
|
component: ParserForm,
|
||||||
|
},
|
||||||
|
[Operator.Tokenizer]: {
|
||||||
|
component: TokenizerForm,
|
||||||
|
},
|
||||||
|
[Operator.Splitter]: {
|
||||||
|
component: SplitterForm,
|
||||||
|
},
|
||||||
|
[Operator.HierarchicalMerger]: {
|
||||||
|
component: HierarchicalMergerForm,
|
||||||
|
},
|
||||||
|
[Operator.Extractor]: {
|
||||||
|
component: ExtractorForm,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
107
web/src/pages/agent/form/extractor-form/index.tsx
Normal file
107
web/src/pages/agent/form/extractor-form/index.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||||
|
import { LargeModelFormField } from '@/components/large-model-form-field';
|
||||||
|
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
|
||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
|
||||||
|
import { buildOptions } from '@/utils/form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
ContextGeneratorFieldName,
|
||||||
|
initialExtractorValues,
|
||||||
|
} from '../../constant/pipeline';
|
||||||
|
import { useBuildNodeOutputOptions } from '../../hooks/use-build-options';
|
||||||
|
import { useFormValues } from '../../hooks/use-form-values';
|
||||||
|
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
|
import { Output } from '../components/output';
|
||||||
|
import { useSwitchPrompt } from './use-switch-prompt';
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
field_name: z.string(),
|
||||||
|
sys_prompt: z.string(),
|
||||||
|
prompts: z.string().optional(),
|
||||||
|
...LlmSettingSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialExtractorValues.outputs);
|
||||||
|
|
||||||
|
const ExtractorForm = ({ node }: INextOperatorForm) => {
|
||||||
|
const defaultValues = useFormValues(initialExtractorValues, node);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const form = useForm<ExtractorFormSchemaType>({
|
||||||
|
defaultValues,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
// mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const promptOptions = useBuildNodeOutputOptions(node?.id);
|
||||||
|
|
||||||
|
const options = buildOptions(ContextGeneratorFieldName, t, 'dataflow');
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFieldNameChange,
|
||||||
|
confirmSwitch,
|
||||||
|
hideModal,
|
||||||
|
visible,
|
||||||
|
cancelSwitch,
|
||||||
|
} = useSwitchPrompt(form);
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<LargeModelFormField></LargeModelFormField>
|
||||||
|
<RAGFlowFormItem label={t('dataflow.fieldName')} name="field_name">
|
||||||
|
{(field) => (
|
||||||
|
<SelectWithSearch
|
||||||
|
onChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
handleFieldNameChange(value);
|
||||||
|
}}
|
||||||
|
value={field.value}
|
||||||
|
placeholder={t('dataFlowPlaceholder')}
|
||||||
|
options={options}
|
||||||
|
></SelectWithSearch>
|
||||||
|
)}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<RAGFlowFormItem label={t('flow.systemPrompt')} name="sys_prompt">
|
||||||
|
<PromptEditor
|
||||||
|
placeholder={t('flow.messagePlaceholder')}
|
||||||
|
showToolbar={true}
|
||||||
|
baseOptions={promptOptions}
|
||||||
|
></PromptEditor>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<RAGFlowFormItem label={t('flow.userPrompt')} name="prompts">
|
||||||
|
<PromptEditor
|
||||||
|
showToolbar={true}
|
||||||
|
baseOptions={promptOptions}
|
||||||
|
></PromptEditor>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Output list={outputList}></Output>
|
||||||
|
</FormWrapper>
|
||||||
|
{visible && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
title={t('dataflow.switchPromptMessage')}
|
||||||
|
open
|
||||||
|
onOpenChange={hideModal}
|
||||||
|
onOk={confirmSwitch}
|
||||||
|
onCancel={cancelSwitch}
|
||||||
|
></ConfirmDeleteDialog>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ExtractorForm);
|
||||||
69
web/src/pages/agent/form/extractor-form/use-switch-prompt.ts
Normal file
69
web/src/pages/agent/form/extractor-form/use-switch-prompt.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
|
||||||
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { UseFormReturn } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
field_name: z.string(),
|
||||||
|
sys_prompt: z.string(),
|
||||||
|
prompts: z.string().optional(),
|
||||||
|
...LlmSettingSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
export function useSwitchPrompt(form: UseFormReturn<ExtractorFormSchemaType>) {
|
||||||
|
const { visible, showModal, hideModal } = useSetModalState();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const previousFieldNames = useRef<string[]>([form.getValues('field_name')]);
|
||||||
|
|
||||||
|
const setPromptValue = useCallback(
|
||||||
|
(field: keyof ExtractorFormSchemaType, key: string, value: string) => {
|
||||||
|
form.setValue(field, t(`dataflow.prompts.${key}.${value}`), {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[form, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFieldNameChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
if (value) {
|
||||||
|
const names = previousFieldNames.current;
|
||||||
|
if (names.length > 1) {
|
||||||
|
names.shift();
|
||||||
|
}
|
||||||
|
names.push(value);
|
||||||
|
showModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[showModal],
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmSwitch = useCallback(() => {
|
||||||
|
const value = form.getValues('field_name');
|
||||||
|
setPromptValue('sys_prompt', 'system', value);
|
||||||
|
setPromptValue('prompts', 'user', value);
|
||||||
|
}, [form, setPromptValue]);
|
||||||
|
|
||||||
|
const cancelSwitch = useCallback(() => {
|
||||||
|
const previousValue = previousFieldNames.current.at(-2);
|
||||||
|
if (previousValue) {
|
||||||
|
form.setValue('field_name', previousValue, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleFieldNameChange,
|
||||||
|
confirmSwitch,
|
||||||
|
hideModal,
|
||||||
|
visible,
|
||||||
|
cancelSwitch,
|
||||||
|
};
|
||||||
|
}
|
||||||
191
web/src/pages/agent/form/hierarchical-merger-form/index.tsx
Normal file
191
web/src/pages/agent/form/hierarchical-merger-form/index.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { BlockButton, Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
|
import { Form, FormLabel } from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
Hierarchy,
|
||||||
|
initialHierarchicalMergerValues,
|
||||||
|
} from '../../constant/pipeline';
|
||||||
|
import { useFormValues } from '../../hooks/use-form-values';
|
||||||
|
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
|
import { Output } from '../components/output';
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialHierarchicalMergerValues.outputs);
|
||||||
|
|
||||||
|
const HierarchyOptions = [
|
||||||
|
{ label: 'H1', value: Hierarchy.H1 },
|
||||||
|
{ label: 'H2', value: Hierarchy.H2 },
|
||||||
|
{ label: 'H3', value: Hierarchy.H3 },
|
||||||
|
{ label: 'H4', value: Hierarchy.H4 },
|
||||||
|
{ label: 'H5', value: Hierarchy.H5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
hierarchy: z.string(),
|
||||||
|
levels: z.array(
|
||||||
|
z.object({
|
||||||
|
expressions: z.array(
|
||||||
|
z.object({
|
||||||
|
expression: z.string().refine(
|
||||||
|
(val) => {
|
||||||
|
try {
|
||||||
|
// Try converting the string to a RegExp
|
||||||
|
new RegExp(val);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Must be a valid regular expression string',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type HierarchicalMergerFormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
type RegularExpressionsProps = {
|
||||||
|
index: number;
|
||||||
|
parentName: string;
|
||||||
|
removeParent: (index: number) => void;
|
||||||
|
isLatest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RegularExpressions({
|
||||||
|
index,
|
||||||
|
parentName,
|
||||||
|
isLatest,
|
||||||
|
removeParent,
|
||||||
|
}: RegularExpressionsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
|
||||||
|
const name = `${parentName}.${index}.expressions`;
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex-row justify-between items-center">
|
||||||
|
<span>H{index + 1}</span>
|
||||||
|
{isLatest && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => removeParent(index)}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<FormLabel required className="mb-2 text-text-secondary">
|
||||||
|
{t('dataflow.regularExpressions')}
|
||||||
|
</FormLabel>
|
||||||
|
<section className="space-y-4">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex items-center gap-2">
|
||||||
|
<div className="space-y-2 flex-1">
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={`${name}.${index}.expression`}
|
||||||
|
label={'expression'}
|
||||||
|
labelClassName="!hidden"
|
||||||
|
>
|
||||||
|
<Input className="!m-0"></Input>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
</div>
|
||||||
|
{index === 0 ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => append({ expression: '' })}
|
||||||
|
variant={'ghost'}
|
||||||
|
>
|
||||||
|
<Plus></Plus>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HierarchicalMergerForm = ({ node }: INextOperatorForm) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const defaultValues = useFormValues(initialHierarchicalMergerValues, node);
|
||||||
|
|
||||||
|
const form = useForm<HierarchicalMergerFormSchemaType>({
|
||||||
|
defaultValues,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = 'levels';
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<RAGFlowFormItem name={'hierarchy'} label={t('dataflow.hierarchy')}>
|
||||||
|
<SelectWithSearch options={HierarchyOptions}></SelectWithSearch>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex items-center">
|
||||||
|
<div className="flex-1">
|
||||||
|
<RegularExpressions
|
||||||
|
parentName={name}
|
||||||
|
index={index}
|
||||||
|
removeParent={remove}
|
||||||
|
isLatest={index === fields.length - 1}
|
||||||
|
></RegularExpressions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{fields.length < 5 && (
|
||||||
|
<BlockButton
|
||||||
|
onClick={() => append({ expressions: [{ expression: '' }] })}
|
||||||
|
>
|
||||||
|
{t('common.add')}
|
||||||
|
</BlockButton>
|
||||||
|
)}
|
||||||
|
</FormWrapper>
|
||||||
|
<div className="p-5">
|
||||||
|
<Output list={outputList}></Output>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(HierarchicalMergerForm);
|
||||||
106
web/src/pages/agent/form/parser-form/common-form-fields.tsx
Normal file
106
web/src/pages/agent/form/parser-form/common-form-fields.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { crossLanguageOptions } from '@/components/cross-language-form-field';
|
||||||
|
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
|
||||||
|
import {
|
||||||
|
LLMFormField,
|
||||||
|
LLMFormFieldProps,
|
||||||
|
} from '@/components/llm-setting-items/llm-form-field';
|
||||||
|
import {
|
||||||
|
SelectWithSearch,
|
||||||
|
SelectWithSearchFlagOptionType,
|
||||||
|
} from '@/components/originui/select-with-search';
|
||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { upperCase, upperFirst } from 'lodash';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
FileType,
|
||||||
|
OutputFormatMap,
|
||||||
|
SpreadsheetOutputFormat,
|
||||||
|
} from '../../constant/pipeline';
|
||||||
|
import { CommonProps } from './interface';
|
||||||
|
import { buildFieldNameWithPrefix } from './utils';
|
||||||
|
|
||||||
|
const UppercaseFields = [
|
||||||
|
SpreadsheetOutputFormat.Html,
|
||||||
|
SpreadsheetOutputFormat.Json,
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildOutputOptionsFormatMap() {
|
||||||
|
return Object.entries(OutputFormatMap).reduce<
|
||||||
|
Record<string, SelectWithSearchFlagOptionType[]>
|
||||||
|
>((pre, [key, value]) => {
|
||||||
|
pre[key] = Object.values(value).map((v) => ({
|
||||||
|
label: UppercaseFields.some((x) => x === v)
|
||||||
|
? upperCase(v)
|
||||||
|
: upperFirst(v),
|
||||||
|
value: v,
|
||||||
|
}));
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutputFormatFormFieldProps = CommonProps & {
|
||||||
|
fileType: FileType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OutputFormatFormField({
|
||||||
|
prefix,
|
||||||
|
fileType,
|
||||||
|
}: OutputFormatFormFieldProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={buildFieldNameWithPrefix(`output_format`, prefix)}
|
||||||
|
label={t('dataflow.outputFormat')}
|
||||||
|
>
|
||||||
|
<SelectWithSearch
|
||||||
|
options={buildOutputOptionsFormatMap()[fileType]}
|
||||||
|
></SelectWithSearch>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParserMethodFormField({
|
||||||
|
prefix,
|
||||||
|
optionsWithoutLLM,
|
||||||
|
}: CommonProps & { optionsWithoutLLM?: { value: string; label: string }[] }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<LayoutRecognizeFormField
|
||||||
|
name={buildFieldNameWithPrefix(`parse_method`, prefix)}
|
||||||
|
horizontal={false}
|
||||||
|
optionsWithoutLLM={optionsWithoutLLM}
|
||||||
|
label={t('dataflow.parserMethod')}
|
||||||
|
></LayoutRecognizeFormField>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LargeModelFormField({
|
||||||
|
prefix,
|
||||||
|
options,
|
||||||
|
}: CommonProps & Pick<LLMFormFieldProps, 'options'>) {
|
||||||
|
return (
|
||||||
|
<LLMFormField
|
||||||
|
name={buildFieldNameWithPrefix('llm_id', prefix)}
|
||||||
|
options={options}
|
||||||
|
></LLMFormField>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LanguageFormField({ prefix }: CommonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={buildFieldNameWithPrefix(`lang`, prefix)}
|
||||||
|
label={t('dataflow.lang')}
|
||||||
|
>
|
||||||
|
{(field) => (
|
||||||
|
<SelectWithSearch
|
||||||
|
options={crossLanguageOptions}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
></SelectWithSearch>
|
||||||
|
)}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
web/src/pages/agent/form/parser-form/email-form-fields.tsx
Normal file
30
web/src/pages/agent/form/parser-form/email-form-fields.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { MultiSelect } from '@/components/ui/multi-select';
|
||||||
|
import { buildOptions } from '@/utils/form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ParserFields } from '../../constant/pipeline';
|
||||||
|
import { CommonProps } from './interface';
|
||||||
|
import { buildFieldNameWithPrefix } from './utils';
|
||||||
|
|
||||||
|
const options = buildOptions(ParserFields);
|
||||||
|
|
||||||
|
export function EmailFormFields({ prefix }: CommonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={buildFieldNameWithPrefix(`fields`, prefix)}
|
||||||
|
label={t('dataflow.fields')}
|
||||||
|
>
|
||||||
|
{(field) => (
|
||||||
|
<MultiSelect
|
||||||
|
options={options}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
variant="inverted"
|
||||||
|
></MultiSelect>
|
||||||
|
)}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
web/src/pages/agent/form/parser-form/image-form-fields.tsx
Normal file
60
web/src/pages/agent/form/parser-form/image-form-fields.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { buildOptions } from '@/utils/form';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ImageParseMethod } from '../../constant/pipeline';
|
||||||
|
import { LanguageFormField, ParserMethodFormField } from './common-form-fields';
|
||||||
|
import { CommonProps } from './interface';
|
||||||
|
import { useSetInitialLanguage } from './use-set-initial-language';
|
||||||
|
import { buildFieldNameWithPrefix } from './utils';
|
||||||
|
|
||||||
|
export function ImageFormFields({ prefix }: CommonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
const options = buildOptions(
|
||||||
|
ImageParseMethod,
|
||||||
|
t,
|
||||||
|
'dataflow.imageParseMethodOptions',
|
||||||
|
);
|
||||||
|
const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix);
|
||||||
|
|
||||||
|
const parseMethod = useWatch({
|
||||||
|
name: parseMethodName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const languageShown = useMemo(() => {
|
||||||
|
return !isEmpty(parseMethod) && parseMethod !== ImageParseMethod.OCR;
|
||||||
|
}, [parseMethod]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmpty(form.getValues(parseMethodName))) {
|
||||||
|
form.setValue(parseMethodName, ImageParseMethod.OCR, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, parseMethodName]);
|
||||||
|
|
||||||
|
useSetInitialLanguage({ prefix, languageShown });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ParserMethodFormField
|
||||||
|
prefix={prefix}
|
||||||
|
optionsWithoutLLM={options}
|
||||||
|
></ParserMethodFormField>
|
||||||
|
{languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>}
|
||||||
|
{languageShown && (
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={buildFieldNameWithPrefix('system_prompt', prefix)}
|
||||||
|
label={t('dataflow.systemPrompt')}
|
||||||
|
>
|
||||||
|
<Textarea placeholder={t('dataflow.systemPromptPlaceholder')} />
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
226
web/src/pages/agent/form/parser-form/index.tsx
Normal file
226
web/src/pages/agent/form/parser-form/index.tsx
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import {
|
||||||
|
SelectWithSearch,
|
||||||
|
SelectWithSearchFlagOptionType,
|
||||||
|
} from '@/components/originui/select-with-search';
|
||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { BlockButton, Button } from '@/components/ui/button';
|
||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { buildOptions } from '@/utils/form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useHover } from 'ahooks';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
UseFieldArrayRemove,
|
||||||
|
useFieldArray,
|
||||||
|
useForm,
|
||||||
|
useFormContext,
|
||||||
|
} from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
FileType,
|
||||||
|
InitialOutputFormatMap,
|
||||||
|
initialParserValues,
|
||||||
|
} from '../../constant/pipeline';
|
||||||
|
import { useFormValues } from '../../hooks/use-form-values';
|
||||||
|
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
|
import { Output } from '../components/output';
|
||||||
|
import { OutputFormatFormField } from './common-form-fields';
|
||||||
|
import { EmailFormFields } from './email-form-fields';
|
||||||
|
import { ImageFormFields } from './image-form-fields';
|
||||||
|
import { PdfFormFields } from './pdf-form-fields';
|
||||||
|
import { buildFieldNameWithPrefix } from './utils';
|
||||||
|
import { VideoFormFields } from './video-form-fields';
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialParserValues.outputs);
|
||||||
|
|
||||||
|
const FileFormatWidgetMap = {
|
||||||
|
[FileType.PDF]: PdfFormFields,
|
||||||
|
[FileType.Video]: VideoFormFields,
|
||||||
|
[FileType.Audio]: VideoFormFields,
|
||||||
|
[FileType.Email]: EmailFormFields,
|
||||||
|
[FileType.Image]: ImageFormFields,
|
||||||
|
};
|
||||||
|
|
||||||
|
type ParserItemProps = {
|
||||||
|
name: string;
|
||||||
|
index: number;
|
||||||
|
fieldLength: number;
|
||||||
|
remove: UseFieldArrayRemove;
|
||||||
|
fileFormatOptions: SelectWithSearchFlagOptionType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
setups: z.array(
|
||||||
|
z.object({
|
||||||
|
fileFormat: z.string().nullish(),
|
||||||
|
output_format: z.string().optional(),
|
||||||
|
parse_method: z.string().optional(),
|
||||||
|
lang: z.string().optional(),
|
||||||
|
fields: z.array(z.string()).optional(),
|
||||||
|
llm_id: z.string().optional(),
|
||||||
|
system_prompt: z.string().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ParserFormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
function ParserItem({
|
||||||
|
name,
|
||||||
|
index,
|
||||||
|
fieldLength,
|
||||||
|
remove,
|
||||||
|
fileFormatOptions,
|
||||||
|
}: ParserItemProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext<ParserFormSchemaType>();
|
||||||
|
const ref = useRef(null);
|
||||||
|
const isHovering = useHover(ref);
|
||||||
|
|
||||||
|
const prefix = `${name}.${index}`;
|
||||||
|
const fileFormat = form.getValues(`setups.${index}.fileFormat`);
|
||||||
|
|
||||||
|
const values = form.getValues();
|
||||||
|
const parserList = values.setups.slice(); // Adding, deleting, or modifying the parser array will not change the reference.
|
||||||
|
|
||||||
|
const filteredFileFormatOptions = useMemo(() => {
|
||||||
|
const otherFileFormatList = parserList
|
||||||
|
.filter((_, idx) => idx !== index)
|
||||||
|
.map((x) => x.fileFormat);
|
||||||
|
|
||||||
|
return fileFormatOptions.filter((x) => {
|
||||||
|
return !otherFileFormatList.includes(x.value);
|
||||||
|
});
|
||||||
|
}, [fileFormatOptions, index, parserList]);
|
||||||
|
|
||||||
|
const Widget =
|
||||||
|
typeof fileFormat === 'string' && fileFormat in FileFormatWidgetMap
|
||||||
|
? FileFormatWidgetMap[fileFormat as keyof typeof FileFormatWidgetMap]
|
||||||
|
: () => <></>;
|
||||||
|
|
||||||
|
const handleFileTypeChange = useCallback(
|
||||||
|
(value: FileType) => {
|
||||||
|
form.setValue(
|
||||||
|
`setups.${index}.output_format`,
|
||||||
|
InitialOutputFormatMap[value],
|
||||||
|
{ shouldDirty: true, shouldValidate: true, shouldTouch: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[form, index],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn('space-y-5 py-2.5 rounded-md', {
|
||||||
|
'bg-state-error-5': isHovering,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-text-primary text-sm font-medium">
|
||||||
|
Parser {index + 1}
|
||||||
|
</span>
|
||||||
|
{index > 0 && (
|
||||||
|
<Button variant={'ghost'} onClick={() => remove(index)} ref={ref}>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={buildFieldNameWithPrefix(`fileFormat`, prefix)}
|
||||||
|
label={t('dataflow.fileFormats')}
|
||||||
|
>
|
||||||
|
{(field) => (
|
||||||
|
<SelectWithSearch
|
||||||
|
value={field.value}
|
||||||
|
onChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
handleFileTypeChange(val as FileType);
|
||||||
|
}}
|
||||||
|
options={filteredFileFormatOptions}
|
||||||
|
></SelectWithSearch>
|
||||||
|
)}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Widget prefix={prefix} fileType={fileFormat as FileType}></Widget>
|
||||||
|
<div className="hidden">
|
||||||
|
<OutputFormatFormField
|
||||||
|
prefix={prefix}
|
||||||
|
fileType={fileFormat as FileType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{index < fieldLength - 1 && <Separator />}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ParserForm = ({ node }: INextOperatorForm) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const defaultValues = useFormValues(initialParserValues, node);
|
||||||
|
|
||||||
|
const FileFormatOptions = buildOptions(
|
||||||
|
FileType,
|
||||||
|
t,
|
||||||
|
'dataflow.fileFormatOptions',
|
||||||
|
).filter(
|
||||||
|
(x) => x.value !== FileType.Video, // Temporarily hide the video option
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
defaultValues,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
shouldUnregister: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = 'setups';
|
||||||
|
const { fields, remove, append } = useFieldArray({
|
||||||
|
name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
const add = useCallback(() => {
|
||||||
|
append({
|
||||||
|
fileFormat: null,
|
||||||
|
output_format: '',
|
||||||
|
parse_method: '',
|
||||||
|
lang: '',
|
||||||
|
fields: [],
|
||||||
|
llm_id: '',
|
||||||
|
});
|
||||||
|
}, [append]);
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="px-5">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<ParserItem
|
||||||
|
key={field.id}
|
||||||
|
name={name}
|
||||||
|
index={index}
|
||||||
|
fieldLength={fields.length}
|
||||||
|
remove={remove}
|
||||||
|
fileFormatOptions={FileFormatOptions}
|
||||||
|
></ParserItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{fields.length < FileFormatOptions.length && (
|
||||||
|
<BlockButton onClick={add} type="button" className="mt-2.5">
|
||||||
|
{t('dataflow.addParser')}
|
||||||
|
</BlockButton>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
<div className="p-5">
|
||||||
|
<Output list={outputList}></Output>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ParserForm);
|
||||||
3
web/src/pages/agent/form/parser-form/interface.ts
Normal file
3
web/src/pages/agent/form/parser-form/interface.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type CommonProps = {
|
||||||
|
prefix: string;
|
||||||
|
};
|
||||||
44
web/src/pages/agent/form/parser-form/pdf-form-fields.tsx
Normal file
44
web/src/pages/agent/form/parser-form/pdf-form-fields.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
import { LanguageFormField, ParserMethodFormField } from './common-form-fields';
|
||||||
|
import { CommonProps } from './interface';
|
||||||
|
import { useSetInitialLanguage } from './use-set-initial-language';
|
||||||
|
import { buildFieldNameWithPrefix } from './utils';
|
||||||
|
|
||||||
|
export function PdfFormFields({ prefix }: CommonProps) {
|
||||||
|
const form = useFormContext();
|
||||||
|
|
||||||
|
const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix);
|
||||||
|
|
||||||
|
const parseMethod = useWatch({
|
||||||
|
name: parseMethodName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const languageShown = useMemo(() => {
|
||||||
|
return (
|
||||||
|
!isEmpty(parseMethod) &&
|
||||||
|
parseMethod !== ParseDocumentType.DeepDOC &&
|
||||||
|
parseMethod !== ParseDocumentType.PlainText
|
||||||
|
);
|
||||||
|
}, [parseMethod]);
|
||||||
|
|
||||||
|
useSetInitialLanguage({ prefix, languageShown });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmpty(form.getValues(parseMethodName))) {
|
||||||
|
form.setValue(parseMethodName, ParseDocumentType.DeepDOC, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, parseMethodName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ParserMethodFormField prefix={prefix}></ParserMethodFormField>
|
||||||
|
{languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { crossLanguageOptions } from '@/components/cross-language-form-field';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { buildFieldNameWithPrefix } from './utils';
|
||||||
|
|
||||||
|
export function useSetInitialLanguage({
|
||||||
|
prefix,
|
||||||
|
languageShown,
|
||||||
|
}: {
|
||||||
|
prefix: string;
|
||||||
|
languageShown: boolean;
|
||||||
|
}) {
|
||||||
|
const form = useFormContext();
|
||||||
|
const lang = form.getValues(buildFieldNameWithPrefix('lang', prefix));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (languageShown && isEmpty(lang)) {
|
||||||
|
form.setValue(
|
||||||
|
buildFieldNameWithPrefix('lang', prefix),
|
||||||
|
crossLanguageOptions[0].value,
|
||||||
|
{
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [form, lang, languageShown, prefix]);
|
||||||
|
}
|
||||||
3
web/src/pages/agent/form/parser-form/utils.ts
Normal file
3
web/src/pages/agent/form/parser-form/utils.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function buildFieldNameWithPrefix(name: string, prefix: string) {
|
||||||
|
return `${prefix}.${name}`;
|
||||||
|
}
|
||||||
22
web/src/pages/agent/form/parser-form/video-form-fields.tsx
Normal file
22
web/src/pages/agent/form/parser-form/video-form-fields.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { LlmModelType } from '@/constants/knowledge';
|
||||||
|
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
|
||||||
|
import {
|
||||||
|
LargeModelFormField,
|
||||||
|
OutputFormatFormFieldProps,
|
||||||
|
} from './common-form-fields';
|
||||||
|
|
||||||
|
export function VideoFormFields({ prefix }: OutputFormatFormFieldProps) {
|
||||||
|
const modelOptions = useComposeLlmOptionsByModelTypes([
|
||||||
|
LlmModelType.Speech2text,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Multimodal Model */}
|
||||||
|
<LargeModelFormField
|
||||||
|
prefix={prefix}
|
||||||
|
options={modelOptions}
|
||||||
|
></LargeModelFormField>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
web/src/pages/agent/form/splitter-form/index.tsx
Normal file
101
web/src/pages/agent/form/splitter-form/index.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { DelimiterInput } from '@/components/delimiter-form-field';
|
||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||||
|
import { BlockButton, Button } from '@/components/ui/button';
|
||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { initialSplitterValues } from '../../constant/pipeline';
|
||||||
|
import { useFormValues } from '../../hooks/use-form-values';
|
||||||
|
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
|
import { Output } from '../components/output';
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialSplitterValues.outputs);
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
chunk_token_size: z.number(),
|
||||||
|
delimiters: z.array(
|
||||||
|
z.object({
|
||||||
|
value: z.string().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
overlapped_percent: z.number(), // 0.0 - 0.3 , 0% - 30%
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SplitterFormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
const SplitterForm = ({ node }: INextOperatorForm) => {
|
||||||
|
const defaultValues = useFormValues(initialSplitterValues, node);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const form = useForm<SplitterFormSchemaType>({
|
||||||
|
defaultValues,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
});
|
||||||
|
const name = 'delimiters';
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<SliderInputFormField
|
||||||
|
name="chunk_token_size"
|
||||||
|
max={2048}
|
||||||
|
label={t('knowledgeConfiguration.chunkTokenNumber')}
|
||||||
|
></SliderInputFormField>
|
||||||
|
<SliderInputFormField
|
||||||
|
name="overlapped_percent"
|
||||||
|
max={30}
|
||||||
|
min={0}
|
||||||
|
label={t('dataflow.overlappedPercent')}
|
||||||
|
></SliderInputFormField>
|
||||||
|
<section>
|
||||||
|
<span className="mb-2 inline-block">{t('flow.delimiters')}</span>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex items-center gap-2">
|
||||||
|
<div className="space-y-2 flex-1">
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={`${name}.${index}.value`}
|
||||||
|
label="delimiter"
|
||||||
|
labelClassName="!hidden"
|
||||||
|
>
|
||||||
|
<DelimiterInput className="!m-0"></DelimiterInput>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<BlockButton onClick={() => append({ value: '\n' })}>
|
||||||
|
{t('common.add')}
|
||||||
|
</BlockButton>
|
||||||
|
</FormWrapper>
|
||||||
|
<div className="p-5">
|
||||||
|
<Output list={outputList}></Output>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SplitterForm);
|
||||||
91
web/src/pages/agent/form/tokenizer-form/index.tsx
Normal file
91
web/src/pages/agent/form/tokenizer-form/index.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||||
|
import { Form } from '@/components/ui/form';
|
||||||
|
import { MultiSelect } from '@/components/ui/multi-select';
|
||||||
|
import { buildOptions } from '@/utils/form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
initialTokenizerValues,
|
||||||
|
TokenizerFields,
|
||||||
|
TokenizerSearchMethod,
|
||||||
|
} from '../../constant';
|
||||||
|
import { useFormValues } from '../../hooks/use-form-values';
|
||||||
|
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||||
|
import { INextOperatorForm } from '../../interface';
|
||||||
|
import { buildOutputList } from '../../utils/build-output-list';
|
||||||
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
|
import { Output } from '../components/output';
|
||||||
|
|
||||||
|
const outputList = buildOutputList(initialTokenizerValues.outputs);
|
||||||
|
|
||||||
|
export const FormSchema = z.object({
|
||||||
|
search_method: z.array(z.string()).min(1),
|
||||||
|
filename_embd_weight: z.number(),
|
||||||
|
fields: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TokenizerFormSchemaType = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
const TokenizerForm = ({ node }: INextOperatorForm) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const defaultValues = useFormValues(initialTokenizerValues, node);
|
||||||
|
|
||||||
|
const SearchMethodOptions = buildOptions(
|
||||||
|
TokenizerSearchMethod,
|
||||||
|
t,
|
||||||
|
`dataflow.tokenizerSearchMethodOptions`,
|
||||||
|
);
|
||||||
|
const FieldsOptions = buildOptions(
|
||||||
|
TokenizerFields,
|
||||||
|
t,
|
||||||
|
'dataflow.tokenizerFieldsOptions',
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<TokenizerFormSchemaType>({
|
||||||
|
defaultValues,
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<FormWrapper>
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name="search_method"
|
||||||
|
label={t('dataflow.searchMethod')}
|
||||||
|
tooltip={t('dataflow.searchMethodTip')}
|
||||||
|
>
|
||||||
|
{(field) => (
|
||||||
|
<MultiSelect
|
||||||
|
options={SearchMethodOptions}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
variant="inverted"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<SliderInputFormField
|
||||||
|
name="filename_embd_weight"
|
||||||
|
label={t('dataflow.filenameEmbeddingWeight')}
|
||||||
|
max={0.5}
|
||||||
|
step={0.01}
|
||||||
|
></SliderInputFormField>
|
||||||
|
<RAGFlowFormItem name="fields" label={t('dataflow.fields')}>
|
||||||
|
{(field) => <SelectWithSearch options={FieldsOptions} {...field} />}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
</FormWrapper>
|
||||||
|
<div className="p-5">
|
||||||
|
<Output list={outputList}></Output>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TokenizerForm);
|
||||||
@ -23,9 +23,11 @@ import {
|
|||||||
initialDuckValues,
|
initialDuckValues,
|
||||||
initialEmailValues,
|
initialEmailValues,
|
||||||
initialExeSqlValues,
|
initialExeSqlValues,
|
||||||
|
initialExtractorValues,
|
||||||
initialGithubValues,
|
initialGithubValues,
|
||||||
initialGoogleScholarValues,
|
initialGoogleScholarValues,
|
||||||
initialGoogleValues,
|
initialGoogleValues,
|
||||||
|
initialHierarchicalMergerValues,
|
||||||
initialInvokeValues,
|
initialInvokeValues,
|
||||||
initialIterationStartValues,
|
initialIterationStartValues,
|
||||||
initialIterationValues,
|
initialIterationValues,
|
||||||
@ -33,16 +35,19 @@ import {
|
|||||||
initialKeywordExtractValues,
|
initialKeywordExtractValues,
|
||||||
initialMessageValues,
|
initialMessageValues,
|
||||||
initialNoteValues,
|
initialNoteValues,
|
||||||
|
initialParserValues,
|
||||||
initialPubMedValues,
|
initialPubMedValues,
|
||||||
initialQWeatherValues,
|
initialQWeatherValues,
|
||||||
initialRelevantValues,
|
initialRelevantValues,
|
||||||
initialRetrievalValues,
|
initialRetrievalValues,
|
||||||
initialRewriteQuestionValues,
|
initialRewriteQuestionValues,
|
||||||
initialSearXNGValues,
|
initialSearXNGValues,
|
||||||
|
initialSplitterValues,
|
||||||
initialStringTransformValues,
|
initialStringTransformValues,
|
||||||
initialSwitchValues,
|
initialSwitchValues,
|
||||||
initialTavilyExtractValues,
|
initialTavilyExtractValues,
|
||||||
initialTavilyValues,
|
initialTavilyValues,
|
||||||
|
initialTokenizerValues,
|
||||||
initialTuShareValues,
|
initialTuShareValues,
|
||||||
initialUserFillUpValues,
|
initialUserFillUpValues,
|
||||||
initialWaitingDialogueValues,
|
initialWaitingDialogueValues,
|
||||||
@ -115,6 +120,17 @@ export const useInitializeOperatorParams = () => {
|
|||||||
[Operator.StringTransform]: initialStringTransformValues,
|
[Operator.StringTransform]: initialStringTransformValues,
|
||||||
[Operator.TavilyExtract]: initialTavilyExtractValues,
|
[Operator.TavilyExtract]: initialTavilyExtractValues,
|
||||||
[Operator.Placeholder]: {},
|
[Operator.Placeholder]: {},
|
||||||
|
[Operator.File]: {},
|
||||||
|
[Operator.Parser]: initialParserValues,
|
||||||
|
[Operator.Tokenizer]: initialTokenizerValues,
|
||||||
|
[Operator.Splitter]: initialSplitterValues,
|
||||||
|
[Operator.HierarchicalMerger]: initialHierarchicalMergerValues,
|
||||||
|
[Operator.Extractor]: {
|
||||||
|
...initialExtractorValues,
|
||||||
|
llm_id: llmId,
|
||||||
|
sys_prompt: t('dataflow.prompts.system.summary'),
|
||||||
|
prompts: t('dataflow.prompts.user.summary'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}, [llmId]);
|
}, [llmId]);
|
||||||
|
|
||||||
|
|||||||
19
web/src/pages/agent/hooks/use-build-options.tsx
Normal file
19
web/src/pages/agent/hooks/use-build-options.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { buildNodeOutputOptions } from '@/utils/canvas-util';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Operator } from '../constant';
|
||||||
|
import OperatorIcon from '../operator-icon';
|
||||||
|
import useGraphStore from '../store';
|
||||||
|
|
||||||
|
export function useBuildNodeOutputOptions(nodeId?: string) {
|
||||||
|
const nodes = useGraphStore((state) => state.nodes);
|
||||||
|
const edges = useGraphStore((state) => state.edges);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return buildNodeOutputOptions({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
nodeId,
|
||||||
|
Icon: ({ name }) => <OperatorIcon name={name as Operator}></OperatorIcon>,
|
||||||
|
});
|
||||||
|
}, [edges, nodeId, nodes]);
|
||||||
|
}
|
||||||
21
web/src/pages/agent/hooks/use-cancel-dataflow.ts
Normal file
21
web/src/pages/agent/hooks/use-cancel-dataflow.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useCancelDataflow } from '@/hooks/use-agent-request';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export function useCancelCurrentDataflow({
|
||||||
|
messageId,
|
||||||
|
stopFetchTrace,
|
||||||
|
}: {
|
||||||
|
messageId: string;
|
||||||
|
stopFetchTrace(): void;
|
||||||
|
}) {
|
||||||
|
const { cancelDataflow } = useCancelDataflow();
|
||||||
|
|
||||||
|
const handleCancel = useCallback(async () => {
|
||||||
|
const code = await cancelDataflow(messageId);
|
||||||
|
if (code === 0) {
|
||||||
|
stopFetchTrace();
|
||||||
|
}
|
||||||
|
}, [cancelDataflow, messageId, stopFetchTrace]);
|
||||||
|
|
||||||
|
return { handleCancel };
|
||||||
|
}
|
||||||
@ -201,6 +201,7 @@ export const useConnectionDrag = (
|
|||||||
}, [removePlaceholderNode, hideModal, clearActiveDropdown]);
|
}, [removePlaceholderNode, hideModal, clearActiveDropdown]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
nodeId: connectionStartRef.current?.nodeId,
|
||||||
onConnectStart,
|
onConnectStart,
|
||||||
onConnectEnd,
|
onConnectEnd,
|
||||||
handleConnect,
|
handleConnect,
|
||||||
|
|||||||
38
web/src/pages/agent/hooks/use-download-output.ts
Normal file
38
web/src/pages/agent/hooks/use-download-output.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||||
|
import { ITraceData } from '@/interfaces/database/agent';
|
||||||
|
import { downloadJsonFile } from '@/utils/file-util';
|
||||||
|
import { get, isEmpty } from 'lodash';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export function findEndOutput(list?: ITraceData[]) {
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
const trace = list.find((x) => x.component_id === 'END')?.trace;
|
||||||
|
|
||||||
|
const str = get(trace, '0.message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isEmpty(str)) {
|
||||||
|
const json = JSON.parse(str);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEndOutputEmpty(list?: ITraceData[]) {
|
||||||
|
return isEmpty(findEndOutput(list));
|
||||||
|
}
|
||||||
|
export function useDownloadOutput(data?: ITraceData[]) {
|
||||||
|
const { data: agent } = useFetchAgent();
|
||||||
|
|
||||||
|
const handleDownloadJson = useCallback(() => {
|
||||||
|
const output = findEndOutput(data);
|
||||||
|
if (!isEndOutputEmpty(data)) {
|
||||||
|
downloadJsonFile(output, `${agent.title}.json`);
|
||||||
|
}
|
||||||
|
}, [agent.title, data]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleDownloadJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
56
web/src/pages/agent/hooks/use-fetch-pipeline-log.ts
Normal file
56
web/src/pages/agent/hooks/use-fetch-pipeline-log.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
export function useFetchPipelineLog(logSheetVisible: boolean) {
|
||||||
|
const {
|
||||||
|
setMessageId,
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
messageId,
|
||||||
|
setISStopFetchTrace,
|
||||||
|
isStopFetchTrace,
|
||||||
|
} = useFetchMessageTrace();
|
||||||
|
|
||||||
|
const isCompleted = useMemo(() => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
const latest = data?.at(-1);
|
||||||
|
return (
|
||||||
|
latest?.component_id === 'END' && !isEmpty(latest?.trace[0].message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const isLogEmpty = !data || !data.length;
|
||||||
|
|
||||||
|
const stopFetchTrace = useCallback(() => {
|
||||||
|
setISStopFetchTrace(true);
|
||||||
|
}, [setISStopFetchTrace]);
|
||||||
|
|
||||||
|
// cancel request
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCompleted) {
|
||||||
|
stopFetchTrace();
|
||||||
|
}
|
||||||
|
}, [isCompleted, stopFetchTrace]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logSheetVisible) {
|
||||||
|
setISStopFetchTrace(false);
|
||||||
|
}
|
||||||
|
}, [logSheetVisible, setISStopFetchTrace]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
logs: data,
|
||||||
|
isLogEmpty,
|
||||||
|
isCompleted,
|
||||||
|
loading,
|
||||||
|
isParsing: !isLogEmpty && !isCompleted && !isStopFetchTrace,
|
||||||
|
messageId,
|
||||||
|
setMessageId,
|
||||||
|
stopFetchTrace,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseFetchLogReturnType = ReturnType<typeof useFetchPipelineLog>;
|
||||||
10
web/src/pages/agent/hooks/use-is-pipeline.ts
Normal file
10
web/src/pages/agent/hooks/use-is-pipeline.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { AgentCategory, AgentQuery } from '@/constants/agent';
|
||||||
|
import { useSearchParams } from 'umi';
|
||||||
|
|
||||||
|
export function useIsPipeline() {
|
||||||
|
const [queryParameters] = useSearchParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryParameters.get(AgentQuery.Category) === AgentCategory.DataflowCanvas
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import message from '@/components/ui/message';
|
||||||
import { SharedFrom } from '@/constants/chat';
|
import { SharedFrom } from '@/constants/chat';
|
||||||
import { useSetModalState } from '@/hooks/common-hooks';
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||||
@ -31,20 +32,27 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Upload,
|
Upload,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ComponentPropsWithoutRef, useCallback } from 'react';
|
import { ComponentPropsWithoutRef, useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'umi';
|
import { useParams } from 'umi';
|
||||||
import AgentCanvas from './canvas';
|
import AgentCanvas from './canvas';
|
||||||
import { DropdownProvider } from './canvas/context';
|
import { DropdownProvider } from './canvas/context';
|
||||||
|
import { Operator } from './constant';
|
||||||
|
import { PipelineLogContext } from './context';
|
||||||
|
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
|
||||||
import { useHandleExportJsonFile } from './hooks/use-export-json';
|
import { useHandleExportJsonFile } from './hooks/use-export-json';
|
||||||
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
||||||
|
import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log';
|
||||||
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
|
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
|
||||||
|
import { useIsPipeline } from './hooks/use-is-pipeline';
|
||||||
import {
|
import {
|
||||||
useSaveGraph,
|
useSaveGraph,
|
||||||
useSaveGraphBeforeOpeningDebugDrawer,
|
useSaveGraphBeforeOpeningDebugDrawer,
|
||||||
useWatchAgentChange,
|
useWatchAgentChange,
|
||||||
} from './hooks/use-save-graph';
|
} from './hooks/use-save-graph';
|
||||||
|
import { PipelineLogSheet } from './pipeline-log-sheet';
|
||||||
import { SettingDialog } from './setting-dialog';
|
import { SettingDialog } from './setting-dialog';
|
||||||
|
import useGraphStore from './store';
|
||||||
import { useAgentHistoryManager } from './use-agent-history-manager';
|
import { useAgentHistoryManager } from './use-agent-history-manager';
|
||||||
import { VersionDialog } from './version-dialog';
|
import { VersionDialog } from './version-dialog';
|
||||||
|
|
||||||
@ -61,6 +69,7 @@ function AgentDropdownMenuItem({
|
|||||||
|
|
||||||
export default function Agent() {
|
export default function Agent() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
const isPipeline = useIsPipeline();
|
||||||
const { navigateToAgents } = useNavigatePage();
|
const { navigateToAgents } = useNavigatePage();
|
||||||
const {
|
const {
|
||||||
visible: chatDrawerVisible,
|
visible: chatDrawerVisible,
|
||||||
@ -99,6 +108,63 @@ export default function Agent() {
|
|||||||
const { navigateToAgentLogs } = useNavigatePage();
|
const { navigateToAgentLogs } = useNavigatePage();
|
||||||
const time = useWatchAgentChange(chatDrawerVisible);
|
const time = useWatchAgentChange(chatDrawerVisible);
|
||||||
|
|
||||||
|
// pipeline
|
||||||
|
|
||||||
|
const {
|
||||||
|
visible: pipelineLogSheetVisible,
|
||||||
|
showModal: showPipelineLogSheet,
|
||||||
|
hideModal: hidePipelineLogSheet,
|
||||||
|
} = useSetModalState();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isParsing,
|
||||||
|
logs,
|
||||||
|
messageId,
|
||||||
|
setMessageId,
|
||||||
|
isCompleted,
|
||||||
|
stopFetchTrace,
|
||||||
|
isLogEmpty,
|
||||||
|
} = useFetchPipelineLog(pipelineLogSheetVisible);
|
||||||
|
|
||||||
|
const [uploadedFileData, setUploadedFileData] =
|
||||||
|
useState<Record<string, any>>();
|
||||||
|
const findNodeByName = useGraphStore((state) => state.findNodeByName);
|
||||||
|
|
||||||
|
const handleRunPipeline = useCallback(() => {
|
||||||
|
if (!findNodeByName(Operator.Tokenizer)) {
|
||||||
|
message.warning(t('dataflow.tokenizerRequired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isParsing) {
|
||||||
|
// show log sheet
|
||||||
|
showPipelineLogSheet();
|
||||||
|
} else {
|
||||||
|
hidePipelineLogSheet();
|
||||||
|
handleRun();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
findNodeByName,
|
||||||
|
handleRun,
|
||||||
|
hidePipelineLogSheet,
|
||||||
|
isParsing,
|
||||||
|
showPipelineLogSheet,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { handleCancel } = useCancelCurrentDataflow({
|
||||||
|
messageId,
|
||||||
|
stopFetchTrace,
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = useCallback(() => {
|
||||||
|
if (isPipeline) {
|
||||||
|
handleRunPipeline();
|
||||||
|
} else {
|
||||||
|
handleRunAgent();
|
||||||
|
}
|
||||||
|
}, [handleRunAgent, handleRunPipeline, isPipeline]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="h-full">
|
<section className="h-full">
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
@ -128,7 +194,7 @@ export default function Agent() {
|
|||||||
>
|
>
|
||||||
<LaptopMinimalCheck /> {t('flow.save')}
|
<LaptopMinimalCheck /> {t('flow.save')}
|
||||||
</ButtonLoading>
|
</ButtonLoading>
|
||||||
<Button variant={'secondary'} onClick={handleRunAgent}>
|
<Button variant={'secondary'} onClick={run}>
|
||||||
<CirclePlay />
|
<CirclePlay />
|
||||||
{t('flow.run')}
|
{t('flow.run')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -136,6 +202,7 @@ export default function Agent() {
|
|||||||
<History />
|
<History />
|
||||||
{t('flow.historyversion')}
|
{t('flow.historyversion')}
|
||||||
</Button>
|
</Button>
|
||||||
|
{isPipeline || (
|
||||||
<Button
|
<Button
|
||||||
variant={'secondary'}
|
variant={'secondary'}
|
||||||
onClick={navigateToAgentLogs(id as string)}
|
onClick={navigateToAgentLogs(id as string)}
|
||||||
@ -143,7 +210,7 @@ export default function Agent() {
|
|||||||
<Logs />
|
<Logs />
|
||||||
{t('flow.log')}
|
{t('flow.log')}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant={'secondary'}>
|
<Button variant={'secondary'}>
|
||||||
@ -160,7 +227,8 @@ export default function Agent() {
|
|||||||
<Settings />
|
<Settings />
|
||||||
{t('flow.setting')}
|
{t('flow.setting')}
|
||||||
</AgentDropdownMenuItem>
|
</AgentDropdownMenuItem>
|
||||||
{location.hostname !== 'demo.ragflow.io' && (
|
{isPipeline ||
|
||||||
|
(location.hostname !== 'demo.ragflow.io' && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<AgentDropdownMenuItem onClick={showEmbedModal}>
|
<AgentDropdownMenuItem onClick={showEmbedModal}>
|
||||||
@ -168,11 +236,14 @@ export default function Agent() {
|
|||||||
{t('common.embedIntoSite')}
|
{t('common.embedIntoSite')}
|
||||||
</AgentDropdownMenuItem>
|
</AgentDropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
<PipelineLogContext.Provider
|
||||||
|
value={{ messageId, setMessageId, setUploadedFileData }}
|
||||||
|
>
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<DropdownProvider>
|
<DropdownProvider>
|
||||||
<AgentCanvas
|
<AgentCanvas
|
||||||
@ -181,6 +252,7 @@ export default function Agent() {
|
|||||||
></AgentCanvas>
|
></AgentCanvas>
|
||||||
</DropdownProvider>
|
</DropdownProvider>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
|
</PipelineLogContext.Provider>
|
||||||
{embedVisible && (
|
{embedVisible && (
|
||||||
<EmbedDialog
|
<EmbedDialog
|
||||||
visible={embedVisible}
|
visible={embedVisible}
|
||||||
@ -199,6 +271,19 @@ export default function Agent() {
|
|||||||
{settingDialogVisible && (
|
{settingDialogVisible && (
|
||||||
<SettingDialog hideModal={hideSettingDialog}></SettingDialog>
|
<SettingDialog hideModal={hideSettingDialog}></SettingDialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{pipelineLogSheetVisible && (
|
||||||
|
<PipelineLogSheet
|
||||||
|
hideModal={hidePipelineLogSheet}
|
||||||
|
isParsing={isParsing}
|
||||||
|
isCompleted={isCompleted}
|
||||||
|
isLogEmpty={isLogEmpty}
|
||||||
|
logs={logs}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
messageId={messageId}
|
||||||
|
uploadedFileData={uploadedFileData}
|
||||||
|
></PipelineLogSheet>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
137
web/src/pages/agent/pipeline-log-sheet/dataflow-timeline.tsx
Normal file
137
web/src/pages/agent/pipeline-log-sheet/dataflow-timeline.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import {
|
||||||
|
Timeline,
|
||||||
|
TimelineContent,
|
||||||
|
TimelineHeader,
|
||||||
|
TimelineIndicator,
|
||||||
|
TimelineItem,
|
||||||
|
TimelineSeparator,
|
||||||
|
TimelineTitle,
|
||||||
|
} from '@/components/originui/timeline';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { ITraceData } from '@/interfaces/database/agent';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { File } from 'lucide-react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Operator } from '../constant';
|
||||||
|
import OperatorIcon from '../operator-icon';
|
||||||
|
import useGraphStore from '../store';
|
||||||
|
|
||||||
|
export type DataflowTimelineProps = {
|
||||||
|
traceList?: ITraceData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const END = 'END';
|
||||||
|
|
||||||
|
interface DataflowTrace {
|
||||||
|
datetime: string;
|
||||||
|
elapsed_time: number;
|
||||||
|
message: string;
|
||||||
|
progress: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
export function DataflowTimeline({ traceList }: DataflowTimelineProps) {
|
||||||
|
const getNode = useGraphStore((state) => state.getNode);
|
||||||
|
|
||||||
|
const getNodeData = useCallback(
|
||||||
|
(componentId: string) => {
|
||||||
|
return getNode(componentId)?.data;
|
||||||
|
},
|
||||||
|
[getNode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getNodeLabel = useCallback(
|
||||||
|
(componentId: string) => {
|
||||||
|
return getNodeData(componentId)?.label as Operator;
|
||||||
|
},
|
||||||
|
[getNodeData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Timeline>
|
||||||
|
{Array.isArray(traceList) &&
|
||||||
|
traceList?.map((item, index) => {
|
||||||
|
const traces = item.trace as DataflowTrace[];
|
||||||
|
const nodeLabel = getNodeLabel(item.component_id);
|
||||||
|
|
||||||
|
const latest = traces[traces.length - 1];
|
||||||
|
const progress = latest.progress * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimelineItem
|
||||||
|
key={item.component_id}
|
||||||
|
step={index}
|
||||||
|
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8 pb-6"
|
||||||
|
>
|
||||||
|
<TimelineHeader>
|
||||||
|
<TimelineSeparator className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-7 bg-accent-primary" />
|
||||||
|
<TimelineTitle className="">
|
||||||
|
<TimelineContent
|
||||||
|
className={cn(
|
||||||
|
'text-foreground rounded-lg border px-4 py-3',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<section className="flex items-center justify-between mb-2">
|
||||||
|
<span className="flex-1 truncate">
|
||||||
|
{getNodeData(item.component_id)?.name || END}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 flex items-center gap-5">
|
||||||
|
<Progress value={progress} className="h-1 flex-1" />
|
||||||
|
<span className="text-accent-primary text-xs">
|
||||||
|
{progress.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div className="divide-y space-y-1">
|
||||||
|
{traces
|
||||||
|
.filter((x) => !isEmpty(x.message))
|
||||||
|
.map((x, idx) => (
|
||||||
|
<section
|
||||||
|
key={idx}
|
||||||
|
className="text-text-secondary text-xs space-x-2 py-2.5 !m-0"
|
||||||
|
>
|
||||||
|
<span>{x.datetime}</span>
|
||||||
|
{item.component_id !== 'END' && (
|
||||||
|
<span
|
||||||
|
className={cn({
|
||||||
|
'text-state-error':
|
||||||
|
x.message.startsWith('[ERROR]'),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{x.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{x.elapsed_time.toString().slice(0, 6)}s
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TimelineContent>
|
||||||
|
</TimelineTitle>
|
||||||
|
<TimelineIndicator
|
||||||
|
className={cn(
|
||||||
|
'border border-accent-primary group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-5 items-center justify-center group-data-[orientation=vertical]/timeline:-left-7',
|
||||||
|
{
|
||||||
|
'rounded bg-accent-primary': nodeLabel === Operator.Begin,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.component_id === END ? (
|
||||||
|
<span className="rounded-full inline-block size-2 bg-accent-primary"></span>
|
||||||
|
) : nodeLabel === Operator.Begin ? (
|
||||||
|
<File className="size-3.5 text-bg-base"></File>
|
||||||
|
) : (
|
||||||
|
<OperatorIcon
|
||||||
|
name={nodeLabel}
|
||||||
|
className="size-3.5 rounded-full"
|
||||||
|
></OperatorIcon>
|
||||||
|
)}
|
||||||
|
</TimelineIndicator>
|
||||||
|
</TimelineHeader>
|
||||||
|
</TimelineItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Timeline>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
web/src/pages/agent/pipeline-log-sheet/index.tsx
Normal file
114
web/src/pages/agent/pipeline-log-sheet/index.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { SkeletonCard } from '@/components/skeleton-card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from '@/components/ui/sheet';
|
||||||
|
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||||
|
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { PipelineResultSearchParams } from '@/pages/dataflow-result/constant';
|
||||||
|
import {
|
||||||
|
ArrowUpRight,
|
||||||
|
CirclePause,
|
||||||
|
Logs,
|
||||||
|
SquareArrowOutUpRight,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import 'react18-json-view/src/style.css';
|
||||||
|
import { useParams } from 'umi';
|
||||||
|
import {
|
||||||
|
isEndOutputEmpty,
|
||||||
|
useDownloadOutput,
|
||||||
|
} from '../hooks/use-download-output';
|
||||||
|
import { UseFetchLogReturnType } from '../hooks/use-fetch-pipeline-log';
|
||||||
|
import { DataflowTimeline } from './dataflow-timeline';
|
||||||
|
|
||||||
|
type LogSheetProps = IModalProps<any> & {
|
||||||
|
handleCancel(): void;
|
||||||
|
uploadedFileData?: Record<string, any>;
|
||||||
|
} & Pick<
|
||||||
|
UseFetchLogReturnType,
|
||||||
|
'isCompleted' | 'isLogEmpty' | 'isParsing' | 'logs' | 'messageId'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function PipelineLogSheet({
|
||||||
|
hideModal,
|
||||||
|
isParsing,
|
||||||
|
logs,
|
||||||
|
handleCancel,
|
||||||
|
isCompleted,
|
||||||
|
isLogEmpty,
|
||||||
|
messageId,
|
||||||
|
uploadedFileData,
|
||||||
|
}: LogSheetProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { id } = useParams();
|
||||||
|
const { data: agent } = useFetchAgent();
|
||||||
|
|
||||||
|
const { handleDownloadJson } = useDownloadOutput(logs);
|
||||||
|
const { navigateToDataflowResult } = useNavigatePage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open onOpenChange={hideModal} modal={false}>
|
||||||
|
<SheetContent
|
||||||
|
className={cn('top-20 h-auto flex flex-col p-0 gap-0')}
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<SheetHeader className="p-5">
|
||||||
|
<SheetTitle className="flex items-center gap-2.5">
|
||||||
|
<Logs className="size-4" /> {t('flow.log')}
|
||||||
|
{isCompleted && (
|
||||||
|
<Button
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={navigateToDataflowResult({
|
||||||
|
id: messageId, // 'log_id',
|
||||||
|
[PipelineResultSearchParams.AgentId]: id, // 'agent_id',
|
||||||
|
[PipelineResultSearchParams.DocumentId]: uploadedFileData?.id, //'doc_id',
|
||||||
|
[PipelineResultSearchParams.AgentTitle]: agent.title, //'title',
|
||||||
|
[PipelineResultSearchParams.IsReadOnly]: 'true',
|
||||||
|
[PipelineResultSearchParams.Type]: 'dataflow',
|
||||||
|
[PipelineResultSearchParams.CreatedBy]:
|
||||||
|
uploadedFileData?.created_by,
|
||||||
|
[PipelineResultSearchParams.DocumentExtension]:
|
||||||
|
uploadedFileData?.extension,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('dataflow.viewResult')} <ArrowUpRight />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<section className="flex-1 overflow-auto px-5 pt-5">
|
||||||
|
{isLogEmpty ? (
|
||||||
|
<SkeletonCard className="mt-2" />
|
||||||
|
) : (
|
||||||
|
<DataflowTimeline traceList={logs}></DataflowTimeline>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<div className="px-5 pb-5">
|
||||||
|
{isParsing ? (
|
||||||
|
<Button
|
||||||
|
className="w-full mt-8 bg-state-error/10 text-state-error hover:bg-state-error hover:text-bg-base"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
<CirclePause /> {t('dataflow.cancel')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleDownloadJson}
|
||||||
|
disabled={isEndOutputEmpty(logs)}
|
||||||
|
className="w-full mt-8 bg-accent-primary-5 text-text-secondary hover:bg-accent-primary-5 hover:text-accent-primary hover:border-accent-primary hover:border"
|
||||||
|
>
|
||||||
|
<SquareArrowOutUpRight />
|
||||||
|
{t('dataflow.exportJson')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import pipe from 'lodash/fp/pipe';
|
|||||||
import isObject from 'lodash/isObject';
|
import isObject from 'lodash/isObject';
|
||||||
import {
|
import {
|
||||||
CategorizeAnchorPointPositions,
|
CategorizeAnchorPointPositions,
|
||||||
|
NoCopyOperatorsList,
|
||||||
NoDebugOperatorsList,
|
NoDebugOperatorsList,
|
||||||
NodeHandleId,
|
NodeHandleId,
|
||||||
Operator,
|
Operator,
|
||||||
@ -403,6 +404,10 @@ export const needsSingleStepDebugging = (label: string) => {
|
|||||||
return !NoDebugOperatorsList.some((x) => (label as Operator) === x);
|
return !NoDebugOperatorsList.some((x) => (label as Operator) === x);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function showCopyIcon(label: string) {
|
||||||
|
return !NoCopyOperatorsList.some((x) => (label as Operator) === x);
|
||||||
|
}
|
||||||
|
|
||||||
// Get the coordinates of the node relative to the Iteration node
|
// Get the coordinates of the node relative to the Iteration node
|
||||||
export function getRelativePositionToIterationNode(
|
export function getRelativePositionToIterationNode(
|
||||||
nodes: RAGFlowNodeType[],
|
nodes: RAGFlowNodeType[],
|
||||||
@ -464,7 +469,9 @@ export function convertToStringArray(
|
|||||||
return list.map((x) => x.value);
|
return list.map((x) => x.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertToObjectArray(list: Array<string | number | boolean>) {
|
export function convertToObjectArray<T extends string | number | boolean>(
|
||||||
|
list: Array<T>,
|
||||||
|
) {
|
||||||
if (!Array.isArray(list)) {
|
if (!Array.isArray(list)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -488,7 +495,7 @@ export const buildCategorizeListFromObject = (
|
|||||||
// Categorize's to field has two data sources, with edges as the data source.
|
// Categorize's to field has two data sources, with edges as the data source.
|
||||||
// Changes in the edge or to field need to be synchronized to the form field.
|
// Changes in the edge or to field need to be synchronized to the form field.
|
||||||
return Object.keys(categorizeItem)
|
return Object.keys(categorizeItem)
|
||||||
.reduce<Array<ICategorizeItem>>((pre, cur) => {
|
.reduce<Array<Omit<ICategorizeItem, 'uuid'>>>((pre, cur) => {
|
||||||
// synchronize edge data to the to field
|
// synchronize edge data to the to field
|
||||||
|
|
||||||
pre.push({
|
pre.push({
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export type DatasetCardProps = {
|
|||||||
} & Pick<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'>;
|
} & Pick<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'>;
|
||||||
|
|
||||||
export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
|
export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
|
||||||
const { navigateToAgent, navigateToDataflow } = useNavigatePage();
|
const { navigateToAgent } = useNavigatePage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HomeCard
|
<HomeCard
|
||||||
@ -26,9 +26,10 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
|
|||||||
}
|
}
|
||||||
sharedBadge={<SharedBadge>{data.nickname}</SharedBadge>}
|
sharedBadge={<SharedBadge>{data.nickname}</SharedBadge>}
|
||||||
onClick={
|
onClick={
|
||||||
data.canvas_category === AgentCategory.DataflowCanvas
|
// data.canvas_category === AgentCategory.DataflowCanvas
|
||||||
? navigateToDataflow(data.id)
|
// ? navigateToDataflow(data.id)
|
||||||
: navigateToAgent(data?.id)
|
// :
|
||||||
|
navigateToAgent(data?.id, data.canvas_category as AgentCategory)
|
||||||
}
|
}
|
||||||
icon={
|
icon={
|
||||||
data.canvas_category === AgentCategory.DataflowCanvas && (
|
data.canvas_category === AgentCategory.DataflowCanvas && (
|
||||||
|
|||||||
@ -1,26 +1,16 @@
|
|||||||
import { IBeginNode } from '@/interfaces/database/flow';
|
import { IBeginNode } from '@/interfaces/database/flow';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { NodeProps, Position } from '@xyflow/react';
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
import get from 'lodash/get';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import { NodeHandleId, Operator } from '../../constant';
|
||||||
BeginQueryType,
|
|
||||||
BeginQueryTypeIconMap,
|
|
||||||
NodeHandleId,
|
|
||||||
Operator,
|
|
||||||
} from '../../constant';
|
|
||||||
import { BeginQuery } from '../../interface';
|
|
||||||
import OperatorIcon from '../../operator-icon';
|
import OperatorIcon from '../../operator-icon';
|
||||||
import { CommonHandle } from './handle';
|
import { CommonHandle } from './handle';
|
||||||
import { RightHandleStyle } from './handle-icon';
|
import { RightHandleStyle } from './handle-icon';
|
||||||
import styles from './index.less';
|
|
||||||
import { NodeWrapper } from './node-wrapper';
|
import { NodeWrapper } from './node-wrapper';
|
||||||
|
|
||||||
// TODO: do not allow other nodes to connect to this node
|
// TODO: do not allow other nodes to connect to this node
|
||||||
function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
|
function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeWrapper selected={selected}>
|
<NodeWrapper selected={selected}>
|
||||||
@ -39,22 +29,6 @@ function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
|
|||||||
{t(`dataflow.begin`)}
|
{t(`dataflow.begin`)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className={cn(styles.generateParameters, 'flex gap-2 flex-col')}>
|
|
||||||
{Object.entries(inputs).map(([key, val], idx) => {
|
|
||||||
const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType];
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className={cn(styles.conditionBlock, 'flex gap-1.5 items-center')}
|
|
||||||
>
|
|
||||||
<Icon className="size-4" />
|
|
||||||
<label htmlFor="">{key}</label>
|
|
||||||
<span className={styles.parameterValue}>{val.name}</span>
|
|
||||||
<span className="flex-1">{val.optional ? 'Yes' : 'No'}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
</NodeWrapper>
|
</NodeWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,24 +3,8 @@ import {
|
|||||||
initialLlmBaseValues,
|
initialLlmBaseValues,
|
||||||
DataflowOperator as Operator,
|
DataflowOperator as Operator,
|
||||||
} from '@/constants/agent';
|
} from '@/constants/agent';
|
||||||
import {
|
|
||||||
ChatVariableEnabledField,
|
|
||||||
variableEnabledFieldMap,
|
|
||||||
} from '@/constants/chat';
|
|
||||||
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
|
|
||||||
export { DataflowOperator as Operator } from '@/constants/agent';
|
export { DataflowOperator as Operator } from '@/constants/agent';
|
||||||
|
|
||||||
import {
|
|
||||||
Circle,
|
|
||||||
CircleSlash2,
|
|
||||||
CloudUpload,
|
|
||||||
ListOrdered,
|
|
||||||
OptionIcon,
|
|
||||||
TextCursorInput,
|
|
||||||
ToggleLeft,
|
|
||||||
WrapText,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
export enum FileType {
|
export enum FileType {
|
||||||
PDF = 'pdf',
|
PDF = 'pdf',
|
||||||
Spreadsheet = 'spreadsheet',
|
Spreadsheet = 'spreadsheet',
|
||||||
@ -104,47 +88,8 @@ export enum ContextGeneratorFieldName {
|
|||||||
Metadata = 'metadata',
|
Metadata = 'metadata',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PromptRole {
|
|
||||||
User = 'user',
|
|
||||||
Assistant = 'assistant',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AgentDialogueMode {
|
|
||||||
Conversational = 'conversational',
|
|
||||||
Task = 'task',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BeginId = 'File';
|
export const BeginId = 'File';
|
||||||
|
|
||||||
export const SwitchLogicOperatorOptions = ['and', 'or'];
|
|
||||||
|
|
||||||
export const CommonOperatorList = Object.values(Operator).filter(
|
|
||||||
(x) => x !== Operator.Note,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SwitchOperatorOptions = [
|
|
||||||
{ value: '=', label: 'equal', icon: 'equal' },
|
|
||||||
{ value: '≠', label: 'notEqual', icon: 'not-equals' },
|
|
||||||
{ value: '>', label: 'gt', icon: 'Less' },
|
|
||||||
{ value: '≥', label: 'ge', icon: 'Greater-or-equal' },
|
|
||||||
{ value: '<', label: 'lt', icon: 'Less' },
|
|
||||||
{ value: '≤', label: 'le', icon: 'less-or-equal' },
|
|
||||||
{ value: 'contains', label: 'contains', icon: 'Contains' },
|
|
||||||
{ value: 'not contains', label: 'notContains', icon: 'not-contains' },
|
|
||||||
{ value: 'start with', label: 'startWith', icon: 'list-start' },
|
|
||||||
{ value: 'end with', label: 'endWith', icon: 'list-end' },
|
|
||||||
{
|
|
||||||
value: 'empty',
|
|
||||||
label: 'empty',
|
|
||||||
icon: <Circle className="size-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'not empty',
|
|
||||||
label: 'notEmpty',
|
|
||||||
icon: <CircleSlash2 className="size-4" />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const SwitchElseTo = 'end_cpn_ids';
|
export const SwitchElseTo = 'end_cpn_ids';
|
||||||
|
|
||||||
export enum TokenizerSearchMethod {
|
export enum TokenizerSearchMethod {
|
||||||
@ -186,15 +131,6 @@ export const initialBeginValues = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const variableCheckBoxFieldMap = Object.keys(
|
|
||||||
variableEnabledFieldMap,
|
|
||||||
).reduce<Record<string, boolean>>((pre, cur) => {
|
|
||||||
pre[cur] = setInitialChatVariableEnabledFieldValue(
|
|
||||||
cur as ChatVariableEnabledField,
|
|
||||||
);
|
|
||||||
return pre;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
export const initialNoteValues = {
|
export const initialNoteValues = {
|
||||||
text: '',
|
text: '',
|
||||||
};
|
};
|
||||||
@ -341,24 +277,6 @@ export const NodeMap = {
|
|||||||
[Operator.Extractor]: 'contextNode',
|
[Operator.Extractor]: 'contextNode',
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum BeginQueryType {
|
|
||||||
Line = 'line',
|
|
||||||
Paragraph = 'paragraph',
|
|
||||||
Options = 'options',
|
|
||||||
File = 'file',
|
|
||||||
Integer = 'integer',
|
|
||||||
Boolean = 'boolean',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BeginQueryTypeIconMap = {
|
|
||||||
[BeginQueryType.Line]: TextCursorInput,
|
|
||||||
[BeginQueryType.Paragraph]: WrapText,
|
|
||||||
[BeginQueryType.Options]: OptionIcon,
|
|
||||||
[BeginQueryType.File]: CloudUpload,
|
|
||||||
[BeginQueryType.Integer]: ListOrdered,
|
|
||||||
[BeginQueryType.Boolean]: ToggleLeft,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NoDebugOperatorsList = [Operator.Begin];
|
export const NoDebugOperatorsList = [Operator.Begin];
|
||||||
|
|
||||||
export enum NodeHandleId {
|
export enum NodeHandleId {
|
||||||
@ -370,17 +288,6 @@ export enum NodeHandleId {
|
|||||||
AgentException = 'agentException',
|
AgentException = 'agentException',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum VariableType {
|
|
||||||
String = 'string',
|
|
||||||
Array = 'array',
|
|
||||||
File = 'file',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AgentExceptionMethod {
|
|
||||||
Comment = 'comment',
|
|
||||||
Goto = 'goto',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileTypeSuffixMap = {
|
export const FileTypeSuffixMap = {
|
||||||
[FileType.PDF]: ['pdf'],
|
[FileType.PDF]: ['pdf'],
|
||||||
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],
|
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],
|
||||||
|
|||||||
Reference in New Issue
Block a user