From 810f9eaaad28e44018d836f9e1b3c17b22f5b30b Mon Sep 17 00:00:00 2001 From: zhsama Date: Wed, 14 Jan 2026 23:23:09 +0800 Subject: [PATCH] feat: Enhance sub-graph components with context handling and variable management --- .../sub-graph/components/config-panel.tsx | 86 +++--- web/app/components/sub-graph/index.tsx | 33 ++- web/app/components/sub-graph/store/index.ts | 5 +- web/app/components/sub-graph/types.ts | 4 + .../variable/var-reference-picker.tsx | 3 + .../variable/var-reference-popup.tsx | 5 +- .../llm/components/config-context-item.tsx | 129 +++++++++ .../nodes/llm/components/config-prompt.tsx | 259 +++++++++++++++--- .../components/workflow/nodes/llm/panel.tsx | 9 +- .../components/workflow/nodes/llm/types.ts | 4 +- .../workflow/nodes/llm/use-config.ts | 4 +- .../tool/components/sub-graph-modal/index.tsx | 43 ++- .../sub-graph-modal/sub-graph-canvas.tsx | 4 + .../tool/components/sub-graph-modal/types.ts | 4 +- web/app/components/workflow/types.ts | 11 + web/i18n/en-US/workflow.json | 7 +- web/i18n/ja-JP/workflow.json | 7 +- web/i18n/zh-Hans/workflow.json | 7 +- web/i18n/zh-Hant/workflow.json | 7 +- 19 files changed, 528 insertions(+), 103 deletions(-) create mode 100644 web/app/components/workflow/nodes/llm/components/config-context-item.tsx diff --git a/web/app/components/sub-graph/components/config-panel.tsx b/web/app/components/sub-graph/components/config-panel.tsx index 0b8674ffe4..edbd746550 100644 --- a/web/app/components/sub-graph/components/config-panel.tsx +++ b/web/app/components/sub-graph/components/config-panel.tsx @@ -67,6 +67,9 @@ const ConfigPanel: FC = ({ description: t('subGraphModal.whenOutputNone.defaultDesc', { ns: 'workflow' }), }, ]), [t]) + const selectedWhenOutputNoneOption = useMemo(() => ( + whenOutputNoneOptions.find(item => item.value === mentionConfig.null_strategy) ?? whenOutputNoneOptions[0] + ), [mentionConfig.null_strategy, whenOutputNoneOptions]) const handleNullStrategyChange = useCallback((item: Item) => { if (typeof item.value !== 'string') @@ -94,6 +97,8 @@ const ConfigPanel: FC = ({ default_value: nextValue, }) }, [mentionConfig, onMentionConfigChange]) + const defaultValue = mentionConfig.default_value ?? '' + const shouldFormatDefaultValue = typeof defaultValue !== 'string' return (
@@ -131,45 +136,54 @@ const ConfigPanel: FC = ({
- - ( -
-
- {selected && ( - - )} -
-
-
{item.name}
-
{item.description}
-
-
- )} - /> -
- {mentionConfig.null_strategy === 'use_default' && ( -
-
- {t('subGraphModal.defaultValueHint', { ns: 'workflow' })} -
-
- + ( +
+
+ {selected && ( + + )} +
+
+
{item.name}
+
{item.description}
+
+
+ )} />
+ )} + > +
+ {selectedWhenOutputNoneOption?.description && ( +
+ {selectedWhenOutputNoneOption.description} +
+ )} + {mentionConfig.null_strategy === 'use_default' && ( +
+ +
+ )}
- )} +
)} diff --git a/web/app/components/sub-graph/index.tsx b/web/app/components/sub-graph/index.tsx index 6a4aef5537..a7d37a880e 100644 --- a/web/app/components/sub-graph/index.tsx +++ b/web/app/components/sub-graph/index.tsx @@ -2,12 +2,13 @@ import type { FC } from 'react' import type { Viewport } from 'reactflow' import type { SubGraphProps } from './types' import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store' -import type { PromptItem } from '@/app/components/workflow/types' -import { memo, useMemo } from 'react' +import type { PromptItem, PromptTemplateItem } from '@/app/components/workflow/types' +import { memo, useEffect, useMemo } from 'react' import WorkflowWithDefaultContext from '@/app/components/workflow' import { NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION } from '@/app/components/workflow/constants' import { WorkflowContextProvider } from '@/app/components/workflow/context' -import { BlockEnum, EditionType, PromptRole } from '@/app/components/workflow/types' +import { useStore } from '@/app/components/workflow/store' +import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types' import SubGraphMain from './components/sub-graph-main' import { useSubGraphNodes } from './hooks' import { createSubGraphSlice } from './store' @@ -38,9 +39,19 @@ const SubGraphContent: FC = (props) => { onMentionConfigChange, extractorNode, toolParamValue, + parentAvailableNodes, + parentAvailableVars, onSave, } = props + const setParentAvailableVars = useStore(state => state.setParentAvailableVars) + const setParentAvailableNodes = useStore(state => state.setParentAvailableNodes) + + useEffect(() => { + setParentAvailableVars?.(parentAvailableVars || []) + setParentAvailableNodes?.(parentAvailableNodes || []) + }, [parentAvailableNodes, parentAvailableVars, setParentAvailableNodes, setParentAvailableVars]) + const promptText = useMemo(() => { if (!toolParamValue) return '' @@ -95,16 +106,18 @@ const SubGraphContent: FC = (props) => { if (!Array.isArray(template)) return applyPromptText(template as PromptItem) - const userIndex = template.findIndex(item => item.role === PromptRole.user) + const promptItems = template.filter((item): item is PromptItem => !isPromptMessageContext(item)) + + const userIndex = promptItems.findIndex(item => item.role === PromptRole.user) if (userIndex >= 0) { - return template.map((item, index) => { + return promptItems.map((item, index) => { if (index !== userIndex) return item return applyPromptText(item) - }) + }) as PromptTemplateItem[] } - const useJinja = template.some((item: PromptItem) => item.edition_type === EditionType.jinja2) + const useJinja = promptItems.some((item: PromptItem) => item.edition_type === EditionType.jinja2) const defaultUserPrompt: PromptItem = useJinja ? { role: PromptRole.user, @@ -113,13 +126,13 @@ const SubGraphContent: FC = (props) => { edition_type: EditionType.jinja2, } : { role: PromptRole.user, text: promptText } - const systemIndex = template.findIndex(item => item.role === PromptRole.system) - const nextTemplate = [...template] + const systemIndex = promptItems.findIndex(item => item.role === PromptRole.system) + const nextTemplate = [...promptItems] if (systemIndex >= 0) nextTemplate.splice(systemIndex + 1, 0, defaultUserPrompt) else nextTemplate.unshift(defaultUserPrompt) - return nextTemplate + return nextTemplate as PromptTemplateItem[] })() return { diff --git a/web/app/components/sub-graph/store/index.ts b/web/app/components/sub-graph/store/index.ts index 17f6bde319..3b314be72a 100644 --- a/web/app/components/sub-graph/store/index.ts +++ b/web/app/components/sub-graph/store/index.ts @@ -1,6 +1,6 @@ import type { CreateSubGraphSlice, SubGraphSliceShape } from '../types' -const initialState: Omit = { +const initialState: Omit = { parentToolNodeId: '', parameterKey: '', sourceAgentNodeId: '', @@ -18,6 +18,7 @@ const initialState: Omit ({ @@ -46,5 +47,7 @@ export const createSubGraphSlice: CreateSubGraphSlice = set => ({ setParentAvailableVars: vars => set(() => ({ parentAvailableVars: vars })), + setParentAvailableNodes: nodes => set(() => ({ parentAvailableNodes: nodes })), + resetSubGraph: () => set(() => ({ ...initialState })), }) diff --git a/web/app/components/sub-graph/types.ts b/web/app/components/sub-graph/types.ts index 587ed9e451..936ac87cde 100644 --- a/web/app/components/sub-graph/types.ts +++ b/web/app/components/sub-graph/types.ts @@ -31,6 +31,8 @@ export type SubGraphProps = { onMentionConfigChange: (config: MentionConfig) => void extractorNode?: Node toolParamValue?: string + parentAvailableNodes?: Node[] + parentAvailableVars?: NodeOutPutVar[] onSave?: (nodes: Node[], edges: Edge[]) => void } @@ -52,6 +54,7 @@ export type SubGraphSliceShape = { isRunning: boolean parentAvailableVars: NodeOutPutVar[] + parentAvailableNodes: Node[] setSubGraphContext: (context: { parentToolNodeId: string @@ -67,6 +70,7 @@ export type SubGraphSliceShape = { setShowDebugPanel: (show: boolean) => void setIsRunning: (running: boolean) => void setParentAvailableVars: (vars: NodeOutPutVar[]) => void + setParentAvailableNodes: (nodes: Node[]) => void resetSubGraph: () => void } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 6dfcbaf4d8..bca8b79c14 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -84,6 +84,7 @@ type Props = { currentTool?: Tool currentProvider?: ToolWithProvider | TriggerWithProvider preferSchemaType?: boolean + hideSearch?: boolean } const DEFAULT_VALUE_SELECTOR: Props['value'] = [] @@ -117,6 +118,7 @@ const VarReferencePicker: FC = ({ currentTool, currentProvider, preferSchemaType, + hideSearch, }) => { const { t } = useTranslation() const store = useStoreApi() @@ -636,6 +638,7 @@ const VarReferencePicker: FC = ({ isSupportFileVar={isSupportFileVar} zIndex={zIndex} preferSchemaType={preferSchemaType} + hideSearch={hideSearch} /> )} diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx index 6184bcad9f..561016132b 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx @@ -15,6 +15,7 @@ type Props = { onChange: (value: ValueSelector, varDetail: Var) => void itemWidth?: number isSupportFileVar?: boolean + hideSearch?: boolean zIndex?: number preferSchemaType?: boolean } @@ -24,6 +25,7 @@ const VarReferencePopup: FC = ({ onChange, itemWidth, isSupportFileVar = true, + hideSearch, zIndex, preferSchemaType, }) => { @@ -35,7 +37,7 @@ const VarReferencePopup: FC = ({ // max-h-[300px] overflow-y-auto todo: use portal to handle long list return (
= ({ showManageInputField={showManageRagInputFields} onManageInputField={() => setShowInputFieldPanel?.(true)} preferSchemaType={preferSchemaType} + hideSearch={hideSearch} /> )}
diff --git a/web/app/components/workflow/nodes/llm/components/config-context-item.tsx b/web/app/components/workflow/nodes/llm/components/config-context-item.tsx new file mode 100644 index 0000000000..77f9b06981 --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/config-context-item.tsx @@ -0,0 +1,129 @@ +'use client' +import type { FC } from 'react' +import type { PromptMessageContext, ValueSelector } from '../../../types' +import type { Node, NodeOutPutVar, Var } from '@/app/components/workflow/types' +import { RiArrowDownSLine, RiDeleteBinLine } from '@remixicon/react' +import { memo, useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' +import VariableLabelInSelect from '@/app/components/workflow/nodes/_base/components/variable/variable-label/variable-label-in-select' +import { BlockEnum } from '@/app/components/workflow/types' +import { cn } from '@/utils/classnames' + +type Props = { + readOnly: boolean + payload: PromptMessageContext + contextVars: NodeOutPutVar[] + availableNodes: Node[] + onChange: (value: ValueSelector) => void + onRemove: () => void +} + +const ConfigContextItem: FC = ({ + readOnly, + payload, + contextVars, + availableNodes, + onChange, + onRemove, +}) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + const selectedNodeId = Array.isArray(payload.$context) ? payload.$context[0] : '' + const selectedNode = useMemo(() => { + return availableNodes.find(node => node.id === selectedNodeId) + }, [availableNodes, selectedNodeId]) + const hasOptions = contextVars.length > 0 + + const handleChange = useCallback((value: ValueSelector, _item?: Var) => { + onChange(value) + setOpen(false) + }, [onChange]) + + const handleToggle = useCallback(() => { + if (readOnly) + return + setOpen(prev => !prev) + }, [readOnly]) + + const handleRemove = useCallback(() => { + onRemove() + setOpen(false) + }, [onRemove]) + + return ( + + + + + +
+ {hasOptions + ? ( + setOpen(false)} + onBlur={() => setOpen(false)} + autoFocus={false} + preferSchemaType + /> + ) + : ( +
+ {t('common.noAgentNodes', { ns: 'workflow' })} +
+ )} + {!readOnly && ( +
+ +
+ )} +
+
+
+ ) +} + +export default memo(ConfigContextItem) diff --git a/web/app/components/workflow/nodes/llm/components/config-prompt.tsx b/web/app/components/workflow/nodes/llm/components/config-prompt.tsx index 5b28c9b48f..147e69b6e1 100644 --- a/web/app/components/workflow/nodes/llm/components/config-prompt.tsx +++ b/web/app/components/workflow/nodes/llm/components/config-prompt.tsx @@ -1,19 +1,28 @@ 'use client' import type { FC } from 'react' -import type { ModelConfig, PromptItem, ValueSelector, Var, Variable } from '../../../types' +import type { ModelConfig, Node, NodeOutPutVar, PromptItem, PromptMessageContext, PromptTemplateItem, ValueSelector, Var, Variable } from '../../../types' import { produce } from 'immer' import * as React from 'react' -import { useCallback } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' +import { useStoreApi } from 'reactflow' import { v4 as uuid4 } from 'uuid' import { DragHandle } from '@/app/components/base/icons/src/vender/line/others' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' import AddButton from '@/app/components/workflow/nodes/_base/components/add-button' import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' +import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' import { cn } from '@/utils/classnames' -import { useWorkflowStore } from '../../../store' -import { EditionType, PromptRole } from '../../../types' +import { useWorkflow } from '../../../hooks' +import { useStore, useWorkflowStore } from '../../../store' +import { BlockEnum, EditionType, isPromptMessageContext, PromptRole, VarType } from '../../../types' import useAvailableVarList from '../../_base/hooks/use-available-var-list' +import ConfigContextItem from './config-context-item' import ConfigPromptItem from './config-prompt-item' const i18nPrefix = 'nodes.llm' @@ -24,8 +33,8 @@ type Props = { filterVar: (payload: Var, selector: ValueSelector) => boolean isChatModel: boolean isChatApp: boolean - payload: PromptItem | PromptItem[] - onChange: (payload: PromptItem | PromptItem[]) => void + payload: PromptItem | PromptTemplateItem[] + onChange: (payload: PromptItem | PromptTemplateItem[]) => void isShowContext: boolean hasSetBlockStatus: { context: boolean @@ -56,6 +65,13 @@ const ConfigPrompt: FC = ({ const { setControlPromptEditorRerenderKey, } = workflowStore.getState() + + const store = useStoreApi() + const { getBeforeNodesInSameBranch } = useWorkflow() + + const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) + const contextMenuTriggerRef = useRef(null) + const payloadWithIds = (isChatModel && Array.isArray(payload)) ? payload.map((item) => { const id = uuid4() @@ -75,11 +91,78 @@ const ConfigPrompt: FC = ({ onlyLeafNodeVar: false, filterVar, }) + const parentAvailableVars = useStore(state => state.parentAvailableVars) || [] + const parentAvailableNodes = useStore(state => state.parentAvailableNodes) || [] + + const mergedAvailableVars = useMemo(() => { + if (!parentAvailableVars.length) + return availableVars + const merged = new Map() + availableVars.forEach((item) => { + merged.set(item.nodeId, item) + }) + parentAvailableVars.forEach((item) => { + if (!merged.has(item.nodeId)) + merged.set(item.nodeId, item) + }) + return Array.from(merged.values()) + }, [availableVars, parentAvailableVars]) + + const mergedAvailableNodesWithParent = useMemo(() => { + if (!parentAvailableNodes.length) + return availableNodesWithParent + const merged = new Map() + availableNodesWithParent.forEach((node) => { + merged.set(node.id, node) + }) + parentAvailableNodes.forEach((node) => { + if (!merged.has(node.id)) + merged.set(node.id, node) + }) + return Array.from(merged.values()) + }, [availableNodesWithParent, parentAvailableNodes]) + + const contextAgentNodes = useMemo(() => { + const agentNodes = mergedAvailableNodesWithParent + .filter(node => node.data.type === BlockEnum.Agent) + + const { getNodes } = store.getState() + const allNodes = getNodes() + const currentNode = allNodes.find(n => n.id === nodeId) + const parentNodeId = currentNode?.parentId + + if (parentNodeId) { + const beforeNodes = getBeforeNodesInSameBranch(parentNodeId) + const parentAgentNodes = beforeNodes + .filter(node => node.data.type === BlockEnum.Agent) + .filter(node => !agentNodes.some(n => n.id === node.id)) + + agentNodes.unshift(...parentAgentNodes) + } + + return agentNodes + }, [mergedAvailableNodesWithParent, nodeId, store, getBeforeNodesInSameBranch]) + + const contextVarOptions = useMemo(() => { + return contextAgentNodes.map(node => ({ + nodeId: node.id, + title: node.data.title, + vars: [ + { + variable: 'context', + type: VarType.arrayObject, + schemaType: 'List[promptMessage]', + }, + ], + })) + }, [contextAgentNodes]) const handleChatModePromptChange = useCallback((index: number) => { return (prompt: string) => { - const newPrompt = produce(payload as PromptItem[], (draft) => { - draft[index][draft[index].edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt + const newPrompt = produce(payload as PromptTemplateItem[], (draft) => { + const item = draft[index] + if (!isPromptMessageContext(item)) + item[item.edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt }) onChange(newPrompt) } @@ -87,8 +170,10 @@ const ConfigPrompt: FC = ({ const handleChatModeEditionTypeChange = useCallback((index: number) => { return (editionType: EditionType) => { - const newPrompt = produce(payload as PromptItem[], (draft) => { - draft[index].edition_type = editionType + const newPrompt = produce(payload as PromptTemplateItem[], (draft) => { + const item = draft[index] + if (!isPromptMessageContext(item)) + item.edition_type = editionType }) onChange(newPrompt) } @@ -96,29 +181,80 @@ const ConfigPrompt: FC = ({ const handleChatModeMessageRoleChange = useCallback((index: number) => { return (role: PromptRole) => { - const newPrompt = produce(payload as PromptItem[], (draft) => { - draft[index].role = role + const newPrompt = produce(payload as PromptTemplateItem[], (draft) => { + const item = draft[index] + if (!isPromptMessageContext(item)) + item.role = role }) onChange(newPrompt) } }, [onChange, payload]) const handleAddPrompt = useCallback(() => { - const newPrompt = produce(payload as PromptItem[], (draft) => { + const newPrompt = produce(payload as PromptTemplateItem[], (draft) => { if (draft.length === 0) { draft.push({ role: PromptRole.system, text: '', id: uuid4() }) - return } - const isLastItemUser = draft[draft.length - 1].role === PromptRole.user + const lastPromptItem = [...draft].reverse().find(item => !isPromptMessageContext(item)) as PromptItem | undefined + const isLastItemUser = lastPromptItem?.role === PromptRole.user draft.push({ role: isLastItemUser ? PromptRole.assistant : PromptRole.user, text: '', id: uuid4() }) }) onChange(newPrompt) }, [onChange, payload]) + const handleAddContext = useCallback((agentNodeId: string) => { + const newPrompt = produce(payload as PromptTemplateItem[], (draft) => { + const contextItem: PromptMessageContext = { + id: uuid4(), + $context: [agentNodeId, 'context'], + } + + const lastUserIndex = draft + .map((item, idx) => ({ item, idx })) + .reverse() + .find(({ item }) => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.user) + ?.idx + + if (lastUserIndex !== undefined) { + draft.splice(lastUserIndex, 0, contextItem) + return + } + + const promptItems = draft.filter(item => !isPromptMessageContext(item)) as PromptItem[] + const hasOnlySystem = promptItems.length === 1 && promptItems[0].role === PromptRole.system + if (hasOnlySystem) { + draft.push({ role: PromptRole.user, text: '', id: uuid4() }) + draft.splice(draft.length - 1, 0, contextItem) + return + } + + draft.push(contextItem) + }) + onChange(newPrompt) + setIsContextMenuOpen(false) + }, [onChange, payload]) + + const handleAddContextVar = useCallback((value: ValueSelector, _item?: Var) => { + if (!Array.isArray(value) || value.length < 2) + return + handleAddContext(value[0]) + }, [handleAddContext]) + + const handleContextChange = useCallback((index: number) => { + return (value: ValueSelector) => { + const newPrompt = produce(payload as PromptTemplateItem[], (draft) => { + const item = draft[index] + if (isPromptMessageContext(item)) + item.$context = value + }) + onChange(newPrompt) + } + }, [onChange, payload]) + const handleRemove = useCallback((index: number) => { return () => { - const newPrompt = produce(payload as PromptItem[], (draft) => { + const newPrompt = produce(payload as PromptTemplateItem[], (draft) => { draft.splice(index, 1) }) onChange(newPrompt) @@ -145,11 +281,12 @@ const ConfigPrompt: FC = ({ }, [onChange, payload]) const canChooseSystemRole = (() => { - if (isChatModel && Array.isArray(payload)) - return !payload.find(item => item.role === PromptRole.system) - + if (isChatModel && Array.isArray(payload)) { + return !payload.find(item => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.system) + } return false })() + return (
{(isChatModel && Array.isArray(payload)) @@ -160,9 +297,12 @@ const ConfigPrompt: FC = ({ className="space-y-1" list={payloadWithIds} setList={(list) => { - if ((payload as PromptItem[])?.[0]?.role === PromptRole.system && list[0].p?.role !== PromptRole.system) - return - + const firstItem = (payload as PromptTemplateItem[])?.[0] + if (firstItem && !isPromptMessageContext(firstItem) && firstItem.role === PromptRole.system) { + const newFirstItem = list[0]?.p + if (newFirstItem && !isPromptMessageContext(newFirstItem) && newFirstItem.role !== PromptRole.system) + return + } onChange(list.map(item => item.p)) }} handle=".handle" @@ -170,7 +310,23 @@ const ConfigPrompt: FC = ({ animation={150} > { - (payload as PromptItem[]).map((item, index) => { + (payload as PromptTemplateItem[]).map((item, index) => { + if (isPromptMessageContext(item)) { + return ( +
+ {!readOnly && } + +
+ ) + } + const canDrag = (() => { if (readOnly) return false @@ -182,7 +338,7 @@ const ConfigPrompt: FC = ({ })() return (
- {canDrag && } + {canDrag && } = ({ onRemove={handleRemove(index)} isShowContext={isShowContext} hasSetBlockStatus={hasSetBlockStatus} - availableVars={availableVars} - availableNodes={availableNodesWithParent} + availableVars={mergedAvailableVars} + availableNodes={mergedAvailableNodesWithParent} varList={varList} handleAddVariable={handleAddVariable} modelConfig={modelConfig} @@ -213,11 +369,48 @@ const ConfigPrompt: FC = ({ }
- +
+ + + setIsContextMenuOpen(!isContextMenuOpen)}> +
+ {}} + /> +
+
+ +
+ {contextVarOptions.length > 0 + ? ( + setIsContextMenuOpen(false)} + onBlur={() => setIsContextMenuOpen(false)} + autoFocus={false} + preferSchemaType + /> + ) + : ( +
+ {t('common.noAgentNodes', { ns: 'workflow' })} +
+ )} +
+
+
+
) : ( @@ -232,8 +425,8 @@ const ConfigPrompt: FC = ({ isChatApp={isChatApp} isShowContext={isShowContext} hasSetBlockStatus={hasSetBlockStatus} - nodesOutputVars={availableVars} - availableNodes={availableNodesWithParent} + nodesOutputVars={mergedAvailableVars} + availableNodes={mergedAvailableNodesWithParent} isSupportPromptGenerator isSupportJinja editionType={(payload as PromptItem).edition_type} diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index 670d3149be..553f7ca599 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -5,6 +5,7 @@ import { RiAlertFill, RiQuestionLine } from '@remixicon/react' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import Badge from '@/app/components/base/badge' import AddButton2 from '@/app/components/base/button/add-button' import Switch from '@/app/components/base/switch' import Toast from '@/app/components/base/toast' @@ -119,7 +120,12 @@ const Panel: FC> = ({ {/* knowledge */} +
{t(`${i18nPrefix}.context`, { ns: 'workflow' })}
+ LEGACY + + )} tooltip={t(`${i18nPrefix}.contextTooltip`, { ns: 'workflow' })!} > <> @@ -130,6 +136,7 @@ const Panel: FC> = ({ value={inputs.context?.variable_selector || []} onChange={handleContextVarChange} filterVar={filterVar} + hideSearch /> {shouldShowContextTip && (
{t(`${i18nPrefix}.notSetContextInPromptTip`, { ns: 'workflow' })}
diff --git a/web/app/components/workflow/nodes/llm/types.ts b/web/app/components/workflow/nodes/llm/types.ts index 70dc4d9cc7..5b15f83ac6 100644 --- a/web/app/components/workflow/nodes/llm/types.ts +++ b/web/app/components/workflow/nodes/llm/types.ts @@ -1,8 +1,8 @@ -import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Variable, VisionSetting } from '@/app/components/workflow/types' +import type { CommonNodeType, Memory, ModelConfig, PromptItem, PromptTemplateItem, ValueSelector, Variable, VisionSetting } from '@/app/components/workflow/types' export type LLMNodeType = CommonNodeType & { model: ModelConfig - prompt_template: PromptItem[] | PromptItem + prompt_template: PromptTemplateItem[] | PromptItem prompt_config?: { jinja2_variables?: Variable[] } diff --git a/web/app/components/workflow/nodes/llm/use-config.ts b/web/app/components/workflow/nodes/llm/use-config.ts index e885f108bb..6922a8989f 100644 --- a/web/app/components/workflow/nodes/llm/use-config.ts +++ b/web/app/components/workflow/nodes/llm/use-config.ts @@ -1,4 +1,4 @@ -import type { Memory, PromptItem, ValueSelector, Var, Variable } from '../../types' +import type { Memory, PromptItem, PromptTemplateItem, ValueSelector, Var, Variable } from '../../types' import type { LLMNodeType, StructuredOutput } from './types' import { produce } from 'immer' import { useCallback, useEffect, useRef, useState } from 'react' @@ -249,7 +249,7 @@ const useConfig = (id: string, payload: LLMNodeType) => { setInputs(newInputs) }, [setInputs]) - const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => { + const handlePromptChange = useCallback((newPrompt: PromptTemplateItem[] | PromptItem) => { const newInputs = produce(inputRef.current, (draft) => { draft.prompt_template = newPrompt }) diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx index d052fba728..fc793db9e4 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/index.tsx @@ -10,12 +10,12 @@ import { RiCloseLine } from '@remixicon/react' import { noop } from 'es-toolkit/function' import { Fragment, memo, useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useStoreApi } from 'reactflow' +import { useStore as useReactFlowStore, useStoreApi } from 'reactflow' import { Agent } from '@/app/components/base/icons/src/vender/workflow' -import { useNodesSyncDraft } from '@/app/components/workflow/hooks' +import { useIsChatMode, useNodesSyncDraft, useWorkflow, useWorkflowVariables } from '@/app/components/workflow/hooks' import { VarKindType } from '@/app/components/workflow/nodes/_base/types' -import { useStore } from '@/app/components/workflow/store' -import { EditionType, PromptRole } from '@/app/components/workflow/types' +import { useStore as useWorkflowStore } from '@/app/components/workflow/store' +import { BlockEnum, EditionType, PromptRole } from '@/app/components/workflow/types' import SubGraphCanvas from './sub-graph-canvas' const SubGraphModal: FC = ({ @@ -29,9 +29,13 @@ const SubGraphModal: FC = ({ }) => { const { t } = useTranslation() const reactflowStore = useStoreApi() - const workflowNodes = useStore(state => state.nodes) - const setControlPromptEditorRerenderKey = useStore(state => state.setControlPromptEditorRerenderKey) + const workflowNodes = useWorkflowStore(state => state.nodes) + const workflowEdges = useReactFlowStore(state => state.edges) + const setControlPromptEditorRerenderKey = useWorkflowStore(state => state.setControlPromptEditorRerenderKey) const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const { getBeforeNodesInSameBranch } = useWorkflow() + const { getNodeAvailableVars } = useWorkflowVariables() + const isChatMode = useIsChatMode() const extractorNodeId = `${toolNodeId}_ext_${paramKey}` const extractorNode = useMemo(() => { @@ -43,6 +47,28 @@ const SubGraphModal: FC = ({ const toolParam = (toolNode?.data as ToolNodeType | undefined)?.tool_parameters?.[paramKey] const toolParamValue = toolParam?.value as string | undefined + const parentAgentNodes = useMemo(() => { + if (!isOpen) + return [] + const beforeNodes = getBeforeNodesInSameBranch(toolNodeId, workflowNodes, workflowEdges) + return beforeNodes.filter(node => node.data.type === BlockEnum.Agent) + }, [getBeforeNodesInSameBranch, isOpen, toolNodeId, workflowEdges, workflowNodes]) + + const parentAgentNodeIds = useMemo(() => { + return parentAgentNodes.map(node => node.id) + }, [parentAgentNodes]) + + const parentAvailableVars = useMemo(() => { + if (!parentAgentNodeIds.length) + return [] + const vars = getNodeAvailableVars({ + beforeNodes: parentAgentNodes, + isChatMode, + filterVar: () => true, + }) + return vars.filter(nodeVar => parentAgentNodeIds.includes(nodeVar.nodeId)) + }, [getNodeAvailableVars, isChatMode, parentAgentNodeIds, parentAgentNodes]) + const mentionConfig = useMemo(() => { const current = toolParam?.mention_config const rawSelector = Array.isArray(current?.output_selector) ? current!.output_selector : [] @@ -115,8 +141,7 @@ const SubGraphModal: FC = ({ const userPrompt = promptTemplate.find(item => item.role === PromptRole.user) if (userPrompt) return resolveText(userPrompt) - const systemPrompt = promptTemplate.find(item => item.role === PromptRole.system) - return resolveText(systemPrompt) + return '' } return resolveText(promptTemplate) }, []) @@ -212,6 +237,8 @@ const SubGraphModal: FC = ({ onMentionConfigChange={handleMentionConfigChange} extractorNode={extractorNode} toolParamValue={toolParamValue} + parentAvailableNodes={parentAgentNodes} + parentAvailableVars={parentAvailableVars} onSave={handleSave} /> diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx index e838ddbceb..04a19c88fc 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx @@ -14,6 +14,8 @@ const SubGraphCanvas: FC = ({ onMentionConfigChange, extractorNode, toolParamValue, + parentAvailableNodes, + parentAvailableVars, onSave, }) => { return ( @@ -28,6 +30,8 @@ const SubGraphCanvas: FC = ({ onMentionConfigChange={onMentionConfigChange} extractorNode={extractorNode} toolParamValue={toolParamValue} + parentAvailableNodes={parentAvailableNodes} + parentAvailableVars={parentAvailableVars} onSave={onSave} /> diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts index b4e3dada85..ae9e227458 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts @@ -1,6 +1,6 @@ import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types' import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' -import type { Edge as WorkflowEdge, Node as WorkflowNode } from '@/app/components/workflow/types' +import type { NodeOutPutVar, Edge as WorkflowEdge, Node as WorkflowNode } from '@/app/components/workflow/types' type WorkflowValueSelector = string[] @@ -24,5 +24,7 @@ export type SubGraphCanvasProps = { onMentionConfigChange: (config: MentionConfig) => void extractorNode?: WorkflowNode toolParamValue?: string + parentAvailableNodes?: WorkflowNode[] + parentAvailableVars?: NodeOutPutVar[] onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void } diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 02ee45aa7a..b4bd68b1ee 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -255,6 +255,17 @@ export type PromptItem = { jinja2_text?: string } +export type PromptMessageContext = { + id?: string + $context: ValueSelector +} + +export type PromptTemplateItem = PromptItem | PromptMessageContext + +export const isPromptMessageContext = (item: PromptTemplateItem): item is PromptMessageContext => { + return '$context' in item +} + export enum MemoryRole { user = 'user', assistant = 'assistant', diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index ec2e838feb..315179cd9c 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -625,8 +625,10 @@ "nodes.listFilter.outputVars.last_record": "Last record", "nodes.listFilter.outputVars.result": "Filter result", "nodes.listFilter.selectVariableKeyPlaceholder": "Select sub variable key", + "nodes.llm.addContext": "Add Context", "nodes.llm.addMessage": "Add Message", "nodes.llm.context": "context", + "nodes.llm.contextBlock": "Context Block", "nodes.llm.contextTooltip": "You can import Knowledge as context", "nodes.llm.files": "Files", "nodes.llm.jsonSchema.addChildField": "Add Child Field", @@ -663,6 +665,7 @@ "nodes.llm.reasoningFormat.tagged": "Keep think tags", "nodes.llm.reasoningFormat.title": "Enable reasoning tag separation", "nodes.llm.reasoningFormat.tooltip": "Extract content from think tags and store it in the reasoning_content field.", + "nodes.llm.removeContext": "Remove context", "nodes.llm.resolution.high": "High", "nodes.llm.resolution.low": "Low", "nodes.llm.resolution.name": "Resolution", @@ -999,8 +1002,8 @@ "subGraphModal.sourceNode": "SOURCE", "subGraphModal.title": "INTERNAL STRUCTURE", "subGraphModal.whenOutputIsNone": "WHEN OUTPUT IS NONE", - "subGraphModal.whenOutputNone.default": "Use default value", - "subGraphModal.whenOutputNone.defaultDesc": "Continue with a default value", + "subGraphModal.whenOutputNone.default": "Default value", + "subGraphModal.whenOutputNone.defaultDesc": "Returns the value below", "subGraphModal.whenOutputNone.error": "Raise an error", "subGraphModal.whenOutputNone.errorDesc": "Pass the error to the outer workflow", "subGraphModal.whenOutputNone.skip": "Skip this step", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index 555867256d..f74a865eaf 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -623,8 +623,10 @@ "nodes.listFilter.outputVars.last_record": "最後のレコード", "nodes.listFilter.outputVars.result": "フィルター結果", "nodes.listFilter.selectVariableKeyPlaceholder": "サブ変数キーを選択する", + "nodes.llm.addContext": "コンテキスト追加", "nodes.llm.addMessage": "メッセージ追加", "nodes.llm.context": "コンテキスト", + "nodes.llm.contextBlock": "コンテキストブロック", "nodes.llm.contextTooltip": "ナレッジベースをコンテキストとして利用", "nodes.llm.files": "ファイル", "nodes.llm.jsonSchema.addChildField": "サブフィールドを追加", @@ -661,6 +663,7 @@ "nodes.llm.reasoningFormat.tagged": "タグを考え続けてください", "nodes.llm.reasoningFormat.title": "推論タグの分離を有効にする", "nodes.llm.reasoningFormat.tooltip": "thinkタグから内容を抽出し、それをreasoning_contentフィールドに保存します。", + "nodes.llm.removeContext": "コンテキストを削除", "nodes.llm.resolution.high": "高", "nodes.llm.resolution.low": "低", "nodes.llm.resolution.name": "解像度", @@ -996,8 +999,8 @@ "subGraphModal.sourceNode": "ソース", "subGraphModal.title": "内部構造", "subGraphModal.whenOutputIsNone": "出力が空の場合", - "subGraphModal.whenOutputNone.default": "デフォルト値を使用", - "subGraphModal.whenOutputNone.defaultDesc": "デフォルト値で続行", + "subGraphModal.whenOutputNone.default": "デフォルト値", + "subGraphModal.whenOutputNone.defaultDesc": "以下の値を返す", "subGraphModal.whenOutputNone.error": "エラーを発生させる", "subGraphModal.whenOutputNone.errorDesc": "エラーを外部ワークフローに渡す", "subGraphModal.whenOutputNone.skip": "このステップをスキップ", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index cebe1cede5..7eb9f556dc 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -623,8 +623,10 @@ "nodes.listFilter.outputVars.last_record": "最后一条记录", "nodes.listFilter.outputVars.result": "过滤结果", "nodes.listFilter.selectVariableKeyPlaceholder": "选择子变量的 Key", + "nodes.llm.addContext": "添加上下文", "nodes.llm.addMessage": "添加消息", "nodes.llm.context": "上下文", + "nodes.llm.contextBlock": "上下文块", "nodes.llm.contextTooltip": "您可以导入知识库作为上下文", "nodes.llm.files": "文件", "nodes.llm.jsonSchema.addChildField": "添加子字段", @@ -661,6 +663,7 @@ "nodes.llm.reasoningFormat.tagged": "保持思考标签", "nodes.llm.reasoningFormat.title": "启用推理标签分离", "nodes.llm.reasoningFormat.tooltip": "从think标签中提取内容,并将其存储在reasoning_content字段中。", + "nodes.llm.removeContext": "删除上下文", "nodes.llm.resolution.high": "高", "nodes.llm.resolution.low": "低", "nodes.llm.resolution.name": "分辨率", @@ -997,8 +1000,8 @@ "subGraphModal.sourceNode": "来源", "subGraphModal.title": "内部结构", "subGraphModal.whenOutputIsNone": "当输出为空时", - "subGraphModal.whenOutputNone.default": "使用默认值", - "subGraphModal.whenOutputNone.defaultDesc": "使用默认值继续执行", + "subGraphModal.whenOutputNone.default": "默认值", + "subGraphModal.whenOutputNone.defaultDesc": "返回以下值", "subGraphModal.whenOutputNone.error": "抛出错误", "subGraphModal.whenOutputNone.errorDesc": "将错误传递给外部工作流", "subGraphModal.whenOutputNone.skip": "跳过此步骤", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index 0d181bb69b..cebfdd2feb 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -623,8 +623,10 @@ "nodes.listFilter.outputVars.last_record": "最後一條記錄", "nodes.listFilter.outputVars.result": "篩選結果", "nodes.listFilter.selectVariableKeyPlaceholder": "Select sub variable key(選擇子變數鍵)", + "nodes.llm.addContext": "新增上下文", "nodes.llm.addMessage": "新增消息", "nodes.llm.context": "上下文", + "nodes.llm.contextBlock": "上下文區塊", "nodes.llm.contextTooltip": "您可以導入知識庫作為上下文", "nodes.llm.files": "文件", "nodes.llm.jsonSchema.addChildField": "新增子欄位", @@ -661,6 +663,7 @@ "nodes.llm.reasoningFormat.tagged": "保持思考標籤", "nodes.llm.reasoningFormat.title": "啟用推理標籤分離", "nodes.llm.reasoningFormat.tooltip": "從 think 標籤中提取內容並將其存儲在 reasoning_content 欄位中。", + "nodes.llm.removeContext": "刪除上下文", "nodes.llm.resolution.high": "高", "nodes.llm.resolution.low": "低", "nodes.llm.resolution.name": "分辨率", @@ -996,8 +999,8 @@ "subGraphModal.sourceNode": "來源", "subGraphModal.title": "內部結構", "subGraphModal.whenOutputIsNone": "當輸出為空時", - "subGraphModal.whenOutputNone.default": "使用預設值", - "subGraphModal.whenOutputNone.defaultDesc": "使用預設值繼續執行", + "subGraphModal.whenOutputNone.default": "預設值", + "subGraphModal.whenOutputNone.defaultDesc": "返回以下值", "subGraphModal.whenOutputNone.error": "拋出錯誤", "subGraphModal.whenOutputNone.errorDesc": "將錯誤傳遞給外部工作流程", "subGraphModal.whenOutputNone.skip": "跳過此步驟",